Server-to-server payment
Pay a checkout entirely from your server: no customer on the page, no browser, no payment link. You create a checkout, then call one endpoint that charges a card and answers with the settled checkout. There are two ways to say which card: a card Yavin already saved for you, or card details you hold.
Switched on by Yavin. Charging a saved card needs the saved-cards feature enabled on your account. Sending card details needs a separate permission, and it puts your servers in PCI DSS scope. Both are refused until Yavin enables them. If you only need to take a payment from a customer, use the Online payment drop-in integration instead: card data never reaches your servers there.
Choose the form
| Form | You send | Account needs |
|---|---|---|
| Saved card | customer_card_id, the id of a card Yavin saved with the cardholder's consent | The saved-cards feature. The whole flow, including how the card gets saved, is in Save a card and charge it later |
| Card details | card_number, expiry_date and cvv, for a single payment | The permission to send card details. Nothing is stored |
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, as for every ecommerce call.
How it works
The charge is made while your call is open. There is no redirect, no 3-D Secure page and no return URL.
Step 1 - Create the checkout
The checkout carries the amount. Create it with POST /checkouts/, the same call as the card widget.
amountis in cents, minimum100- Give each checkout its own
checkout_external_id: an id is claimed permanently, even by a cancelled checkout - Keep the default instant capture. A checkout created with
is_instant_capture: falsecannot be paid from your server - Keep your
webhook_url: it receivespayment_receivedwhen the charge succeeds
POST https://api.yavin.com/api/v5/ecommerce/checkouts/
Yavin-Secret: <your yavin_secret_key>
Content-Type: application/json
{ "checkout_external_id": "invoice_2041",
"amount": 4200,
"webhook_url": "https://shop.example.com/hooks/yavin" }// 201 Created
{ "checkout_token": "Zk9q...",
"payment_link": "https://..." }Step 2 - Pay the checkout
POST https://api.yavin.com/api/v5/ecommerce/checkouts/{checkout_token}/pay/
Headers: Yavin-Secret, Content-Type: application/json, and optionally X-Request-Id, a value of your choice that Yavin records so a call can be traced when you contact support.
Send one of the two forms. Mixing them is refused.
Form A - a saved card
customer_card_idString (UUID)required- The stored_card.id of a saved card of your account
The card has to have been saved with the cardholder's consent, be enabled and not expired, and the charge has to fit the spending limit of your account. See Save a card and charge it later.
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" }Form B - card details
card_numberStringrequired- The card number, digits only
expiry_dateStringrequired- MMYY, for example 1228 for December 2028
cvvStringrequired- The card verification code
All three fields are required together. What to know about this form:
- One payment, nothing stored. The card is not saved and the transaction has no
stored_card. To keep a card for later, use the saved-card flow - No cardholder, so no 3-D Secure. The payment asks the issuer not to challenge it. If the issuer insists on authenticating the cardholder, the payment cannot complete and is refused with
challenge_not_completable. For such a card, use the card widget, where the customer can authenticate - Never log or store the card details. They are yours to protect under PCI DSS the moment they reach your server
In both forms the card is charged for the whole amount still to pay on the checkout.
POST https://api.yavin.com/api/v5/ecommerce/checkouts/Zk9q.../pay/
Yavin-Secret: <your yavin_secret_key>
Content-Type: application/json
{ "card_number": "4242424242420042",
"expiry_date": "1228",
"cvv": "123" }Response
The response is the Online payment: Ecommerce API, returned directly and not wrapped in data. It is the same object your webhook_url receives as payment_received. stored_card is null for a one-shot card, and holds the saved card for Form A.
Set a client-side timeout of at least 30 seconds, and never retry a payment blindly. If you got no answer, read the checkout with /get_cart_information/ first: the payment may have gone through.
// 200 OK, the Checkout object
{ "checkout_external_id": "invoice_2041",
"payment_link": "https://...",
"requested_amount": 4200,
"asked_amount": 4200,
"paid_amount": 4200,
"service_fee": 0,
"gift_amount": 0,
"status": "ok",
"transactions": [
{ "transaction_id": "trs_91c4d0aa",
"total_amount": 4200,
"gift_amount": 0,
"currency_code": "EUR",
"date_of_payment": "2026-11-20 09:12:44",
"gateway": "NEPTING_ECOMMERCE",
"issuer": "VISA",
"pan": "4242********0042",
"status": "ok",
"stored_card": null } ] }Step 3 - Confirm
The 200 response and the payment_received webhook are your confirmation, as for any other payment. To reconcile a checkout later, or after an unclear answer:
Release goods on the checkout status (ok) and paid_amount, never on a missing error.
POST https://api.yavin.com/api/v5/ecommerce/get_cart_information/
{ "checkout_external_id": "invoice_2041" }Errors
Every refusal returns the same body shape. code is the stable value to branch on, detail is English text for your logs, and errors names the fields for a validation failure. transaction_id is present when the attempt reached the payment gateway.
The request or the checkout
| code | HTTP | Meaning and what to do |
|---|---|---|
invalid_payload | 422 | The body is not exactly one form: neither customer_card_id nor the three card fields, both together, or a card field missing. errors names the fields. |
card_data_not_allowed | 422 | Form B on an account without the permission to send card details (errors names card_number). Contact Yavin, or use the saved-card form or the widget. |
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. Create one with the default instant capture. |
checkout_version_incompatible | 422 | This checkout cannot be paid from your server. Create it with POST /checkouts/. |
The saved card (Form A)
| code | HTTP | Meaning and what to do |
|---|---|---|
card_not_found | 404 | No saved card with this id on your account. |
card_not_consented | 422 | The card was never saved with the cardholder's consent. |
card_disabled | 422 | The card was disabled. |
card_expired | 422 | The card's expiry month has passed. Ask the customer for a new card. |
amount_over_limit | 422 | This charge would take the customer over the saved-card spending limit of your account. |
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. Contact Yavin. |
The payment was attempted
| code | HTTP | Meaning and what to do |
|---|---|---|
invalid_auth | 422 | The card details were not accepted as valid (Form B). Nothing was taken: correct them and send the request again. |
payment_declined | 402 | The issuer declined the payment. Nothing was taken. Ask for another way to pay. |
insufficient_funds | 402 | The card does not hold enough funds. Nothing was taken. |
challenge_not_completable | 402 | The issuer requires the cardholder to authenticate, which cannot happen here. Nothing was taken. Use the card widget for this card. |
gateway_connection_error | 503 | The payment gateway could not be reached, so nothing was charged. 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 code challenge_in_progress is rare: treat the payment as not taken, and contact support if it persists.
An unknown outcome is not a failed one. After acquirer_timeout, 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. A paid checkout cannot be paid twice, but a replayed charge on a new checkout can charge the customer twice.
{ "detail": "The payment was declined.",
"code": "payment_declined",
"errors": {},
"transaction_id": "trs_91c4d0aa" }Retries and idempotency
- A checkout can be paid once. A second call on a paid checkout is refused with
checkout_already_paid, which is why a checkout is the unit to retry against - Only one attempt runs on a checkout at a time: a call made while another is running gets
attempt_in_progress - After a refusal where nothing was taken (
payment_declined,insufficient_funds,invalid_auth,challenge_not_completable), the checkout stays payable and you can call/pay/again on it, with the same or another card - After an unclear outcome, read the checkout first. Only create a new checkout if the first one shows no payment
Sandbox
Call api.sandbox.yavin.com with your sandbox key: the same endpoints and codes, with no card charged and no money moved. Card details in Form B behave like the sandbox test cards of the card widget: see the test-card table in Online payment drop-in integration. Use any future expiry and any 3-digit cvv. A card that triggers a 3-D Secure step-up in the widget is refused here with challenge_not_completable.