This guide covers one flow end to end. A user's EUR account is topped up by card, and money is paid out from it to a card, in EUR. Nothing is converted on the way in or out: the card is charged in EUR, the account holds EUR, and the recipient is paid in EUR.

You collect the card details in your own interface and send them to PayCA over the API. Card data never goes through a PayCA page, apart from the 3-D Secure step the card issuer may ask for.

How It Works #

  1. The user has an EUR account at PayCA.
  2. Top-up: the user enters a card in your interface. You quote the top-up, then create it with the card. If the issuer asks for 3-D Secure, you redirect the user to confirm. Once the user has paid, the top-up is processing. The account is credited, and the top-up becomes completed, only when the card acquirer settles the payment: by default four business days later (see Settlement).
  3. Payout: the user enters the card to receive money. You quote the payout, then create it. PayCA holds the full amount on the account and pays it out. The hold becomes a debit once the payout is confirmed. A payout draws only on the available balance, so top-ups that are still settling cannot be paid out yet.
  4. Webhooks tell you the final outcome of each top-up and payout.

Before You Start #

  • PCI DSS. Card numbers and CVV pass through your systems. The systems that collect and transmit them must be PCI DSS compliant. Never store the CVV, and never write card data to logs.
  • Enabled by PayCA. PayCA enables card top-ups and payouts for your client. GET /v1/client/settings lists EUR in cardFundingCurrencies once top-ups are on. If EUR top-ups go through an acquirer you select per request rather than your default, EUR is listed under that acquirer in cardAcquirers.currencies instead: see Selecting the acquirer.
  • Redirect URLs. HTTPS prefixes for your return pages must be allow-listed by PayCA. The user comes back to them after 3-D Secure.
  • Payer profile. Top-ups and payouts need the payer's full profile: first and last name, email, phone in international format, date of birth, city, address, postal code and country. For a user, send it when you create the user, or in each request. On your own account, send it in each request.
  • Device IP. Every top-up and payout needs the IP address of the user's device, as your server sees it. Do not take it from a header the user's browser can set.
  • Webhooks. Register an endpoint for deposit and one for payout. See Webhooks.

API authentication, users and accounts are described in Getting Started and API Authentication.

Step 1 - The User's EUR Account #

Open the EUR account next to the account the user already has. The call is safe to repeat: an existing account is returned unchanged.

curl -s -X POST "$PAYCA_BASE_URL/v1/users/$USER_ID/accounts" \
  -H "Content-Type: application/json" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET" \
  -d '{ "currency": "EUR" }'
{
  "id": "0b8f5c7e-2f4d-4a51-9b1e-3c6a7d9e0f12",
  "currency": "EUR",
  "balance": { "available": "0.00", "pending": "0.00" },
  "status": "active"
}

Later you find it in otherAccounts of GET /v1/users/{id}. The steps below use this account's id as $EUR_ACCOUNT_ID. More on accounts in several currencies: Accounts in Several Currencies and Exchange.

Or one account for all your payers #

You don't have to keep an account per payer. If you hold the balance yourself, for example as a merchant that takes card payments from its customers and pays them out, top up and pay out through your own EUR account and name the payer in each request:

  • Your EUR account is your client-level account, not bound to a user. PayCA opens it when it enables card top-ups and payouts for you, and gives you its id. GET /v1/accounts does not list it; read its balance with GET /v2/accounts/{accountId}/balance.
  • Your payers are not PayCA users. You do not create them with POST /v1/users, and PayCA creates nothing for them.
  • Every top-up and payout on this account carries payerReference: your own stable id of the payer, 1–128 characters, for example the customer id in your system. One person is always the same payerReference; different people always have different ones. The card network checks payers individually, so payments from different people must never share a payerReference. Use an opaque id, not an email, phone number or name: it appears in logs and webhooks.
  • The payer's full profile goes in every request: payer in a top-up, recipient and ipAddress in a payout. The fields are the same as in Before You Start. PayCA keeps nothing between requests for such a payer, so a field you leave out is not filled in from an earlier request.
  • A request on this account with neither payerReference nor userId is refused with 400. userId is accepted here only if it names an existing user of yours.
  • The top-up and the payout return payerReference, and so do their webhooks.

The body of Create with the card on your own account (the quote's expected… values are left out here for brevity):

{
  "method": "card",
  "amount": "100.00",
  "country": "DE",
  "payerReference": "customer-58213",
  "payer": {
    "email": "alex.morgan@example.com",
    "firstName": "Alex",
    "lastName": "Morgan",
    "phone": "+4915112345678",
    "dateOfBirth": "1990-04-12",
    "city": "Berlin",
    "postalCode": "10115",
    "address": "Example Strasse 1",
    "ipAddress": "203.0.113.7"
  },
  "card": { "number": "4111111111111111", "holder": "ALEX MORGAN", "expiryMonth": "08", "expiryYear": "2028", "cvv": "737" },
  "completeRedirectUrl": "https://app.example.com/topup/complete",
  "cancelRedirectUrl": "https://app.example.com/topup/cancel"
}

A payout to the same person uses the same "payerReference": "customer-58213", with recipient and ipAddress as in the payout request.

Everything else in this guide is the same for both models. The steps below say $EUR_ACCOUNT_ID for whichever account you use.

Step 2 - Top Up by Card #

Check the option #

curl -s "$PAYCA_BASE_URL/v2/accounts/$EUR_ACCOUNT_ID/funding-options?country=DE" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET"

country is the user's billing country.

{
  "options": [
    {
      "method": "card",
      "currency": "EUR",
      "minAmount": "10.00",
      "maxAmount": "1000.00",
      "cardDetailsRequired": true
    }
  ]
}

cardDetailsRequired: true means you send the card with the create request. minAmount and maxAmount bound what the card is charged. An empty list means card top-ups are not available for this account.

Selecting the acquirer #

If PayCA allows your client more than one card acquirer, you may select per request which of them may serve the top-up: the acquirers query parameter here, and the acquirers body field in the quote and the create. Take the codes from cardAcquirers of GET /v1/client/settings: the one listing EUR under currencies serves EUR top-ups. Leave the parameter out to be served by your default acquirer, as before. An empty selection means no acquirer, and card top-ups are then not available for the request. Send the same selection to the options, the quote and the create. Details: Choosing the Card Acquirer per Request.

Quote #

curl -s -X POST "$PAYCA_BASE_URL/v2/accounts/$EUR_ACCOUNT_ID/deposit-quote" \
  -H "Content-Type: application/json" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET" \
  -d '{
    "method": "card",
    "amount": "100.00",
    "amountMode": "credited",
    "country": "DE"
  }'
{
  "requestedAmount": "100.00",
  "amountMode": "credited",
  "currency": "EUR",
  "processingFee": "5.85",
  "providerFee": "0",
  "declineFee": "0.50",
  "refundFee": "1.00",
  "chargebackFee": "0",
  "creditedAmount": "100.00",
  "chargedAmount": "105.85"
}

With amountMode: credited, amount is what the account receives. With charged, it is what the card is charged. Show the user chargedAmount, creditedAmount and processingFee before they confirm. declineFee, refundFee and chargebackFee are charged to the account only if the payment is declined, later refunded or charged back.

Create with the card #

curl -s -X POST "$PAYCA_BASE_URL/v2/accounts/$EUR_ACCOUNT_ID/deposits" \
  -H "Content-Type: application/json" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET" \
  -H "Idempotency-Key: topup-20260929-0001" \
  -d '{
    "method": "card",
    "amount": "100.00",
    "expectedChargedAmount": "105.85",
    "expectedCreditedAmount": "100.00",
    "expectedPaycaFee": "5.85",
    "expectedProviderFee": "0",
    "expectedDeclineFee": "0.50",
    "expectedRefundFee": "1.00",
    "expectedChargebackFee": "0",
    "country": "DE",
    "payer": {
      "email": "alex.morgan@example.com",
      "firstName": "Alex",
      "lastName": "Morgan",
      "phone": "+4915112345678",
      "dateOfBirth": "1990-04-12",
      "city": "Berlin",
      "postalCode": "10115",
      "address": "Example Strasse 1",
      "ipAddress": "203.0.113.7"
    },
    "card": {
      "number": "4111111111111111",
      "holder": "ALEX MORGAN",
      "expiryMonth": "08",
      "expiryYear": "2028",
      "cvv": "737"
    },
    "completeRedirectUrl": "https://app.example.com/topup/complete",
    "cancelRedirectUrl": "https://app.example.com/topup/cancel"
  }'
  • amount and the expected… values come from the quote. If the price changed in the meantime, the request is refused with 409. Quote again.
  • payer completes the profile PayCA holds for the user. Anything still missing is listed in a 400, and nothing is created.
  • card: Visa or Mastercard only. number is digits only, expiryMonth is 01–12, expiryYear has four digits, cvv has three or four. A card that fails these checks is refused with 400, and nothing is created.
  • On a user's account the payer is the user, and userId may be omitted. On your own account, add payerReference and send the whole payer: see Or one account for all your payers.

The response is the deposit (201). What happens next depends on the card issuer:

  • 3-D Secure requested. The deposit carries checkoutUrl. Redirect the user's browser to it. After confirming, the user returns to completeRedirectUrl, or to cancelRedirectUrl if the payment did not go through.
  • No 3-D Secure. There is no checkoutUrl. The result arrives within seconds: poll the deposit or wait for the webhook.

The return to your page is not the result. Always read the deposit's status:

curl -s "$PAYCA_BASE_URL/v2/accounts/$EUR_ACCOUNT_ID/deposits/$DEPOSIT_ID" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET"

Top-up statuses #

Status Meaning Credited?
created Recorded. If checkoutUrl is present, the payment waits for 3-D Secure. No
awaiting_payment The payment is being processed. No
processing The user has paid. PayCA waits for the card acquirer to settle the payment, by default four business days, then credits the account. No
completed The account is credited with creditedAmount. Yes
failed The payment was declined. declineFee is debited from the account. No
cancelled The payment was cancelled. No
expired 3-D Secure was not completed in time. No
refunded The top-up was refunded. If it was already credited, the credit is reversed. refundFee is debited either way. Reversed, if credited
chargeback A completed top-up was charged back. PayCA records it when the acquirer reports it. The credit is reversed and chargebackFee is debited. Reversed
manual_review PayCA is reviewing the top-up. Wait for the next status. No

Settlement #

A successful payment is not credited straight away. PayCA credits a top-up only after the card acquirer has settled it: by default four business days after the payment. Business days are Monday to Friday in UTC; public holidays are not taken into account. A payment made on a Saturday or Sunday counts from the following Monday 00:00 UTC. For example, a top-up paid on Tuesday at 10:00 UTC is credited on the following Monday at 10:00 UTC, and one paid on Saturday on the following Friday at 00:00 UTC. PayCA can set a different number of business days for your client; ask your PayCA contact. A change applies to top-ups created after it, never to those already created.

Until then the top-up is processing: the user has paid, and the money is not on the account yet. It cannot be spent or paid out, and no webhook is sent until the top-up reaches its next status. If the payment is refunded while it waits, nothing is credited. If the acquirer reports it as not successful after all, the top-up goes to manual_review and nothing is credited. Treat only completed as money on the account.

The balance while top-ups settle #

GET /v2/accounts/{accountId}/balance shows what is on the account and what is on its way. It works for any account of yours, including your client-level account.

curl -s "$PAYCA_BASE_URL/v2/accounts/$EUR_ACCOUNT_ID/balance" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET"
{
  "accountId": "0b8f5c7e-2f4d-4a51-9b1e-3c6a7d9e0f12",
  "currency": "EUR",
  "available": "1250.00",
  "pending": "102.00",
  "settling": {
    "amount": "200.00",
    "count": 2,
    "nextSettlementAt": "2026-10-16T09:12:44Z"
  },
  "awaitingPayment": {
    "amount": "100.00",
    "count": 1
  }
}
Field Meaning
available What can be spent or paid out now.
pending Held for operations still in flight, such as a payout that is not final yet. The same figure as in GET /v1/accounts.
settling Top-ups the user has paid that wait for settlement (processing). amount is what they will credit, net of fees. nextSettlementAt is when the first of them is expected to settle, or null.
awaitingPayment Top-ups the user has not finished paying yet, for example with 3-D Secure still open (created or awaiting_payment).

Top-ups in manual_review are counted in neither settling nor awaitingPayment. nextSettlementAt is an estimate: the top-up is credited once it settles, never before. An account you do not own, or one that does not exist, returns 404.

Step 3 - Pay Out to a Card #

Check the option #

curl -s "$PAYCA_BASE_URL/v2/accounts/$EUR_ACCOUNT_ID/payout-options?country=DE" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET"

country is the recipient's country.

{
  "options": [
    {
      "method": "bank_card",
      "currency": "EUR",
      "minAmount": "5.00",
      "maxAmount": "5000.00",
      "fees": {
        "paycaFeeFixed": "0.50",
        "paycaFeeRate": "1.5",
        "providerFeeFixed": "0",
        "providerFeeRate": "0",
        "failureFeeFixed": "0.30"
      }
    }
  ]
}

fees is for showing an estimate: a fixed part plus a percentage of the amount you send. failureFeeRate and failureFeeFixed, when present, price a failed payout (see below). The exact figures come from the quote, which also applies any minimum or maximum fee. minAmount and maxAmount bound the amount you send. An empty list means payouts are not available for this account and country.

Selecting the acquirer for a payout #

Payouts follow the same selection as top-ups. If PayCA allows your client more than one card acquirer, you may select per request which of them may serve the payout: the acquirers query parameter on payout-options, and the acquirers body field in the payout quote and the create. Take the codes from cardAcquirers.allowed of GET /v1/client/settings.

  • Absent: your default acquirer serves the payout, as before.
  • A list: only the listed acquirers that are in cardAcquirers.allowed may serve it. A code that is unknown or not allowed for you is ignored, not refused.
  • Empty (acquirers= or "acquirers": []): no acquirer is selected. payout-options returns no card option, and a quote or create is refused with 400, as for any payout that has no route.

Send the same selection to the options, the quote and the create. Payout methods that do not go through a card acquirer are not affected. Details on the codes and the selection: Choosing the Card Acquirer per Request.

Quote #

curl -s -X POST "$PAYCA_BASE_URL/v2/accounts/$EUR_ACCOUNT_ID/payout-quote" \
  -H "Content-Type: application/json" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET" \
  -d '{
    "method": "bank_card",
    "amount": "100.00",
    "feeMode": "add_on",
    "country": "DE"
  }'
{
  "requestedAmount": "100.00",
  "feeMode": "add_on",
  "currency": "EUR",
  "paycaFee": "2.00",
  "providerFee": "0",
  "totalFee": "2.00",
  "failureFee": "0.30",
  "recipientAmount": "100.00",
  "totalDebitAmount": "102.00"
}
feeMode amount is The fee
add_on what the recipient receives is debited on top: totalDebitAmount = amount + totalFee
deducted what leaves the account is taken out of it: recipientAmount = amount − totalFee

The fee is amount × rate% + fixed, rounded up to the cent, then raised to the minimum or lowered to the maximum fee when the route has one. totalFee is the whole fee; providerFee is always 0. Show the user recipientAmount, totalFee and totalDebitAmount before they confirm.

failureFee is charged only if the provider accepts the payout and then declines it or it fails. It is not part of totalFee or totalDebitAmount and is not held. A payout refused before the provider accepts it (a low balance, an invalid card, a request the provider rejects) or a cancelled payout pays no failure fee.

Create #

curl -s -X POST "$PAYCA_BASE_URL/v2/accounts/$EUR_ACCOUNT_ID/payouts" \
  -H "Content-Type: application/json" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET" \
  -H "Idempotency-Key: payout-20260929-0001" \
  -d '{
    "method": "bank_card",
    "amount": "100.00",
    "feeMode": "add_on",
    "expectedTotalFee": "2.00",
    "country": "DE",
    "ipAddress": "203.0.113.7",
    "recipient": {
      "email": "alex.morgan@example.com",
      "firstName": "Alex",
      "lastName": "Morgan",
      "phone": "+4915112345678",
      "dateOfBirth": "1990-04-12",
      "city": "Berlin",
      "postalCode": "10115",
      "address": "Example Strasse 1"
    },
    "destination": {
      "type": "card",
      "reference": "alex-morgan-card-1",
      "card": {
        "cardNumber": "5555555555554444",
        "expiryMonth": "08",
        "expiryYear": "2028",
        "customerName": "Alex Morgan"
      }
    }
  }'
  • expectedTotalFee is totalFee from the quote. If the fee changed, the request is refused with 409. Quote again.
  • The recipient. On a user's account the payout is made for the user the account belongs to: recipient completes the profile PayCA holds for that user, and ipAddress is the user's device. On your own account the recipient is named by payerReference and described in full by recipient in every request (see Or one account for all your payers). Anything still missing is listed in the refusal, and nothing is reserved.
  • destination is always type: card on this flow. Visa and Mastercard only. cardNumber has 12–19 digits, expiryMonth is 01–12, expiryYear has four digits.
  • customerName is the name on the card, in Latin letters. It needs at least two words of three or more letters each, 20 characters at most, for example Alex Morgan.
  • reference is your own stable id for this card, up to 128 characters. It is never returned.

The response is the payout (201), in status created:

{
  "id": "9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f",
  "status": "created",
  "method": "bank_card",
  "accountId": "0b8f5c7e-2f4d-4a51-9b1e-3c6a7d9e0f12",
  "currency": "EUR",
  "requestedAmount": "100.00",
  "feeMode": "add_on",
  "paycaFee": "2.00",
  "providerFee": "0",
  "totalFee": "2.00",
  "failureFee": "0.30",
  "recipientAmount": "100.00",
  "totalDebitAmount": "102.00",
  "destinationDisplay": "**** 4444",
  "createdAt": "2026-09-29T12:10:00Z"
}

Right after creation, PayCA holds totalDebitAmount on the account and sends the payout. The hold comes from the available balance only; top-ups still processing are not part of it. If the available balance cannot cover it, the payout moves to failed and nothing is sent. Track it with GET /v2/accounts/{accountId}/payouts/{payoutId} or the webhook. GET /v2/accounts/{accountId}/payouts lists every payout of the account, including failed ones.

Payout statuses #

Status Meaning The money
created Recorded; the hold is being placed. Being held
submitted Sent for payout. Held
processing The payout is in progress. Held
completed Paid out and debited. debitedAmount and completedAt are set. Debited
failed Not paid out, for example the card was refused or the balance was too low. Hold returned
cancelled Not paid out. Hold returned
manual_review PayCA is reviewing the payout. Wait for the next status. Held
reversed A paid-out payout came back afterwards. PayCA reviews every reversal, so it shows as manual_review first. Reviewed by PayCA

On the account, a payout reads as follows. At creation, available goes down and pending goes up by totalDebitAmount. On completed, pending goes down by the same amount. On failed or cancelled, it moves from pending back to available. In account transactions these movements have subtype payout_order. A failure fee, when charged, is a separate transaction with subtype payout_failure_fee; what the available balance cannot cover is recorded as fee debt.

Safe retries #

Top-ups and payouts both need an Idempotency-Key. If a create request times out, repeat it with the same key and the same body: you get the original top-up or payout back. The card is not charged twice, and the recipient is not paid twice. A key reused with a different body returns 409; a different payerReference counts as a different body. Use a new key for each new top-up or payout.

Webhooks #

Register one endpoint per type:

curl -s -X POST "$PAYCA_BASE_URL/v1/webhooks" \
  -H "Content-Type: application/json" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET" \
  -d '{ "url": "https://hooks.example.com/payca/deposits", "type": "deposit" }'

curl -s -X POST "$PAYCA_BASE_URL/v1/webhooks" \
  -H "Content-Type: application/json" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET" \
  -d '{ "url": "https://hooks.example.com/payca/payouts", "type": "payout" }'
Type Events
deposit account.deposit.completed, account.deposit.failed, account.deposit.cancelled, account.deposit.expired, account.deposit.refunded, account.deposit.chargeback, account.deposit.manual_review
payout account.payout.completed, account.payout.failed, account.payout.cancelled, account.payout.reversed, account.payout.manual_review

In-flight statuses send no webhook: created, awaiting_payment and processing for top-ups, and created, submitted and processing for payouts. Poll while you wait.

Each delivery carries the event name and the object as the API returns it:

{
  "event": "account.payout.completed",
  "data": {
    "id": "9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f",
    "status": "completed",
    "method": "bank_card",
    "accountId": "0b8f5c7e-2f4d-4a51-9b1e-3c6a7d9e0f12",
    "currency": "EUR",
    "requestedAmount": "100.00",
    "feeMode": "add_on",
    "paycaFee": "2.00",
    "providerFee": "0",
    "totalFee": "2.00",
    "failureFee": "0.30",
    "recipientAmount": "100.00",
    "totalDebitAmount": "102.00",
    "debitedAmount": "102.00",
    "destinationDisplay": "**** 4444",
    "createdAt": "2026-09-29T12:10:00Z",
    "completedAt": "2026-09-29T12:10:42Z"
  }
}

Verify x-signature before parsing, and deduplicate by x-idempotency-key. A completed top-up can still become refunded or chargeback later, so keep accepting events after completed. Signatures and redelivery: Webhook Handling Patterns.

Card Data #

Top-up Payout
What you send Card number, holder name, expiry, CVV Card number, expiry, name on the card
Where Only in POST …/deposits Only in POST …/payouts
What PayCA keeps Nothing. The card goes to the acquirer and is not stored. Encrypted until the payout's final result, then deleted. Never returned by the API.
What you may keep Per your PCI DSS scope. Never the CVV. Per your PCI DSS scope. Use reference to recognise the card.

Errors #

Status Top-up Payout
400 Invalid request, amount outside the limits, profile incomplete, invalid card, redirect URL not allow-listed, or the payment refused by the acquirer. Invalid request, amount outside the limits, fees larger than the amount, profile incomplete, invalid card or name, or no route for this account and country.
403 Top-ups are not enabled for your client. Payouts are not enabled for your client.
404 Account or deposit not found. Account or payout not found.
409 The quote changed, or the Idempotency-Key was used for a different request. The fee changed, or the Idempotency-Key was used for a different request.

The error body names what was wrong in details. Fix the request, or quote again after a 409.

Checklist #

  • PCI DSS in place for the systems that handle card data; CVV never stored, card data never logged.
  • GET /v1/client/settings lists EUR in cardFundingCurrencies.
  • The user has an EUR account and a full profile, or, on your own account, every request carries payerReference and the payer's full profile; the device IP is taken from the connection.
  • Every top-up and payout is quoted first, and the user confirms the quoted figures.
  • A new Idempotency-Key per top-up and payout, reused on retries.
  • For top-ups, redirect to checkoutUrl when there is one. Otherwise poll or wait for the webhook.
  • Webhooks registered for deposit and payout, signatures verified, deliveries deduplicated.
  • Only completed counts: money on the account for a top-up, money gone for a payout.
  • A paid top-up stays processing until it settles, by default four business days. Payouts draw only on available; GET /v2/accounts/{accountId}/balance shows what is still settling.
  • refunded, chargeback and reversed handled after completion.