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. The account is credited as soon as the payment is confirmed.
- 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.
- 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. - Redirect URLs. HTTPS prefixes for your return pages must be allow-listed by PayCA. The user comes back to them after 3-D Secure.
- User profile. Top-ups and payouts need the user's full profile: first and last name, email, phone in international format, date of birth, city, address, postal code and country. Send it when you create the user, or 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.
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.
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",
"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 and refundFee are charged to the account only if the payment is declined or later refunded.
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",
"country": "DE",
"userId": "73f77d0f-8b30-46e8-9081-b506180f764e",
"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.userIdmay be omitted when the account belongs to the user.
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 payment is confirmed and the credit is being recorded. | 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 |
A completed top-up was refunded. The credit is reversed and refundFee is debited. |
Reversed |
chargeback |
A completed top-up was charged back. The credit is reversed. | Reversed |
manual_review |
PayCA is reviewing the top-up. Wait for the next status. | No |
A successful payment is credited straight away; there is no settlement wait on this flow. Treat only completed as money on the account.
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"
}
}
]
}
fees is for showing an estimate: a fixed part plus a percentage of the amount you send. The exact figures come from the quote. minAmount and maxAmount bound the amount you send. An empty list means payouts are not available for this account and country.
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",
"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. totalFee is the whole fee; providerFee is always 0. Show the user recipientAmount, totalFee and totalDebitAmount before they confirm.
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 is the account holder. The payout is made for the user the account belongs to.
recipientcompletes the profile PayCA holds for that user, andipAddressis the user's device. Anything still missing is listed in the refusal, and nothing is reserved. An account with no user needsuserId. 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",
"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. 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.
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. 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",
"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; 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. refunded,chargebackandreversedhandled after completion.