# 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](https://docs.yavin.com/docs/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](https://docs.yavin.com/docs/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

```mermaid
sequenceDiagram
    autonumber
    participant B as Your backend
    participant A as Yavin API

    B->>A: POST /checkouts/ with the amount
    A-->>B: 201 with checkout_token
    B->>A: POST /checkouts/{checkout_token}/pay/ with customer_card_id, or card details
    A->>A: charge the card, no cardholder involved
    A-->>B: 200 with the checkout, status ok
    A-)B: webhook payment_received
```

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.

```bash
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" }
```

```json
// 201 Created
{ "checkout_token": "Zk9q...",
  "payment_link": "https://..." }
```

- amount is in cents, minimum 100

- 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: false cannot be paid from your server

- Keep your webhook_url: it receives payment_received when the charge succeeds

---

## 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

```bash
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" }
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| customer_card_id | String (UUID) | yes | 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](https://docs.yavin.com/docs/save-a-card-and-charge-it-later).

### Form B - card details

```bash
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" }
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| card_number | String | yes | The card number, digits only |
| expiry_date | String | yes | MMYY, for example 1228 for December 2028 |
| cvv | String | yes | 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.

### Response

```json
// 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 } ] }
```

The response is the [Online payment: Ecommerce API](https://docs.yavin.com/docs/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.

---

## 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:

```bash
POST https://api.yavin.com/api/v5/ecommerce/get_cart_information/
{ "checkout_external_id": "invoice_2041" }
```

Release goods on the checkout status (ok) and paid_amount, never on a missing error.

---

## 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.

```json
{ "detail": "The payment was declined.",
  "code": "payment_declined",
  "errors": {},
  "transaction_id": "trs_91c4d0aa" }
```

### 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.

---

## 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](https://docs.yavin.com/docs/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.

---

## Related pages

[Save a card and charge it later](https://docs.yavin.com/docs/save-a-card-and-charge-it-later)

[Online payment drop-in integration](https://docs.yavin.com/docs/online-payment-drop-in-integration)

[Online payment: Ecommerce API](https://docs.yavin.com/docs/online-payment-ecommerce-api)

[Webhooks Management](https://docs.yavin.com/docs/webhooks-management)
