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. The account is credited as soon as the payment is confirmed.
  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.
  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.
  • 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 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.

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"
  }'
  • 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.
  • userId may 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 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 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"
      }
    }
  }'
  • expectedTotalFee is totalFee from the quote. If the fee changed, the request is refused with 409. Quote again.
  • The recipient is the account holder. 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. Anything still missing is listed in the refusal, and nothing is reserved. An account with no user needs userId.
  • 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",
  "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/settings lists EUR in cardFundingCurrencies.
  • 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-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.
  • refunded, chargeback and reversed handled after completion.