Save a card and charge it later
Take a card payment on your own checkout, keep the card with the customer's consent, and charge it again later without the customer being present: a no-show fee, extras added after check-out, a deposit topped up. This page walks through the whole flow, from creating the first checkout to charging the saved card, with the requests and responses at each step.
Switched on by Yavin. Saving a card and charging a saved card both need the saved-cards feature enabled on your account. Ask your Yavin contact to enable it; until then the requests below are refused.
How it works
Two separate moments, linked by one id. The first payment saves the card and gives you stored_card.id. Any time later, your server charges that id on a new checkout. The card number never reaches your servers in either moment.
Before you start
| You need | Why |
|---|---|
| The feature enabled on your account | One switch covers saving a card through the card widget and charging a saved card through the API. Ask Yavin to turn it on. |
| The card widget | The cardholder's consent travels with the payment, so the card is saved from your own page through the widget (submit({ enrolConsent: true })). Yavin's hosted payment page does not offer it. See Online payment drop-in integration. |
| Your own consent notice | Yavin does not collect the consent for you. You show your own notice and pass enrolConsent: true only for a customer who accepted it. |
A webhook_url | The saved card's id reaches you in the payment_received webhook. Keep it on every checkout of the flow. |
| A spending limit that fits | Saved-card charges are capped per customer, over the customer's lifetime, at an amount set on your account (300 € by default). A charge that would take the customer over it is refused with amount_over_limit. Ask Yavin to change it. |
Base URL: production https://api.yavin.com/api/v5/ecommerce, sandbox https://api.sandbox.yavin.com/api/v5/ecommerce. Authentication: your API key in the Yavin-Secret header.
Moment 1 - Save the card
Step 1 - Create the checkout that saves the card
Create the checkout server-side, exactly as in the drop-in integration, with two additions: features.enrol_card and a customer.
features.enrol_cardis a boolean and defaults tofalse. Every value infeaturesmust be a real boolean- With
enrol_card: true, thecustomeris required:first_name,last_nameand at least one oftelephone(50 characters at most) oremail. Yavin keeps this contact with the saved card, and your merchant sees it in MyYavin - A request that breaks one of these rules, or that comes from an account without the feature, is rejected before any checkout exists:
422on/checkouts/, with an error namingfeaturesorcustomer enrol_cardis only acted on by the card widget. A checkout opened on Yavin's hosted payment page sends no consent, so nothing is saved there
POST https://api.yavin.com/api/v5/ecommerce/checkouts/
Yavin-Secret: <your yavin_secret_key>
Content-Type: application/json
{ "checkout_external_id": "booking_88",
"amount": 2900,
"customer": { "first_name": "Ada", "last_name": "Lovelace",
"telephone": "+33612345678", "email": "ada@example.com" },
"features": { "enrol_card": true },
"return_url_success": "https://shop.example.com/return",
"return_url_cancelled": "https://shop.example.com/cancel",
"webhook_url": "https://shop.example.com/hooks/yavin" }// 201 Created
{ "checkout_token": "Ab3f...",
"payment_link": "https://..." }Step 2 - Send the consent with the payment
Mount the widget with the checkout_token, show your consent notice, and pass the consent when you take the payment:
enrolConsentis optional and defaults tofalse. It must be a real boolean:"true",1ornullmakesubmit()reject and nothing is sent- The consent applies to that attempt only. A retry that does not pass it saves nothing, even if an earlier attempt did
- After a 3-D Secure step-up the consent given when the payment started is the one that counts
- Saving the card never fails the payment: if the card cannot be saved, the payment still succeeds and no card is returned
const widget = window.YavinPaymentsSDK.mount(
document.getElementById('card'),
{ checkoutToken: 'Ab3f...' }
)
// the customer ticked your "keep my card for later payments" box
const result = await widget.submit({ enrolConsent: customerAgreed })Step 3 - Read the saved card
Once the payment is authorised, the payment_received webhook arrives with the checkout. Each transaction carries stored_card:
| Field | Meaning |
|---|---|
stored_card | null when no saved card is linked to the transaction, which is the case whenever nothing was saved. |
stored_card.id | The card's id (a UUID). Keep it with your customer or booking: it is the customer_card_id you send to charge the card. |
stored_card.expiration_date | YYYY-MM, the card's expiry month, as printed on the card. A card is valid through the end of that month. null when the expiry is not known. |
Check it before relying on it. If the webhook shows stored_card: null the customer paid but no card is on file, for example because the consent was not sent. Handle that case in your flow: ask for another payment method rather than assuming a card exists.
The card is saved when the payment is authorised, before any capture, so stored_card is already in the payment_received webhook. Only card payments are saved: a wallet payment such as Apple Pay never is. You can read the same object at any time with /get_cart_information/.
// webhook POST to your webhook_url
{ "action": "payment_received",
"checkout_external_id": "booking_88",
"requested_amount": 2900,
"asked_amount": 2900,
"paid_amount": 2900,
"service_fee": 0,
"status": "ok",
"transactions": [
{ "transaction_id": "trs_7f3a92c5",
"total_amount": 2900,
"gift_amount": 0,
"currency_code": "EUR",
"date_of_payment": "2026-11-03 14:35:15",
"gateway": "NEPTING_ECOMMERCE",
"issuer": "VISA",
"pan": "4970********0001",
"status": "ok",
"stored_card": { "id": "9b2f4c1e-7a3d-4f55-8a61-2d9c3b7e1a10",
"expiration_date": "2028-12" } } ] }Moment 2 - Charge the saved card
When you need to charge, create a new checkout for the new amount, then pay it with the saved card. Both calls are made by your server, with no customer and no browser.
Step 4 - Create the checkout to charge
- Give it a new
checkout_external_id: an id is claimed permanently, even by a cancelled checkout - Leave the capture mode at its default (instant). A checkout created with
is_instant_capture: falsecannot be paid with a saved card - You do not need
enrol_cardor a customer here
POST https://api.yavin.com/api/v5/ecommerce/checkouts/
Yavin-Secret: <your yavin_secret_key>
Content-Type: application/json
{ "checkout_external_id": "booking_88_noshow",
"amount": 1500,
"webhook_url": "https://shop.example.com/hooks/yavin" }// 201 Created
{ "checkout_token": "Zk9q...",
"payment_link": "https://..." }Step 5 - Pay with the saved card
POST https://api.yavin.com/api/v5/ecommerce/checkouts/{checkout_token}/pay/
customer_card_idString (UUID)required- The stored_card.id you kept in Step 3
The card is charged for the whole amount still to pay on the checkout. The body takes only customer_card_id: do not send card details with it.
The response is the Online payment: Ecommerce API, returned directly (not wrapped in data). The same checkout is then sent to your webhook_url as payment_received, and the transaction is linked to the saved card through stored_card.
Set a client-side timeout of at least 30 seconds, and never retry a charge blindly. If you did not get an answer, read the checkout with /get_cart_information/ first: the payment may have gone through.
POST https://api.yavin.com/api/v5/ecommerce/checkouts/Zk9q.../pay/
Yavin-Secret: <your yavin_secret_key>
Content-Type: application/json
{ "customer_card_id": "9b2f4c1e-7a3d-4f55-8a61-2d9c3b7e1a10" }// 200 OK, the Checkout object
{ "checkout_external_id": "booking_88_noshow",
"payment_link": "https://...",
"requested_amount": 1500,
"asked_amount": 1500,
"paid_amount": 1500,
"service_fee": 0,
"gift_amount": 0,
"status": "ok",
"transactions": [
{ "transaction_id": "trs_91c4d0aa",
"total_amount": 1500,
"gift_amount": 0,
"currency_code": "EUR",
"date_of_payment": "2026-11-20 09:12:44",
"gateway": "NEPTING_ECOMMERCE",
"issuer": "VISA",
"pan": "4970********0001",
"status": "ok",
"stored_card": { "id": "9b2f4c1e-7a3d-4f55-8a61-2d9c3b7e1a10",
"expiration_date": "2028-12" } } ] }Step 6 - Confirm
Treat the 200 response and the payment_received webhook as the confirmation, as for any other payment. To reconcile, read the checkout:
POST https://api.yavin.com/api/v5/ecommerce/get_cart_information/
{ "checkout_external_id": "booking_88_noshow" }Errors
A refused charge returns an error body, the same shape for every code:
code is the stable value to branch on. transaction_id is present when the attempt reached the payment gateway.
The card cannot be charged
| code | HTTP | Meaning and what to do |
|---|---|---|
card_not_found | 404 | No saved card with this id on your account. Check the id you kept; a card of another merchant looks the same. |
card_not_consented | 422 | The card was never saved with the cardholder's consent, so it cannot be charged. |
card_disabled | 422 | The card was disabled, for example by your merchant in MyYavin. |
card_expired | 422 | The card's expiry month has passed. Ask the customer for a new card. A card saved without an expiry date never expires on Yavin's side. |
amount_over_limit | 422 | This charge would take the total charged to the customer through saved cards over the limit set on your account. Ask Yavin to raise it, or collect another way. |
amount_invalid | 422 | The amount to charge is not above zero. |
company_not_configured | 422 | The account is not set up to charge saved cards: the feature is off, or a payment contract is missing. Contact Yavin. |
The checkout cannot be paid
| code | HTTP | Meaning and what to do |
|---|---|---|
invalid_payload | 422 | The body is not a valid customer_card_id (a UUID), or it mixes it with card details. errors names the field. |
checkout_not_found | 404 | No checkout with this token on your account. |
checkout_cancelled | 409 | The checkout was cancelled. Create a new one. |
checkout_already_paid | 409 | The checkout is already paid. Do not charge again. |
checkout_not_payable | 409 | The checkout is not waiting for a payment. |
attempt_in_progress | 409 | Another payment attempt is running on this checkout. Wait, then read the checkout before trying again. |
checkout_amount_changed | 409 | The amount to pay moved while the call was in flight. Read the checkout and decide. |
checkout_requires_capture | 422 | The checkout uses deferred capture, which a saved card cannot pay. Create a checkout with the default instant capture. |
checkout_version_incompatible | 422 | This checkout cannot be paid server-to-server. Create it with POST /checkouts/. |
The payment was attempted
| code | HTTP | Meaning and what to do |
|---|---|---|
payment_declined | 402 | The issuer declined the charge. Nothing was taken. Ask the customer for another way to pay. |
insufficient_funds | 402 | The card does not hold enough funds. Nothing was taken. |
gateway_connection_error | 503 | The payment gateway could not be reached, so the charge was not made. It is safe to try again. |
acquirer_timeout | 504 | The acquirer did not answer in time. The outcome is unknown. Read the checkout before you retry. |
gateway_error | 502 | The gateway answered with something Yavin could not conclude from. The outcome is not certain. Read the checkout before you retry. |
The codes invalid_auth, challenge_in_progress and challenge_not_completable are rare: treat the payment as not taken, and contact support if they persist.
An unknown outcome is not a failed one. After acquirer_timeout or gateway_error, or any call that gave you no answer, read the checkout with /get_cart_information/ and look at its status and paid_amount before you charge again. Replaying a charge unattended is how a customer gets charged twice.
{ "detail": "The payment was declined.",
"code": "payment_declined",
"errors": {},
"transaction_id": "trs_91c4d0aa" }Good to know
- You never handle card data. You keep an id and an expiry date. The card number and CVC stay inside Yavin's card field and never reach your page or your servers
- Keep the consent evidence. The notice you showed and the moment the customer accepted it are yours to record
- Your merchant sees the cards in MyYavin. The saved-cards section lists the cards saved from your checkouts, with the customer's contact, and lets them disable one. A disabled card is then refused with
card_disabled - Cards saved in store can be charged the same way. A card saved on a Yavin terminal is charged with the same call and its id, provided the saved-cards feature is enabled on your account
- Several payments, one customer. The spending limit adds up every successful saved-card charge of the same customer, across all their saved cards