# Server-to-server payments

Server-to-server (S2S) is the right choice when you already have a card-collection UI and need to **keep customers on your own domain**. You handle the card form; Fynex handles the processor integration, 3DS, and settlement.

> [!CAUTION]
> S2S means your servers receive PAN/CVV. You are responsible for PCI DSS compliance up to **SAQ D** scope. If that is not workable, use [hosted checkout](https://api.fynex.ai/payments-api/v2/docs#tag/hosted-checkout) instead — it keeps you at SAQ A.

## Lifecycle

```
Customer            Your backend                    Fynex
────────            ─────────────                   ─────
fills card form ─►  POST /initialize-payment ──────► authorizes via processor
                                                     returns status + actionUrl?
                ◄── 202 response
                    if requiresAction:
redirect ──────────────────────────────────────────► customer completes 3DS
                                                     browser bounces back
                    POST /finalize-payment ─────────► captures / finalizes
                                                     returns final status
                ◄── 200 result
                    verify status, fulfil order
```

> [!NOTE]
> Fynex delivers a `PaymentCompleted` webhook to the webhook URL(s) configured on your seller account (your receiver must return HTTP 200). You can also use the GraphQL `genericPayment(id)` query or SSE to poll for the final status — recommended as a backstop. See [Polling & SSE](https://api.fynex.ai/payments-api/v2/docs#tag/polling-sse).

## Step-by-step

1. **Collect card details**

   Build a form on your frontend that captures PAN, expiry month/year, CVV, and cardholder name. Submit them to your backend over HTTPS — never log raw card numbers.

2. **Initialize the payment**

   `POST /payments-api/v1/initialize-payment` with `Authorization: Bearer <token>` and a unique `Idempotency-Key` (UUID) header.

#### curl

```bash
curl -sS -X POST "$FYNEX_API/payments-api/v1/initialize-payment" \
  -H "Authorization: Bearer $FYNEX_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "externalOrderRef": "ORDER-1042",
    "amount": 49.00,
    "paymentType": "card",
    "paymentMethod": "card",
    "currencyCode": "GBP",
    "countryCode": "GB",
    "autoSettlement": false,
    "captureMode": "manual",
    "cardData": {
      "cardNumber": "4111111111111111",
      "expMonth": 12,
      "expYear": 2028,
      "cvv": "123",
      "holderName": "Jane Doe"
    },
    "billingDetails": {
      "country": "GB",
      "zip": "SW1A1AA",
      "city": "London",
      "street": "1 Example Street"
    },
    "returnLinks": [
      { "rel": "on_completed", "href": "https://example.com/orders/1042/success", "method": "GET" },
      { "rel": "on_failed",    "href": "https://example.com/orders/1042/failure", "method": "GET" },
      { "rel": "default",      "href": "https://example.com/orders/1042/return",  "method": "GET" }
    ]
  }'
```

#### JavaScript

```js
import { randomUUID } from 'node:crypto';

export async function initializePayment(order, card) {
  const res = await fetch(
    `${process.env.FYNEX_API}/payments-api/v1/initialize-payment`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.FYNEX_TOKEN}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': randomUUID(),
      },
      body: JSON.stringify({
        externalOrderRef: order.id,
        amount: order.amount,
        paymentType: 'card',
        paymentMethod: 'card',
        currencyCode: order.currency,
        countryCode: order.countryCode,
        autoSettlement: false,
        captureMode: 'manual',
        cardData: {
          cardNumber: card.number,
          expMonth: card.expMonth,
          expYear: card.expYear,
          cvv: card.cvv,
          holderName: card.holderName,
        },
        billingDetails: {
          country: order.billingCountry,
          zip: order.billingZip,
          city: order.billingCity,
          street: order.billingStreet,
        },
        returnLinks: [
          { rel: 'on_completed', href: order.successUrl, method: 'GET' },
          { rel: 'on_failed',    href: order.failureUrl, method: 'GET' },
          { rel: 'default',      href: order.returnUrl,  method: 'GET' },
        ],
      }),
    }
  );
  if (!res.ok) throw new Error(await res.text());
  return res.json(); // 202 Accepted
}
```

#### Python

```python
import os
import uuid
import requests

def initialize_payment(order: dict, card: dict) -> dict:
    res = requests.post(
        f"{os.environ['FYNEX_API']}/payments-api/v1/initialize-payment",
        headers={
            "Authorization": f"Bearer {os.environ['FYNEX_TOKEN']}",
            "Content-Type": "application/json",
            "Idempotency-Key": str(uuid.uuid4()),
        },
        json={
            "externalOrderRef": order["id"],
            "amount": order["amount"],
            "paymentType": "card",
            "paymentMethod": "card",
            "currencyCode": order["currency"],
            "countryCode": order["country_code"],
            "autoSettlement": False,
            "captureMode": "manual",
            "cardData": {
                "cardNumber": card["number"],
                "expMonth": card["exp_month"],
                "expYear": card["exp_year"],
                "cvv": card["cvv"],
                "holderName": card["holder_name"],
            },
            "billingDetails": {
                "country": order["billing_country"],
                "zip": order["billing_zip"],
                "city": order["billing_city"],
                "street": order["billing_street"],
            },
            "returnLinks": [
                {"rel": "on_completed", "href": order["success_url"], "method": "GET"},
                {"rel": "on_failed",    "href": order["failure_url"], "method": "GET"},
                {"rel": "default",      "href": order["return_url"],  "method": "GET"},
            ],
        },
        timeout=15,
    )
    res.raise_for_status()
    return res.json()  # 202 Accepted
```

   The response is `202 Accepted` for a new payment:

   ```json
   {
     "paymentId": "ORDER-1042",
     "status": "provider_pending",
     "amount": 49.00,
     "currencyCode": "GBP",
     "requiresAction": false,
     "actionUrl": ""
   }
   ```

   When 3DS is required the response looks like:

   ```json
   {
     "paymentId": "ORDER-1042",
     "status": "provider_pending",
     "amount": 49.00,
     "currencyCode": "GBP",
     "requiresAction": true,
     "actionUrl": "https://3ds.example.com/challenge/..."
   }
   ```

3. **Handle 3DS (if `requiresAction: true`)**

   See [3DS handling](#3ds-handling) below for the full flow. Short version: redirect the customer's browser to `actionUrl`, then proceed to finalize once they return.

   When `requiresAction: false`, skip this step entirely.

4. **Finalize the payment**

   `POST /payments-api/v1/finalize-payment` with `paymentId` (your `externalOrderRef`) and a new unique `Idempotency-Key`.

#### curl

```bash
curl -sS -X POST "$FYNEX_API/payments-api/v1/finalize-payment" \
  -H "Authorization: Bearer $FYNEX_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "paymentId": "ORDER-1042" }'
```

#### JavaScript

```js
export async function finalizePayment(paymentId) {
  const res = await fetch(
    `${process.env.FYNEX_API}/payments-api/v1/finalize-payment`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.FYNEX_TOKEN}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': randomUUID(),
      },
      body: JSON.stringify({ paymentId }),
    }
  );
  if (!res.ok) throw new Error(await res.text());
  return res.json(); // 200 OK
}
```

#### Python

```python
def finalize_payment(payment_id: str) -> dict:
    res = requests.post(
        f"{os.environ['FYNEX_API']}/payments-api/v1/finalize-payment",
        headers={
            "Authorization": f"Bearer {os.environ['FYNEX_TOKEN']}",
            "Content-Type": "application/json",
            "Idempotency-Key": str(uuid.uuid4()),
        },
        json={"paymentId": payment_id},
        timeout=15,
    )
    res.raise_for_status()
    return res.json()
```

   The response is `200 OK`:

   ```json
   {
     "paymentId": "ORDER-1042",
     "status": "provider_completed",
     "providerPaymentId": "pay_01J2EXAMPLE",
     "capturedAmount": 49.00,
     "currencyCode": "GBP"
   }
   ```

5. **Verify and fulfil**

   Mark the order paid only when `status` reaches a terminal success value. If `failureCode` is present, the payment was declined — surface the `failureDescription` to the customer as appropriate.

   Poll `genericPayment(id)` if you need to check status asynchronously. See [Polling & SSE](https://api.fynex.ai/payments-api/v2/docs#tag/polling-sse).

## Request fields reference

### `POST /initialize-payment` — required fields

| Field | Type | Description |
|-------|------|-------------|
| `externalOrderRef` | string | Your order reference. Returned as `paymentId` in all subsequent responses and used as the path param for capture/refund. |
| `amount` | float | Major units (e.g., `49.00`). |
| `paymentType` | string | `card`, `bank_transfer`, or `apm`. |
| `paymentMethod` | string | `card`, `bank_account`, `google_pay`, or `apple_pay`. Must be compatible with `paymentType`: card type accepts `card`/`google_pay`/`apple_pay`; bank_transfer accepts only `bank_account`; apm type accepts `bancontact`/`multibanco`/`mbway`/`wero`/`swish` (see [Alternative payment methods (APM)](https://api.fynex.ai/payments-api/v2/docs#tag/alternative-payment-methods-apm) for the redirect flow). |
| `currencyCode` | string (3-letter) | Must be one of: `EUR`, `USD`, `GBP`, `DKK`, `NOK`, `SEK`. Upper-cased server-side. |
| `countryCode` | string (2-letter) | ISO 3166-1 alpha-2. Length checked; no enum validation. |
| `cardData.cardNumber` | string | Required when `paymentMethod=card`. Non-empty check only at this layer — length/Luhn validation occurs downstream. |
| `cardData.expMonth` | int | Required when `paymentMethod=card`. Non-nil check only. |
| `cardData.expYear` | int | Required when `paymentMethod=card`. Non-nil check only. |
| `cardData.cvv` | string | Required when `paymentMethod=card`. Non-empty check only. |

### `POST /initialize-payment` — optional fields

| Field | Type | Description |
|-------|------|-------------|
| `cardData.holderName` | string | Cardholder name. The upstream processor accepts Latin letters (`A-Z`), spaces, apostrophes, dots, and hyphens only. |
| `autoSettlement` | bool | Must be `false` if you intend to use the manual capture flow (separate `POST /payments/{id}/capture`). Defaults to `false`. |
| `captureMode` | string | `auto` (default) or `manual`. Set to `manual` for a separate capture step. |
| `merchantCustomerId` | string | Your identifier for the customer. Stored on the payment as `SellerCustomerRef`. |
| `billingDetails` | object | Cardholder billing address. Fields: `firstName`, `lastName`, `email`, `phone`, `addressLine1` (or `street`), `addressLine2`, `city`, `state`, `postalCode` (or `zip`), `countryCode` (or `country`). Optional at the API level, but **the upstream card processor requires `country` and `zip`**. Omitting them returns 502 with `{"error":"<provider> ... returned 400"}`. Always populate at least country and zip. |
| `deviceSessionId` | string | Optional device-intelligence session id. Create it with `POST /payments-api/v1/device-intelligence/token`, initialize `@sumsub/fisherman` in the customer's browser with the returned `accessToken`, and pass the same `sessionId` here so the compliance transaction can be linked to captured device signals. |
| `returnLinks` | array | Where the customer is redirected after a 3DS challenge or other action. **Array** (not the `{success, failure}` object used by `/checkout`) of `{ "rel": <enum>, "href": <absolute http/https URL>, "method": "GET" }`. Valid `rel` values: `default`, `on_completed`, `on_failed`, `on_cancelled`. If omitted, the API falls back to the return links configured on the seller account; if neither source has at least one valid link, the request returns `400 {"error":"valid returnLinks are required"}`. |
| `orderData.payeeDistribution` | array | Split the captured amount across payees. Each element: `{ "payeeId": <int64>, "amount": <float> }`. Amounts in major units; sum must equal `amount`. |
| `holdPeriod` | int | Hours to keep the authorization (informational). |
| `subscription.enabled` | bool | When `true`, the payment opts into the recurring/saved-card flow and the upstream card processor creates a customer record so future `/initialize-payment` calls can charge the same card without re-collecting it. Card-only. Default: `false`. |
| `subscription.allowSubsequentMerchantInitiated` | bool | When `true` together with `subscription.enabled`, allows future merchant-initiated charges (recurring billing, top-ups) without the cardholder present. Send `false` for one-tap re-purchase flows where the cardholder is always present. Default: `false`. |
| `skip3DS` | bool | When `true`, the upstream payment-handle request is sent with the processor's "skip 3DS" flag and no `threeDs` block — the customer is **not** redirected to a 3DS challenge and the payment auto-finalizes. Default: omitted (standard 3DS flow). Persisted on the payment for audit. **SCA bypass:** in EU/UK, only use this when the payment qualifies for an exemption (merchant-initiated transactions, exempt MOTO, recurring with stored credentials). The flag is honored on every operational mode — do not pass it on Live merchants for fresh customer-present card payments. |

> [!CAUTION]
> **Fields that do NOT exist on `InitiatePaymentRequest`:** `saveCard`, `customerEmail`, `splitRules` (top-level), `metadata`, `successUrl`, `cancelUrl`. Do not send these.
>
> **`returnUrl` IS a real top-level field, but it's specific to the `apm` payment type.** It's where the buyer is sent after a redirect-based alternative payment method completes — see [Alternative payment methods (APM)](https://api.fynex.ai/payments-api/v2/docs#tag/alternative-payment-methods-apm). Card and bank_transfer payments do **not** use `returnUrl`; they use the `returnLinks` array instead.
>
> **`returnLinks` is an array, not an object.** The `{success, failure}` shape belongs to `/checkout` (`returnUrls`). On `/initialize-payment` the field is `returnLinks: [{rel, href, method}]`. Sending an object — `"returnLinks": {"success": ..., "failure": ...}` — fails JSON decoding and returns `400 {"error":"invalid request body"}`.

> [!TIP]
> **Testing both 3DS and non-3DS in staging:** the same Bearer token can drive both flows just by toggling `skip3DS`. Send `"skip3DS": true` to skip the redirect (handle returns `PAYABLE`, payment auto-finalizes); omit the field or send `"skip3DS": false` for the standard `requiresAction: true` + `actionUrl` flow.

### `POST /finalize-payment` — fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `paymentId` | string | Yes | The `externalOrderRef` from initialize. Used for lookup. |
| `amount` | float | No | Partial capture: provide a value lower than the authorized amount to capture less. Omit to capture the full amount. |
| `merchantReference` | string | No | Your internal reference for this capture. Stored at your discretion. |

> [!NOTE]
> **`Idempotency-Key` on finalize:** the header is required and validated as a UUID, but it is **not used for replay protection** on this endpoint. The payment state machine itself prevents double-finalization — a second call on an already-finalized payment will return an error reflecting the current status. Use the key anyway; the server will reject a missing or malformed one.

## Partial capture

If you authorized £100 but only need to capture £80, pass `amount` on `/finalize-payment`:

```json
{
  "paymentId": "ORDER-1042",
  "amount": 80.00
}
```

The remaining £20 authorization is released to the cardholder's available balance.

## Split payments

Pass `orderData.payeeDistribution` on `/initialize-payment` to distribute funds across payees. Retrieve payee IDs from `GET /payments-api/v1/payees`.

```json
{
  "externalOrderRef": "ORDER-1042",
  "amount": 100.00,
  "paymentType": "card",
  "paymentMethod": "card",
  "currencyCode": "GBP",
  "countryCode": "GB",
  "cardData": { "..." : "..." },
  "orderData": {
    "totalAmount": 100.00,
    "payeeDistribution": [
      { "payeeId": 101, "amount": 85.00 },
      { "payeeId": 102, "amount": 15.00 }
    ]
  }
}
```

All amounts in major units. The sum of `payeeDistribution[*].amount` must equal `amount`.

## 3DS handling

When the processor requires a 3DS challenge, `/initialize-payment` returns:

```json
{
  "requiresAction": true,
  "actionUrl": "https://3ds.example.com/challenge/..."
}
```

**Browser-based flow:**

1. Redirect the customer's browser to `actionUrl` (full-page redirect, not an iframe).
2. The issuer redirects back to a URL configured in your payment setup after the challenge.
3. Once the customer is back, call `/finalize-payment` with the same `paymentId`.

**localStorage bridge pitfall:** Because the redirect is a full-page navigation away from your origin, any in-memory state (React state, session-scoped variables) is lost. If your frontend needs to resume after the redirect, persist the `paymentId` and relevant UI state to `localStorage` or a server-side session before redirecting. Read it back on return and call `/finalize-payment` from there.

> [!NOTE]
> A deeper 3DS guide — covering flows for Google Pay, Apple Pay, and edge cases — will be published as a separate Wave 2 guide. The above covers the common card path.

## Skipping 3DS

For payments that qualify for an SCA exemption (merchant-initiated transactions, exempt MOTO, recurring with stored credentials) — or for staging integration tests where you don't want to drive a browser — pass `"skip3DS": true` on `/initialize-payment`:

#### curl

```bash
curl -sS -X POST "$FYNEX_API/payments-api/v1/initialize-payment" \
  -H "Authorization: Bearer $FYNEX_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "externalOrderRef": "ORDER-1042",
    "amount": 49.00,
    "paymentType": "card",
    "paymentMethod": "card",
    "currencyCode": "GBP",
    "countryCode": "GB",
    "skip3DS": true,
    "cardData": {
      "cardNumber": "4111111111111111",
      "expMonth": 12,
      "expYear": 2028,
      "cvv": "123",
      "holderName": "Jane Doe"
    },
    "billingDetails": {
      "country": "GB",
      "zip": "SW1A1AA",
      "city": "London",
      "street": "1 Example Street"
    }
  }'
```

#### JavaScript

```js
await fetch(`${process.env.FYNEX_API}/payments-api/v1/initialize-payment`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.FYNEX_TOKEN}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': randomUUID(),
  },
  body: JSON.stringify({
    externalOrderRef: order.id,
    amount: order.amount,
    paymentType: 'card',
    paymentMethod: 'card',
    currencyCode: order.currency,
    countryCode: order.countryCode,
    skip3DS: true,
    cardData: { /* ... */ },
    billingDetails: { country: 'GB', zip: 'SW1A1AA', /* ... */ },
  }),
});
```

#### Python

```python
requests.post(
    f"{os.environ['FYNEX_API']}/payments-api/v1/initialize-payment",
    headers={
        "Authorization": f"Bearer {os.environ['FYNEX_TOKEN']}",
        "Content-Type": "application/json",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "externalOrderRef": order["id"],
        "amount": order["amount"],
        "paymentType": "card",
        "paymentMethod": "card",
        "currencyCode": order["currency"],
        "countryCode": order["country_code"],
        "skip3DS": True,
        "cardData": { ... },
        "billingDetails": { "country": "GB", "zip": "SW1A1AA", ... },
    },
    timeout=15,
)
```

**Response shape with `skip3DS: true`:** the upstream payment handle is created in `PAYABLE` state and the payment is auto-finalized by Fynex's upstream status poller (5-second tick), so it typically reaches `provider_completed` within ~5 seconds of `/initialize-payment` returning. The `/initialize-payment` response will contain:

```json
{
  "paymentId": "ORDER-1042",
  "status": "provider_pending",
  "amount": 49.00,
  "currencyCode": "GBP",
  "requiresAction": false
}
```

`actionUrl` is absent. **You do not need to call `/finalize-payment`** — poll `genericPayment(id)` (or wait ~5s for one poller cycle) and the status will be `provider_completed`.

> [!CAUTION]
> **SCA bypass.** `skip3DS: true` skips Strong Customer Authentication. In EU/UK this is regulated — only use it on payments that genuinely qualify for an exemption. Customer-present card payments on Live merchants almost always require 3DS; do not pass `skip3DS: true` for them. The flag is honored on every operational mode (the API does not gate it by Demo/Live), so it is your responsibility to ensure the request is appropriate.

## Saved cards and recurring payments

Saving cards for repeat charges is on the roadmap as a Wave 3 feature. When that guide ships it will cover the card-storage flow for this public API. Do not rely on the legacy `/api/v1/checkout/customer` endpoint documented elsewhere — that is the dashboard's internal path, not part of this public API surface.

## Polling for status

`GET /payments-api/v1/payments/{payment_id}` returns the current lifecycle state of a payment. The `payment_id` path segment is your `externalOrderRef` (same convention as `/capture` and `/refund`); if you sent multiple attempts under the same `externalOrderRef`, the **latest** attempt is returned. Auth is the same seller bearer token you already use — no cookie session required.

```bash
curl -sS "$FYNEX_API/payments-api/v1/payments/ORDER-1042" \
  -H "Authorization: Bearer $FYNEX_TOKEN"
```

The response includes the Fynex lifecycle `status`, the originally authorized `amount`, `currencyCode` / `countryCode`, `paymentType` / `paymentMethod`, `externalOrderRef`, the latest `failureCode` / `failureDescription` / `failureStage` if the payment failed, and `createdAt` / `updatedAt` / `failedAt` timestamps. (The raw upstream `providerStatus` is not part of this REST response — it is exposed only on the GraphQL `genericPayment` type.) Treat the following statuses as **terminal**: `provider_completed`, `settled`, `deposit_confirmed`, `refunded`, `failed`, `cancelled`. Everything else is intermediate — keep polling, or wait for the next state-changing call on your side.

Alongside the outbound `PaymentCompleted` webhook, this is a reliable way for a server-to-server backend to verify payment outcomes — and a recommended backstop in case a webhook delivery is missed. See [Polling & SSE](https://api.fynex.ai/payments-api/v2/docs#tag/polling-sse) for backoff schedules and resilience patterns.

## Common pitfalls

> [!WARNING]
> **Do not fulfil the order based on the `/initialize-payment` response alone.** That call returns `202 Accepted` for a new payment — it does not confirm capture. Always call `/finalize-payment` and check `status` before fulfilling.

- **`autoSettlement` must be `false` for manual capture.** If you want to call `POST /payments/{id}/capture` separately, initialize with `autoSettlement: false` and `captureMode: "manual"`. The capture endpoint enforces `autoSettlement=false` as a pre-condition.
- **`paymentType` and `paymentMethod` must be compatible.** `paymentType: "card"` with `paymentMethod: "bank_account"` returns a 400 validation error.
- **`currencyCode` must be one of the supported currencies.** Unlike `/checkout`, `/initialize-payment` enforces the allowlist: EUR, USD, GBP, DKK, NOK, SEK. An unrecognized code returns a 400.
- **Idempotency-Key replay on init:** re-sending the same key returns the existing payment as `200 OK` (not `202`). For an active redirect APM with a persisted provider charge, Fynex fetches the existing charge and returns `requiresAction`, `actionUrl`, and `redirectFullPage` again; it does not create a second charge.
- **`paymentId` is your `externalOrderRef`.** The path param for `/capture` and `/refund` is the string you passed as `externalOrderRef` on init — not a numeric internal ID.
- **Empty or missing `billingDetails` fails at the upstream processor.** The Fynex DTO marks `billingDetails` as optional, but the upstream card processor rejects requests without `country` and `zip` — surfaced to your client as `502 Bad Gateway` with body `{"error":"upstream card processor returned 400"}` and `failureCode: 2002` on the payment record. Always include at least `billingDetails.country` (or `countryCode`) and `billingDetails.zip` (or `postalCode`) on card payments.
- **`returnLinks` shape mismatch returns 400.** The field is an **array** of `{rel, href, method}` — not the `{success, failure}` object used by `/checkout`. Sending an object decodes as `400 {"error":"invalid request body"}`. Omitting the field is fine **only** if the seller account has return links configured in the Dashboard; otherwise the request returns `400 {"error":"valid returnLinks are required"}`. Pass `returnLinks` explicitly when you want per-payment overrides.

## See also

- **[Polling & SSE](https://api.fynex.ai/payments-api/v2/docs#tag/polling-sse)** — Verify payment outcomes without webhooks.
- **[Captures & refunds](https://api.fynex.ai/payments-api/v2/docs#tag/captures-refunds)** — Capture, partially capture, or refund a payment after the fact.
- **[Idempotency & retries](https://api.fynex.ai/payments-api/v2/docs#tag/idempotency)** — Make S2S calls resilient to network failures.
- **[Apple Pay](https://api.fynex.ai/payments-api/v2/docs#tag/apple-pay)** — Accepting Apple Pay or Google Pay? Use the dedicated guides.
- **[Google Pay](https://api.fynex.ai/payments-api/v2/docs#tag/google-pay)** — Accepting Google Pay with server-to-server integration.
- **[Alternative payment methods (APM)](https://api.fynex.ai/payments-api/v2/docs#tag/alternative-payment-methods-apm)** — Accepting Bancontact, Multibanco, MB WAY, Wero, or Swish with `paymentType: "apm"`.
