Overview #

On Provider W BINs a card can be issued with a card buffer: a small reserve
PayCA keeps next to the card, in your name. The provider's own charges on the
card — decline fees, cross-border fees — are paid from the buffer, so they do
not reduce the card balance your end user sees.

When the card buffer is enabled for your client, cards on these BINs are issued
through /v2/cards, and every card issued that way is operated through
/v2/cards too. The /v1/cards calls that move money or change a card's state
answer 400 with card_buffer_requires_v2 in details instead: an issue on
such a BIN, and a top-up, withdrawal, freeze, unfreeze, close or history request
on a buffered card. Your PayCA account manager enables the buffer for your
client; the capability call below tells you whether it is on.

These calls stay on v1 and work unchanged:

Call Purpose
POST /v1/users, POST /v1/users/{id}/cardholder Create the end user and the cardholder a card is issued to.
GET /v1/cards/{id}/sensitive Read the card number, CVC and expiry.
POST /v1/webhooks Register webhook URLs.
GET /v2/accounts List your accounts, balances and BINs.

Every amount in /v2/cards is a string in USD with at most two decimals.
Send it as "50.00" or "50", never as a number. Responses drop trailing
zeros ("18", "2.8"), so parse them as decimals, not by format.


Before You Start #

  1. Credentials. Send x-client-id and x-client-secret on every request.
    GET /v2/accounts returns 200 once they are valid.
  2. Funding account. Take the id of the account that carries the BIN from
    GET /v2/accounts. The examples call it <account-id>; card money comes
    from it and goes back to it.
  3. Cardholder. Every card is issued to an approved cardholder (pass_audit).
    How to create one depends on the BIN — see
    Provider W Cardholder Integration. On a
    B2B BIN it is one call with the user's first name, last name and email.
  4. Webhooks. Register these types with POST /v1/webhooks:
Type What arrives
card_buffer Every status change of your /v2/cards operations — this is how you learn that a card was issued.
card Card events, including 3DS codes. Without it your users cannot confirm online payments.
cardholder Cardholder status changes.

How a Payment Is Split #

Each BIN has a buffer policy. Read the figures from the API, never hard-code
them — they are part of every quote and of GET /v2/cards/{id}:

Field Meaning
target_amount The size of a full buffer.
freeze_threshold_amount 20% of the target. When the buffer falls to this amount or below, or a charge was left unpaid, the card is frozen automatically.
resume_threshold_amount 50% of the target. A frozen card resumes automatically once the buffer is back at this amount and nothing is left unpaid.

Issuing. funding_amount is everything debited from your account. It is
split as

funding_amount = operation_fee (issue fee) + buffer_refill (full target) + card_credit

With a $14 target and a $3 issue fee, a funding_amount of "50.00" puts
$33.00 on the card. The smallest accepted amount is returned as
minimum_funding_amount ($18.00 in this example: the $1 card minimum, the
target and the fee).

Topping up. At most 50% of a top-up goes to the buffer: first to repay
any charge the buffer could not cover (uncovered_repayment), then to refill
the buffer up to the target (buffer_refill). The rest is card_credit. A
buffer that is already full takes nothing.

When the buffer runs short. A charge larger than the buffer empties it and
the remainder is recorded as uncovered_fee_amount; the card is frozen. The next
top-up repays it from its 50% share.

Your own fees. The fees of your tariff (issue fee, monthly fee, unfreeze
fee, withdrawal fee) are charged as before; the issue fee is shown in the quote
as operation_fee.


Issue a Card #

Step 1 — Check Capability #

curl -s -G "$PAYCA_BASE_URL/v2/cards/issue/capability" \
  --data-urlencode "source_account_id=<account-id>" \
  --data-urlencode "bin=49372410" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET"
{
  "model": "separate_buffer_v1",
  "required": true,
  "eligible": true,
  "minimum_funding_amount": "18"
}

required: true means the BIN is issued through /v2/cards for your client.
eligible: false comes with a reason_code:

reason_code Meaning
not_enabled The card buffer is not enabled for your client, or not for this BIN.
bin_not_attached The BIN is not connected to this account.
bin_disabled The BIN is switched off on this account.
client_not_ready, program_not_ready, parent_not_ready, delivery_fee_policy_unresolved A setup step on our side is missing. Contact support.

The capability call is optional; the quote checks the same things.

Step 2 — Request a Quote #

curl -s -X POST "$PAYCA_BASE_URL/v2/cards/issue/quote" \
  -H 'Content-Type: application/json' \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET" \
  -d '{
    "source_account_id": "<account-id>",
    "bin": "49372410",
    "cardholder_id": "<cardholder-id>",
    "funding_amount": "50.00",
    "external_user_id": "usr-001"
  }'
Field Required Purpose
source_account_id Yes The account the money comes from.
bin Yes The BIN to issue on.
funding_amount Yes Total budget, fee and buffer included.
cardholder_id Always pass it The approved cardholder the card is issued to. Without it the card belongs to no cardholder, and the per-cardholder card limit is not applied to it.
external_user_id No Your own reference for the card.

Response (abridged):

{
  "quote_id": "8b2d6f0e-6c1a-4b7e-9d43-2f1c0a9e5b11",
  "kind": "issue",
  "bin": "49372410",
  "currency": "USD",
  "expires_at": "2026-10-29T10:01:00Z",
  "funding_amount": "50",
  "operation_fee": "3",
  "buffer_refill": "14",
  "card_credit": "33",
  "total_debit": "50",
  "buffer_after": "14",
  "freeze_threshold_amount": "2.8",
  "resume_threshold_amount": "7",
  "minimum_funding_amount": "18"
}

Nothing is debited yet. The cardholder checks run here: a cardholder that is
not approved, belongs to another client, is registered for another BIN or
already holds its maximum number of cards is refused at this step.

A quote is valid for one minute (expires_at). Accept it right away, or
request a new one.

Step 3 — Accept the Quote #

curl -s -X POST "$PAYCA_BASE_URL/v2/cards" \
  -H 'Content-Type: application/json' \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET" \
  -d '{
    "quote_id": "8b2d6f0e-6c1a-4b7e-9d43-2f1c0a9e5b11",
    "idempotency_key": "issue-usr-001-0001"
  }'

The answer is 202 with an operation. total_debit is now held on your
account; the card is issued asynchronously.

{
  "operation_id": "3f9a1c52-0d7e-4f6b-a1e8-6c2b9d4e7a30",
  "kind": "issue",
  "status": "funds_held",
  "source_account_id": "<account-id>",
  "funding_amount": "50",
  "quoted_card_credit": "33",
  "quoted_operation_fee": "3",
  "maximum_source_debit": "50",
  "idempotency_key": "issue-usr-001-0001"
}

idempotency_key (1–200 characters) makes the call safe to retry: the same key
with the same quote returns the same operation. Reusing a key with a different
request is refused with idempotency_conflict.

Step 4 — Wait for the Result #

Follow the operation on the card_buffer webhook, or poll it:

curl -s "$PAYCA_BASE_URL/v2/cards/operations/<operation-id>" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET"
status Meaning
funds_held Accepted; the money is held on your account.
provider_submission_prepared, provider_pending Sent to the card issuer, waiting for its answer.
needs_reconciliation The issuer's answer is being confirmed on our side. Wait; do not repeat the request with a new key.
succeeded Done. For an issue, card_id is set and the actual_* fields show what was booked.
rejected Refused; error_code says why. For a refused issue the held money is returned to your account.
compensated Undone; the money was returned.

succeeded, rejected and compensated are final.

Step 5 — Read the Card #

curl -s "$PAYCA_BASE_URL/v2/cards/<card-id>" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET"

The answer has the card (card) and its buffer (buffer) side by side:
buffer.balance, target_amount, uncovered_fee_amount, the two thresholds,
and flags that tell what the card accepts right now — can_topup,
can_auto_resume, can_manual_unfreeze. model is separate_buffer_v1 for a
buffered card and legacy_on_card for a card issued before the buffer was
enabled.

Card number, CVC and expiry come from GET /v1/cards/{card-id}/sensitive.


Top Up a Card #

Same two steps as an issue: quote, then accept.

curl -s -X POST "$PAYCA_BASE_URL/v2/cards/<card-id>/topup/quote" \
  -H 'Content-Type: application/json' \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET" \
  -d '{ "source_account_id": "<account-id>", "funding_amount": "100.00" }'

curl -s -X POST "$PAYCA_BASE_URL/v2/cards/<card-id>/topup" \
  -H 'Content-Type: application/json' \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET" \
  -d '{ "quote_id": "<quote-id>", "idempotency_key": "topup-0001" }'

The quote shows the split: uncovered_repayment, buffer_refill,
card_credit, and the buffer before and after (buffer_before,
buffer_after).

  • Frozen for its buffer. A card frozen because its buffer ran low takes a
    top-up. The top-up must be large enough to repay what is unpaid and bring the
    buffer back to resume_threshold_amount within its 50% share; the quote names
    the minimum otherwise. The card resumes by itself once the top-up lands.
  • Frozen by you. A card frozen with manual or insufficient_funds takes
    no top-up — unfreeze it first.
  • Busy. One operation runs on a card at a time. A second one is refused with
    operation_in_progress until the first is final.
  • Stale quote. If the buffer changed between the quote and the accept, the
    accept is refused with quote_stale; request a new quote.

Withdraw From a Card #

Moves money from the card back to the account that funded it. The buffer is not
touched.

curl -s -X POST "$PAYCA_BASE_URL/v2/cards/<card-id>/withdraw" \
  -H 'Content-Type: application/json' \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET" \
  -d '{ "source_account_id": "<account-id>", "amount": "20.00", "idempotency_key": "withdraw-0001" }'

source_account_id must be the account the card was issued from. A withdrawal
fee of your tariff, if any, comes out of the amount (quoted_operation_fee); an
amount that would not cover it is refused.


Freeze and Unfreeze #

curl -s -X POST "$PAYCA_BASE_URL/v2/cards/<card-id>/freeze" \
  -H 'Content-Type: application/json' \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET" \
  -d '{ "idempotency_key": "freeze-0001", "reason": "manual" }'

POST /v2/cards/<card-id>/unfreeze takes the same body. A card can carry
several freeze reasons at once, and each is removed on its own:

reason Set by Removed by
manual (default) You unfreeze with manual. Free.
insufficient_funds You, after a decline for insufficient funds unfreeze with insufficient_funds. Charges the unfreeze fee of your tariff (buffer.unfreeze_fee) once the issuer confirms the unfreeze.
buffer freeze PayCA, automatically A top-up that restores the buffer. You cannot remove it with unfreeze.

An unfreeze that another reason would keep in place is refused; remove that
reason first. can_manual_unfreeze on GET /v2/cards/{id} tells you in advance.


Close a Card #

curl -s -X DELETE "$PAYCA_BASE_URL/v2/cards/<card-id>" \
  -H 'Content-Type: application/json' \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET" \
  -d '{ "idempotency_key": "close-0001" }'

Closing is confirmed by the card issuer, which can take about 30 days. Until
then the operation stays in progress (provider_pending).

  • Card balance. Returned to your account when the issuer confirms the close.
  • Buffer. Charges can still reach a card after it is closed, so the buffer
    stays reserved for 60 days after the close request and pays them. What is
    left is then returned to your account, and the close operation's
    card_buffer webhook is sent again with the event close_hold_released.

History #

Call Returns
GET /v2/cards/{id}/transactions Card, buffer and fee entries of one card.
GET /v2/accounts/{id}/transactions Account history including buffer operations.

Both take limit (1–100, default 50) and cursor; pass the next_cursor of a
page to get the next one. Once an account has buffer operations,
GET /v1/accounts/{id}/transactions answers card_buffer_history_requires_v2
for it.


Webhooks #

The card_buffer webhook body is the operation itself — the same object as
GET /v2/cards/operations/{operation_id}. It is sent on each status change, so
one operation produces several deliveries. Deduplicate on
operation_id + status, and verify the x-signature header as for every
PayCA webhook.

Read kind before acting: besides your own operations (issue, topup,
withdraw, freeze, unfreeze, close), deliveries can carry operations you
did not start, such as a fee the buffer paid.


Errors #

Errors come as {"code": "...", "details": "..."}.

HTTP code Meaning What to do
409 quote_expired The quote is older than one minute. Request a new quote.
409 quote_stale The quote no longer matches the card or the account. Request a new quote.
409 idempotency_conflict The key was used for a different request. Use a new key for a new request.
409 operation_in_progress Another operation runs on the card, or it is being closed. Wait until it is final.
409 buffer_state_changed The freeze state changed before your request was applied. Read the card again.
403 feature_not_enabled The card buffer, top-ups, or the BIN's buffer policy is not enabled for your client. Contact your account manager.
400 ErrFailedPrecondition with card_buffer_requires_v2 A v1 call for a BIN or card that uses the buffer. Use the /v2/cards call.
400 ErrFailedPrecondition with card_buffer_history_requires_v2 GET /v1/accounts/{id}/transactions on an account with buffer operations. Use GET /v2/accounts/{id}/transactions.
400 buffer_model_requires_v2 A v2 funding call on a card issued without a buffer. Use the v1 call for that card.
400 card_buffer_ineligible The request cannot be carried out as asked: an amount below the minimum, a card that is frozen or being closed, an account that does not own the card. details names the reason, and the minimum where an amount is too small. Fix the request as details says.
400 ErrInvalidArg with card_buffer_ineligible The BIN does not support the card buffer. Use a BIN that does.
400 ErrFailedPrecondition with kyc required: ... The cardholder is missing, not approved, or not registered for this BIN. See Provider W Cardholder Integration.
400 ErrFailedPrecondition with cardholder card limit reached: ... The cardholder holds its maximum number of live cards. Close one of its cards or use another cardholder.
400 ErrInsufficientFunds The account does not cover the quote. Fund the account.
400 ErrInvalidArg A malformed body: an amount that is not a positive string with at most two decimals, or both or neither of the amount fields. Fix the request.

Endpoint Reference #

Method Endpoint Description
GET /v2/cards/issue/capability Whether the BIN is issued with a buffer for this account, and the minimum funding.
POST /v2/cards/issue/quote Quote an issue.
POST /v2/cards Accept an issue quote.
GET /v2/cards/{id} Card and buffer.
POST /v2/cards/{id}/topup/quote Quote a top-up.
POST /v2/cards/{id}/topup Accept a top-up quote.
POST /v2/cards/{id}/withdraw Withdraw from the card to its account.
POST /v2/cards/{id}/freeze Freeze with a reason.
POST /v2/cards/{id}/unfreeze Remove a freeze reason.
DELETE /v2/cards/{id} Close the card.
GET /v2/cards/operations/{operation_id} Read an operation.
GET /v2/cards/{id}/transactions Card history.
GET /v2/accounts/{id}/transactions Account history.