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 #
- The user has an EUR account at PayCA.
- 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 becomescompleted, only when the card acquirer settles the payment: by default four business days later (see Settlement). - 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.
- 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/settingslistsEURincardFundingCurrenciesonce top-ups are on. If EUR top-ups go through an acquirer you select per request rather than your default,EURis listed under that acquirer incardAcquirers.currenciesinstead: 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
depositand one forpayout. 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/accountsdoes not list it; read its balance withGET /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 samepayerReference; different people always have different ones. The card network checks payers individually, so payments from different people must never share apayerReference. 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:
payerin a top-up,recipientandipAddressin 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
payerReferencenoruserIdis refused with400.userIdis 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"
}'
amountand theexpected…values come from the quote. If the price changed in the meantime, the request is refused with409. Quote again.payercompletes the profile PayCA holds for the user. Anything still missing is listed in a400, and nothing is created.card: Visa or Mastercard only.numberis digits only,expiryMonthis01–12,expiryYearhas four digits,cvvhas three or four. A card that fails these checks is refused with400, and nothing is created.- On a user's account the payer is the user, and
userIdmay be omitted. On your own account, addpayerReferenceand send the wholepayer: 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 tocompleteRedirectUrl, or tocancelRedirectUrlif 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.allowedmay 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-optionsreturns no card option, and a quote or create is refused with400, 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"
}
}
}'
expectedTotalFeeistotalFeefrom the quote. If the fee changed, the request is refused with409. Quote again.- The recipient. On a user's account the payout is made for the user the account belongs to:
recipientcompletes the profile PayCA holds for that user, andipAddressis the user's device. On your own account the recipient is named bypayerReferenceand described in full byrecipientin every request (see Or one account for all your payers). Anything still missing is listed in the refusal, and nothing is reserved. destinationis alwaystype: cardon this flow. Visa and Mastercard only.cardNumberhas 12–19 digits,expiryMonthis01–12,expiryYearhas four digits.customerNameis 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 exampleAlex Morgan.referenceis 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/settingslistsEURincardFundingCurrencies.- The user has an EUR account and a full profile, or, on your own account, every request carries
payerReferenceand 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-Keyper top-up and payout, reused on retries. - For top-ups, redirect to
checkoutUrlwhen there is one. Otherwise poll or wait for the webhook. - Webhooks registered for
depositandpayout, signatures verified, deliveries deduplicated. - Only
completedcounts: money on the account for a top-up, money gone for a payout. - A paid top-up stays
processinguntil it settles, by default four business days. Payouts draw only onavailable;GET /v2/accounts/{accountId}/balanceshows what is still settling. refunded,chargebackandreversedhandled after completion.