# Integrate: Server-to-Server

An interactive bench for API-only integrations. You render the checkout, collect the payment details, and send one server-to-server request with `confirm: true` and your secret key. Pick a payment method on the left and the page generates a runnable request for it, then walks the steps that method actually needs.

This is the style with the most responsibility on your side, compliance included: whatever credential you collect, you are handling account data, and your systems are in PCI DSS scope for it. How much scope differs sharply by credential — see below.

## What this page covers
- A payment-method rail grouped by category — card, bank transfer, pay later, wallet, voucher, crypto — showing each method's currency, flow shape and refund rule
- A generated `POST /payments` body with `confirm: true`, the method's `payment_method_data`, and the billing fields the connector reads the shopper identifier from
- Controls for the fields each method needs: card or network-token credentials, the encrypted wallet payloads, PayShap's bank, Capitec Pay's account type, Mobicred's password, RCS's store card number, 1Voucher's voucher PIN
- The shopper redirect, embedded in an iframe with a new-tab fallback when the provider refuses to be framed
- Inline QR for Scan to Pay and MauCAS via `payment_experience: "display_qr_code"`
- Final-state confirmation through the webhook feed and `GET /payments/{id}?force_sync=true`
- Refund steps for the methods that support them, with each method's limits applied

## Flow shapes
Which steps you get depends on the method:

- **Direct** (cards, network tokens, wallet passthrough) — you already hold the credential, so there is no provider page to send anyone to. A 3-D Secure challenge can still redirect, but that is authentication, not the payment method.
- **Asynchronous redirect** (most alternative methods) — confirm returns `requires_customer_action` with `next_action.redirect_to_url`. The final state arrives by webhook or PSync.
- **Synchronous** (1Voucher only) — the confirm response is already `succeeded` or `failed`. No redirect, no pending state.
- **QR or redirect** (Scan to Pay, MauCAS) — redirects by default; add `payment_experience: "display_qr_code"` and the response carries `next_action.qr_code_information` instead, with a PNG data URL, the scan target and `display_to_timestamp`.

## Card-backed credentials

Cards are not a redirect method, and the API-only style offers four different ways to present a card-backed credential. They differ mainly in what you have to hold, and therefore in your compliance burden.

PCI DSS calls the whole lot **account data**, split in two: **cardholder data** (the card number, expiry, cardholder name, service code) and **sensitive authentication data** (the CVC, track data, PINs, and the token cryptogram's equivalent). Sensitive authentication data may not be stored once the payment is authorised, in any form. Every credential below is account data of some kind — none of them takes your systems out of scope on its own, and which validation route applies to you is a question for your acquirer or QSA, not for this page.

| Credential | `payment_method` | You hold | Your PCI scope |
|---|---|---|---|
| Card (PAN) | `card` | Card number, expiry, name, CVC | Highest — cardholder data *and* SAD in your systems |
| Network token | `network_token` | A network token + cryptogram | Lower — account data, but a surrogate restricted to you |
| Apple Pay | `wallet` | An encrypted Apple payload | Lowest — opaque, you forward and never decrypt |
| Google Pay | `wallet` | An encrypted Google token | Lowest — opaque, you forward and never decrypt |
| Samsung Pay | `wallet` | An encrypted Samsung credential | Lowest — opaque, you forward and never decrypt |

### Card (PAN)

**This is the heaviest option there is.** The number, expiry and name are cardholder data; the CVC is sensitive authentication data, which may never be stored after authorisation — not encrypted, not hashed, not in a log or a support ticket. Capture cards only in an environment you have had assessed for exactly that, then send them with `confirm: true`. If that is not you, do not reach for a network token as the way out — embed our checkout or use our hosted page, where the card data goes to us and never reaches your servers.

```json
"payment_method": "card",
"payment_method_type": "credit",
"payment_method_data": {
  "card": {
    "card_number": "4200000000000091",
    "card_exp_month": "01",
    "card_exp_year": "32",
    "card_cvc": "123",
    "card_holder_name": "Test Holder"
  }
}
```

### Network token

A token the card network provisioned for the card, sent with a single-use cryptogram. It is a surrogate for the card number and the network restricts it to you, so a stolen one is worth far less than a stolen card — but the token and its expiry are still cardholder data, and the cryptogram behaves as sensitive authentication data: single use, and not something to keep once the authorisation is done. Hold both the way you would hold card data. You also need to be onboarded with the network as a token requestor.

```json
"payment_method": "network_token",
"payment_method_type": "network_token",
"payment_method_data": {
  "network_token": {
    "network_token": "4111111111111111",
    "token_exp_month": "01",
    "token_exp_year": "32",
    "token_cryptogram": "AgAAAAAA...",
    "card_holder_name": "Test Holder",
    "eci": "05"
  }
}
```

- `payment_method_type` must be `network_token` too. Pairing it with `credit` is rejected: *"payment_method_type doesn't correspond to the specified payment_method"*.
- The cryptogram is single-use per authorisation — not a credential you can store and replay.
- A connector has to be configured to accept network tokens, or routing returns *"No eligible connector was found"*.

## Wallet passthrough: Apple Pay, Google Pay, Samsung Pay

Passthrough means exactly what it says: the device hands you an **encrypted** payload, you forward it untouched, and the PSP decrypts it. No cleartext account data ever reaches you, which is what makes these the lowest-scope way to take a card-backed payment server-to-server — lowest, not zero: the page or app you collect on is still yours. All three send `payment_method: "wallet"` with the brand as `payment_method_type`.

What you need in place first is a wallet merchant identity — an Apple merchant identifier with domain verification, a Google Pay merchant profile, or a Samsung Pay service registration. The playground cannot fabricate a working token: they are single-use and bound to the session that produced them, so these requests are here to show the shape.

**Apple Pay** — `payment_data` is the base64 token from the Apple Pay session, forwarded as-is.

```json
"payment_method": "wallet",
"payment_method_type": "apple_pay",
"payment_method_data": {
  "wallet": {
    "apple_pay": {
      "payment_data": "eyJ2ZXJzaW9uIjoiRUNfdjEi...",
      "payment_method": {
        "display_name": "Visa 1234",
        "network": "Visa",
        "type": "debit"
      },
      "transaction_identifier": "c5e...9f2"
    }
  }
}
```

**Google Pay** — request the token with the `PAYMENT_GATEWAY` tokenization type. `DIRECT` means you decrypt it yourself, which is a different integration.

```json
"payment_method": "wallet",
"payment_method_type": "google_pay",
"payment_method_data": {
  "wallet": {
    "google_pay": {
      "type": "CARD",
      "description": "Visa •••• 1234",
      "info": { "card_network": "VISA", "card_details": "1234" },
      "tokenization_data": { "type": "PAYMENT_GATEWAY", "token": "{...}" }
    }
  }
}
```

**Samsung Pay** — the credential arrives under `payment_credential`, with the encrypted data in the `3_d_s` object.

```json
"payment_method": "wallet",
"payment_method_type": "samsung_pay",
"payment_method_data": {
  "wallet": {
    "samsung_pay": {
      "payment_credential": {
        "card_brand": "visa",
        "card_last4digits": "1234",
        "3_d_s": { "type": "S", "version": "100", "data": "eyJ0eXAiOiJKV1Qi..." }
      }
    }
  }
}
```

All three settle on card rails, so refunds behave like a card refund. If the connector reports *"card properties must be set"* (`200.300.404`), it received the request but could not decrypt the payload — that is what a fabricated or expired token looks like.

## Customer-initiated vs merchant-initiated

Every flow above is a **CIT** — a customer-initiated transaction. The shopper is present and authorises the payment themselves. An **MIT** charges a credential they agreed to earlier with nobody there: subscriptions, usage billing, a retry after they have gone.

An MIT is always two payments, never one, because the customer's acceptance of the mandate has to be captured while they are present and cannot be recorded afterwards.

**Step 1 — customer-present setup.** A normal confirm plus the two fields that make the credential reusable:

```json
"customer_id": "cust_123",
"setup_future_usage": "off_session",
"authentication_type": "three_ds",
"mandate_data": {
  "customer_acceptance": {
    "acceptance_type": "online",
    "online": { "ip_address": "127.0.0.1", "user_agent": "Mozilla/5.0" }
  },
  "mandate_type": { "multi_use": { "amount": 100000, "currency": "ZAR" } }
}
```

The response carries `payment_method_id` — the handle you charge against later. 3-D Secure on this first payment is what makes later off-session charges acceptable to the issuer.

**Step 2 — merchant-initiated charge.** No payment method data, no shopper, no authentication:

```json
"amount": 6500,
"currency": "ZAR",
"confirm": true,
"off_session": true,
"customer_id": "cust_123",
"recurring_details": { "type": "payment_method_id", "data": "pm_..." }
```

### Two vaults, one field

`recurring_details` covers both vaulting models, and which one you use is the real choice behind an MIT.

**Vault with us.** We store the credential and give you a `payment_method_id`. That id only exists after a customer-present setup payment, which is why that flow has one.

```json
"recurring_details": { "type": "payment_method_id", "data": "pm_..." }
```

**Bring your own vault.** You store the credential. Each charge sends it along with the `network_transaction_id` from the original customer-present payment — there is nothing for us to look up, and no setup payment in this flow because the credential already exists on your side.

```json
"recurring_details": {
  "type": "network_transaction_id_and_card_details",
  "data": {
    "card_number": "4200000000000091",
    "card_exp_month": "01",
    "card_exp_year": "32",
    "card_holder_name": "Test Holder",
    "network_transaction_id": "016153570198200"
  }
}
```

For a network token in your own vault, the same idea with the token variant:

```json
"recurring_details": {
  "type": "network_transaction_id_and_network_token_details",
  "data": {
    "network_token": "4111111111111111",
    "token_exp_month": "01",
    "token_exp_year": "32",
    "card_holder_name": "Test Holder",
    "network_transaction_id": "016153570198200"
  }
}
```

The `network_transaction_id` is not decoration: it is what proves to the network that the cardholder agreed to this originally. A merchant migrating between vaults has to bring their transaction history with them, not just their card data.

The full set of `recurring_details` variants, including ones this page does not walk:

| Variant | When |
|---|---|
| `payment_method_id` | Credential vaulted with us |
| `mandate_id` | Charge against a mandate rather than a stored payment method |
| `processor_payment_token` | A token held by the processor, scoped by `merchant_connector_id` |
| `network_transaction_id_and_card_details` | Own-vault PAN |
| `network_transaction_id_and_network_token_details` | Own-vault network token |
| `card_with_limited_data` | Card number and expiry only, without a network transaction id |

Things that catch people out:

- **`payment_method_id` is not a field on `POST /payments`.** Sending it at the top level is rejected as an unknown field. A stored credential is always referenced through `recurring_details`, whichever vault it lives in.
- With our vault, the setup payment must actually complete. Until it does, the credential is not chargeable and the MIT comes back with no eligible connector.
- Own-vault MITs are structurally accepted on sandbox but return *"No eligible connector was found"* unless a connector is configured to accept that credential type.
- An MIT can be declined outright. The issuer may want the shopper back, which arrives as a failure rather than a redirect — there is nobody to redirect.
- **Only card-backed credentials support this.** PANs and network tokens can be stored and charged later; the alternative payment methods cannot, so every alternative payment method flow here is CIT-only. Network tokens are the better credential for recurring charges, since the token survives card reissue.

## Billing requirements
Several methods derive the shopper identifier from the billing address, so these fields are effectively required:

| Method | Requires | Format sent to Peach |
|---|---|---|
| PayShap | phone | `+27-711111200` (hyphenated international) |
| Capitec Pay (cellphone, no `account_id`) | phone | `0711111200` (local, 10 digits) |
| M-PESA | phone | `254711111200` (digits only, with country code) |
| blink by Emtel, MCB Juice | phone | local 8-digit number as provided |
| 1Voucher | phone | `27711111200` (digits only, no plus) |
| Mobicred | email | the account identifier |

Billing phone is always sent in split form: `{ "number": "711111200", "country_code": "+27" }`.

## Key concepts
- All *alternative* payment methods support **automatic capture only** — manual capture and void are rejected up front. Cards are the exception: they support the full authorisation lifecycle, covered under Integrate: cards.
- Amounts are in **minor units** (`6000` = R60.00). Zero-amount payments are not accepted.
- Peach rate-limits status queries to **2 per minute per transaction**, so automatic polling here is 30 seconds apart. A rate-limited sync keeps the current status rather than failing the payment.
- Failures carry the Peach result code in `error_code` — for example `100.396.101` (cancelled by shopper) or `800.100.152` (declined) — including declines Peach returns with HTTP 200.
- `800.900.201` ("unknown channel") means the Entity ID is not enabled for that brand: an account-provisioning issue, not a request bug.

## Sandbox behaviour
- **Pay by Bank**, **Capitec Pay**, **PayShap**, **Payflex**, **Float**, **ZeroPay**, **Scan to Pay**, **Happy Pay**, **Apple Pay**, **Google Pay**, **RCS**, **blink by Emtel**, **MauCAS**, **MCB Juice**, and **M-PESA** can be tested in sandbox. Credentials and steps are on [Testing](/docs/testing.md).
- Capitec Pay uses the registered test ID numbers on that page. PayShap uses the registered test phone numbers.
- Several other alternative payment methods cannot be tested in sandbox — perform testing in the live environment.
- Intermittent acquirer errors surface as `900.100.1xx` — retry later.

## Related
- [Integrate: Hosted Checkout](/integrate/hosted-checkout.md): the same methods with Peach hosting the page
- [Integrate: Embedded Checkout](/integrate/sdk.md): one embedded checkout for every method
- [Integrate: cards](/integrate/cards.md): 3-D Secure, saving cards, and the authorisation lifecycle
- [API reference](/playground.md): every endpoint, with the OpenAPI reference beside it

---

Interactive version: https://playground.peachpayments.com/integrate/api-only
