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 #
- Credentials. Send
x-client-idandx-client-secreton every request.
GET /v2/accountsreturns200once they are valid. - Funding account. Take the
idof 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. - 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. - 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 toresume_threshold_amountwithin 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
manualorinsufficient_fundstakes
no top-up — unfreeze it first. - Busy. One operation runs on a card at a time. A second one is refused with
operation_in_progressuntil the first is final. - Stale quote. If the buffer changed between the quote and the accept, the
accept is refused withquote_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_bufferwebhook is sent again with the eventclose_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. |