> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zafapay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Server-to-Server Payments

> Collect card details on your own form and process payments via the API

## Overview

Server-to-Server (S2S) payments let you collect card details on your own payment form instead of using the hosted checkout page. Card data is securely tokenized in the browser using our JavaScript SDK, and the token is sent to your server to create a payment.

<Note>
  S2S integration requires PCI SAQ A-EP compliance. If you are unsure about your PCI compliance level, use [hosted checkout](/en/guide/card-payments) instead.
</Note>

## How It Works

```mermaid theme={null}
sequenceDiagram
    participant B as Browser
    participant S as Your Server
    participant Z as ZAFA PAY API

    B->>Z: 1. Create token (JS SDK, publishable key)
    Z-->>B: tok_xxx
    B->>S: 2. Send token to your server
    S->>Z: 3. Create payment with token (secret key)
    Z-->>S: Payment result
    S-->>B: 4. Show result to customer
```

<Steps>
  <Step title="Tokenize card in browser">
    Call `POST /v1/tokens` with your **publishable key** (`pk_live_*` / `pk_test_*`) to tokenize card details. You can use the JavaScript SDK or call the API directly. The raw card number never reaches your server.
  </Step>

  <Step title="Create payment">
    Send the `tok_*` token to your backend, then call `POST /v1/payments` with the `token` parameter using your **secret access token**.
  </Step>

  <Step title="Handle 3D Secure (if required)">
    If the response status is `requires_action`, redirect the customer to `redirect_url` to complete 3D Secure authentication.
  </Step>
</Steps>

## Integration

### 1. Tokenize card details

Collect card details on your payment page and send them to `POST /v1/tokens` using your publishable key. You can use the JavaScript SDK or call the API directly.

<CodeGroup>
  ```javascript JavaScript SDK theme={null}
  // Load: <script src="https://js.zafapay.com/v1/zafapay.js"></script>
  const zafapay = Zafapay('pk_test_xxxxx');

  try {
    const { token, card } = await zafapay.createToken({
      number: '4242424242424242',
      exp_month: 12,
      exp_year: 2027,
      cvc: '123',
      cardholder_name: 'John Doe'  // optional
    });

    console.log(token);      // "tok_xxxxxxxxxxxxxxxxxxxxxx"
    console.log(card.brand);  // "visa"
    console.log(card.last4);  // "4242"

    // Send token to your server
    await fetch('/your-server/pay', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token, amount: 10.00, currency: 'usd' })
    });
  } catch (error) {
    console.error(error.message);
  }
  ```

  ```javascript Direct API Call theme={null}
  try {
    const response = await fetch('https://api.sandbox.zafapay.com/v1/tokens', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer pk_test_xxxxx',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        card: {
          number: '4242424242424242',
          exp_month: 12,
          exp_year: 2027,
          cvc: '123',
          cardholder_name: 'John Doe'
        }
      })
    });

    const { id, card } = await response.json();
    // id: "tok_xxxxxxxxxxxxxxxxxxxxxx"

    // Send token to your server
    await fetch('/your-server/pay', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token: id, amount: 10.00, currency: 'usd' })
    });
  } catch (error) {
    console.error(error.message);
  }
  ```
</CodeGroup>

**Response properties:**

| Field            | Description                                                 |
| ---------------- | ----------------------------------------------------------- |
| `id`             | Token ID (`tok_` prefix). Valid for 30 minutes, single use. |
| `card.last4`     | Last 4 digits                                               |
| `card.brand`     | Card brand (`visa`, `mastercard`, `amex`, etc.)             |
| `card.exp_month` | Expiration month                                            |
| `card.exp_year`  | Expiration year                                             |
| `expires_at`     | Token expiration timestamp (ISO 8601)                       |

<Info>
  The JavaScript SDK returns the token ID as `token` instead of `id` for convenience. When calling the API directly, the field name is `id`.
</Info>

### 2. Create payment with token

On your server, call `POST /v1/payments` with the token.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.sandbox.zafapay.com/v1/payments \
    -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "amount": 10.00,
      "currency": "usd",
      "token": "tok_xxxxxxxxxxxxxxxxxxxxxx",
      "return_url": "https://your-site.com/payment-complete",
      "external_id": "order_12345"
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch('https://api.sandbox.zafapay.com/v1/payments', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      amount: 10.00,
      currency: 'usd',
      token: 'tok_xxxxxxxxxxxxxxxxxxxxxx',
      return_url: 'https://your-site.com/payment-complete',
      external_id: 'order_12345'
    })
  });

  const payment = await response.json();
  ```
</CodeGroup>

### 3. Handle the response

The response status determines the next step:

| Status            | HTTP Code | Action                                                                    |
| ----------------- | --------- | ------------------------------------------------------------------------- |
| `completed`       | 201       | Payment succeeded. Show success page.                                     |
| `authorized`      | 201       | Authorization succeeded (when `capture_method: "manual"`). Capture later. |
| `requires_action` | 202       | 3D Secure required. Redirect customer to `redirect_url`.                  |
| `failed`          | 400       | Payment failed. Show error to customer.                                   |

**Handling 3D Secure:**

```javascript theme={null}
const payment = await response.json();

if (payment.status === 'requires_action') {
  // Redirect customer to 3DS authentication page
  window.location.href = payment.redirect_url;
  // After 3DS, customer is redirected to your return_url
}
```

<Warning>
  Always include `return_url` for S2S payments. If 3D Secure is triggered, the customer is redirected to this URL after authentication with `status=succeeded` or `status=failed` as a query parameter. Without `return_url`, the customer will have no redirect destination after 3DS.
</Warning>

## Save Card for Recurring Payments

You can save the customer's card during an S2S token payment by adding `save_card: true` and `customer_id` to the payment request. After the payment completes (including 3D Secure if required), the card is saved and a `payment_method_id` is returned in the webhook.

### Request

```bash theme={null}
curl -X POST https://api.sandbox.zafapay.com/v1/payments \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 10.00,
    "currency": "usd",
    "token": "tok_xxxxxxxxxxxxxxxxxxxxxx",
    "customer_id": "cust_abc123",
    "save_card": true,
    "return_url": "https://your-site.com/payment-complete",
    "external_id": "order_12345"
  }'
```

<ParamField body="save_card" type="boolean">
  Set to `true` to save the card after payment completion.
</ParamField>

<ParamField body="customer_id" type="string">
  Required when `save_card` is `true`. A unique ID to identify the customer.
</ParamField>

### Webhook

On payment completion, the webhook includes the saved `payment_method_id`:

```json theme={null}
{
  "event": "payment.succeeded",
  "transaction_id": "tx_abc123",
  "status": "succeeded",
  "amount": "10.00",
  "currency": "usd",
  "external_id": "order_12345",
  "payment_method": "card",
  "payment_method_id": "pmi_xyz789",
  "save_card": true,
  "is_recurring": false,
  "customer_id": "cust_abc123",
  "card_brand": "visa",
  "card_last4": "4242",
  "card_country": "US",
  "card_funding": "credit",
  "metadata": {},
  "created_at": "2026-04-08T10:00:00.000Z",
  "timestamp": "2026-04-08T10:00:05.000Z"
}
```

### Subsequent Recurring Payments

Use the saved `payment_method_id` to charge the customer without requiring card input or tokenization:

```bash theme={null}
curl -X POST https://api.sandbox.zafapay.com/v1/payments \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 10.00,
    "currency": "usd",
    "customer_id": "cust_abc123",
    "payment_method_id": "pmi_xyz789",
    "external_id": "subscription_renewal_002"
  }'
```

For full details on managing saved cards and handling recurring payment failures, see [Recurring Payments](/en/guide/recurring-payments).

## Full Example

### Client-side (Browser)

```html theme={null}
<form id="payment-form">
  <input type="text" id="card-number" placeholder="Card number" />
  <input type="text" id="card-expiry-month" placeholder="MM" />
  <input type="text" id="card-expiry-year" placeholder="YYYY" />
  <input type="text" id="card-cvc" placeholder="CVC" />
  <input type="text" id="cardholder-name" placeholder="Name on card" />
  <button type="submit">Pay $10.00</button>
</form>

<script src="https://js.zafapay.com/v1/zafapay.js"></script>
<script>
const zafapay = Zafapay('pk_test_xxxxx');

document.getElementById('payment-form').addEventListener('submit', async (e) => {
  e.preventDefault();

  try {
    // 1. Tokenize card
    const { token } = await zafapay.createToken({
      number: document.getElementById('card-number').value,
      exp_month: parseInt(document.getElementById('card-expiry-month').value),
      exp_year: parseInt(document.getElementById('card-expiry-year').value),
      cvc: document.getElementById('card-cvc').value,
      cardholder_name: document.getElementById('cardholder-name').value
    });

    // 2. Send token to your server
    const response = await fetch('/api/pay', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token })
    });

    const payment = await response.json();

    // 3. Handle result
    if (payment.status === 'requires_action') {
      window.location.href = payment.redirect_url;
    } else if (payment.status === 'completed' || payment.status === 'authorized') {
      window.location.href = '/success';
    } else {
      alert('Payment failed: ' + payment.error?.message);
    }
  } catch (error) {
    alert('Error: ' + error.message);
  }
});
</script>
```

### Server-side

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  const express = require('express');
  const app = express();
  app.use(express.json());

  app.post('/api/pay', async (req, res) => {
    const { token } = req.body;

    // In production, retrieve amount/currency from your order database
    const response = await fetch('https://api.sandbox.zafapay.com/v1/payments', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        amount: 10.00,
        currency: 'usd',
        token,
        return_url: 'https://your-site.com/payment-complete',
        external_id: `order_${Date.now()}`
      })
    });

    const payment = await response.json();
    res.json(payment);
  });
  ```

  ```python Python (Flask) theme={null}
  from flask import Flask, request, jsonify
  import requests
  import time

  app = Flask(__name__)

  @app.route('/api/pay', methods=['POST'])
  def pay():
      token = request.json['token']

      # In production, retrieve amount/currency from your order database
      response = requests.post(
          'https://api.sandbox.zafapay.com/v1/payments',
          headers={
              'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
              'Content-Type': 'application/json'
          },
          json={
              'amount': 10.00,
              'currency': 'usd',
              'token': token,
              'return_url': 'https://your-site.com/payment-complete',
              'external_id': f'order_{int(time.time())}'
          }
      )

      return jsonify(response.json())
  ```

  ```php PHP theme={null}
  <?php
  $payload = json_decode(file_get_contents('php://input'), true);
  $token = $payload['token'];

  // In production, retrieve amount/currency from your order database
  $ch = curl_init('https://api.sandbox.zafapay.com/v1/payments');
  curl_setopt_array($ch, [
      CURLOPT_POST => true,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => [
          'Authorization: Bearer YOUR_ACCESS_TOKEN',
          'Content-Type: application/json'
      ],
      CURLOPT_POSTFIELDS => json_encode([
          'amount' => 10.00,
          'currency' => 'usd',
          'token' => $token,
          'return_url' => 'https://your-site.com/payment-complete',
          'external_id' => 'order_' . time()
      ])
  ]);

  $response = curl_exec($ch);
  curl_close($ch);

  header('Content-Type: application/json');
  echo $response;
  ```
</CodeGroup>

## Publishable Key

The publishable key (`pk_test_*` / `pk_live_*`) is used to authenticate tokenization requests from the browser. It can only be used to create tokens and cannot access payments, customers, or any other API resources.

You can obtain your publishable key from the merchant dashboard under **Merchant Settings > API Settings**.

For details, see [Authentication](/en/api-reference/authentication).

## Test Cards

| Card Number      | Description        |
| ---------------- | ------------------ |
| 4242424242424242 | Successful payment |
| 4000002500003155 | Requires 3D Secure |
| 4000000000000002 | Declined           |
| 4000000000009995 | Insufficient funds |

<Info>
  For complete API parameters and response details, see the [Create Payment](/api-reference/payments/create-payment) API reference and [Create Token](/en/api-reference/tokens) API reference.
</Info>
