Skip to main content

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.

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.
S2S integration requires PCI SAQ A-EP compliance. If you are unsure about your PCI compliance level, use hosted checkout instead.

How It Works

1

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

Create payment

Send the tok_* token to your backend, then call POST /v1/payments with the token parameter using your secret access token.
3

Handle 3D Secure (if required)

If the response status is requires_action, redirect the customer to redirect_url to complete 3D Secure authentication.

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.
// 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);
}
Response properties:
FieldDescription
idToken ID (tok_ prefix). Valid for 30 minutes, single use.
card.last4Last 4 digits
card.brandCard brand (visa, mastercard, amex, etc.)
card.exp_monthExpiration month
card.exp_yearExpiration year
expires_atToken expiration timestamp (ISO 8601)
The JavaScript SDK returns the token ID as token instead of id for convenience. When calling the API directly, the field name is id.

2. Create payment with token

On your server, call POST /v1/payments with the token.
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"
  }'

3. Handle the response

The response status determines the next step:
StatusHTTP CodeAction
completed201Payment succeeded. Show success page.
authorized201Authorization succeeded (when capture_method: "manual"). Capture later.
requires_action2023D Secure required. Redirect customer to redirect_url.
failed400Payment failed. Show error to customer.
Handling 3D Secure:
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
}
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.

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

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"
  }'
save_card
boolean
Set to true to save the card after payment completion.
customer_id
string
Required when save_card is true. A unique ID to identify the customer.

Webhook

On payment completion, the webhook includes the saved payment_method_id:
{
  "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:
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.

Full Example

Client-side (Browser)

<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

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);
});

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.

Test Cards

Card NumberDescription
4242424242424242Successful payment
4000002500003155Requires 3D Secure
4000000000000002Declined
4000000000009995Insufficient funds
For complete API parameters and response details, see the Create Payment API reference and Create Token API reference.