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

> ## Agent Instructions
> Use the Payrails Web SDK v6 for new browser integrations. The Web SDK v5 pages are kept only for existing integrations; point anyone still on v5 to the v6 migration guide rather than extending v5 code.
> Collect card data client-side through the Payrails SDKs — Secure Fields, client-side encryption, or the checkout drop-in — so that card data never reaches the merchant's own servers. Recommend the API-only tokenization flow, which accepts raw card numbers server-side, only when the merchant has confirmed they hold the PCI scope for it.
> Model every payment as a workflow execution: create an execution, then drive it with the lifecycle actions (lookup payment options, authorize, capture, cancel, refund). Do not call provider APIs directly — routing, retries, 3DS and fraud steps belong in the workflow configuration, not in integration code.
> Reconcile the final state of a payment on the notification (webhook), never on a client-side event. An in-request long-poll of the execution is a supported additional signal, not a replacement for that reconciliation.
> Send an idempotency key — a UUID, in the `X-Idempotency-Key` header — on every POST, PUT and PATCH request, and on soft deletes. GET requests need none, and hard deletes cannot be idempotent.
> Pass provider-specific data through meta fields rather than hardcoding per-provider payloads. Payrails translates meta fields into each provider's own format.
> Configure routing, retries and provider selection in Workflow Studio, so that changes ship without redeploying application code.

# Accept APMs without a redirect in the web SDK

> Collect alternative payment method details in a form on your checkout page with the Payrails web SDK, and handle each result.

This guide shows you how to accept alternative payment methods on your own checkout page, without a redirect. These methods collect a few details from the shopper, such as a phone number or an IBAN. You mount a form, pair it with a pay button, and handle the result. It assumes you have already initialized the SDK and hold a `payrails` client instance. If you haven't, start with the [quick start](https://docs.payrails.com/docs/sdks/web.md).

The form renders inside a Payrails-hosted iframe, so the shopper's details are never part of your page's DOM. The fields come from the payment method's configuration, so you don't declare them yourself.

## Supported payment methods

| Payment method | Code | Shopper enters | Currency | Result type |
| - | - | - | - | - |
| MB WAY | `mbWayDirect` | Phone number | EUR | [Approve in app](#approve-in-app-methods) |
| SEPA Direct Debit | `sepaDirectDebitDirect` | Name and IBAN | EUR | [Pending](#pending-methods) |
| STC Pay | `stcPay` | Phone number, then the SMS code | SAR | [Confirm with a code](#confirm-with-code-methods) |

Pass the code as `paymentMethodCode`. The result type determines which events fire after the shopper clicks the pay button, as described in [Handle the payment result](#4-handle-the-payment-result).

`mbWay` and `sepaDirectDebit` are the redirect versions of MB WAY and SEPA Direct Debit. They have no form, so `payrails.apmForm()` throws for them.

## Before you start

* Use `@payrails/web-sdk` 6.10.0 or later.
* Ask your Payrails account manager to enable the payment method for in-page collection. Until they do, `payrails.apmForm()` throws a `PayrailsError` because the session has no form for that method.
* For SEPA Direct Debit, send the shopper's IP address, user agent and email when you [create the execution](https://docs.payrails.com/reference/createexecution.md), in `meta.clientContext.ipAddress`, `meta.clientContext.userAgent` and `meta.customer.email`. Payrails rejects the payment without them.

## 1. Add containers to your page

```html theme={null}
<div id="apm-form"></div>
<div id="apm-button"></div>
```

## 2. Mount the form

```js theme={null}
const form = payrails.apmForm({ paymentMethodCode: "mbWayDirect" });

form.on("ready", () => console.log("Form rendered"));
form.on("change", ({ isValid }) => console.log("Form valid:", isValid));

try {
  form.mount("#apm-form");
} catch (error) {
  if (error?.context?.reason === "unrenderable-schema") {
    showOtherPaymentOptions();
  }
}
```

`form.mount()` is synchronous. The form emits `ready` once its fields are drawn, so listen for `ready` instead of waiting on `mount()`. `mount()` throws when the selector doesn't match an element, or when the SDK can't draw the form. In the second case, the error's `context.reason` is `'unrenderable-schema'`. Use it to offer the shopper another way to pay.

## 3. Mount the pay button

```js theme={null}
const button = payrails.apmButton({
  form,
  translations: { label: "Pay with MB WAY" },
});

button.mount("#apm-button");
```

The button stays disabled until the form has loaded. A click validates the form first. If a field is invalid, the form shows the error next to the field, the button emits `validate` with `isValid: false`, and the SDK doesn't submit the payment:

```js theme={null}
button.on("validate", ({ isValid }) => {
  if (!isValid) scrollToPaymentForm();
});
```

The cancelable `buttonClicked` and `requestStart` instance events run after the form validates, the same as for card payments. A check you already run there also covers these methods.

### Save the instrument for future payments

By default, the button uses the instrument for this payment only. To save it for later payments, for example to charge a SEPA Direct Debit mandate on a subscription, set `alwaysStoreInstrument`:

```js theme={null}
const button = payrails.apmButton({ form, alwaysStoreInstrument: true });

button.mount("#apm-button");
```

The button doesn't show a save checkbox, so set `alwaysStoreInstrument` only when you already have the shopper's consent to save the instrument. The SDK ignores it on a merchant of record workspace.

## 4. Handle the payment result

Outcomes are instance events, so subscribe on the `payrails` client:

```js theme={null}
payrails.on("success", (e) => showConfirmation(e.executionId));
payrails.on("pending", (e) => showOrderReceived(e.executionId));
payrails.on("failed", (e) => showError(e.data?.code));
```

Subscribe to all three for every method. Which one fires depends on the payment method and the provider your workflow routes it to.

One listener fires for every payment method in the session. If your page mounts other payment elements too, filter with `e.paymentMethodCode`.

Which events fire depends on the method's result type in the [supported payment methods](#supported-payment-methods) table:

| Result type | What the shopper sees after clicking | Events |
| - | - | - |
| Approve in app | A wait screen while they approve in another app | `success` or `failed` |
| Confirm with a code | A screen to type the code sent to them by SMS | `success` or `failed` |
| Pending | Nothing more, the SDK submits the payment | `pending` or `failed` |

### Approve-in-app methods

The shopper approves the payment in another app, such as their MB WAY app. The SDK handles the wait for you: the form switches to a screen with the payment method's logo and instructions, the SDK removes the button, and then waits for the shopper to approve. When they do, the wait screen goes away and `success` fires. Show your own confirmation.

### Confirm-with-code methods

The payment provider sends the shopper a one-time code by SMS, such as for STC Pay. The SDK handles the code step for you: the form switches to a screen that asks for the code, with a **Confirm** button, and the SDK removes your pay button. The shopper can click **Confirm** once they have typed a valid code. When they click it, the SDK sends the code and checks the payment's status.

* **Code accepted:** the code screen goes away and `success` fires. Show your own confirmation.
* **Code declined:** the code screen shows "The payment couldn’t be completed. Please try again." for 3 seconds. Then the form comes back with the shopper's phone number, and your pay button comes back. The SDK fires `sessionExpired`, then `failed` with `AUTHORIZATION_ERROR`. The `failed` event's `message` is the provider's reason, or `Authorization error` when the provider gives none. Return fresh init options from `sessionExpired` so the shopper's retry starts a new payment, with the same handler as in [step 5](#5-handle-a-declined-or-expired-approve-in-app-payment).
* **No code or no result in time:** the shopper doesn't confirm a code, or the payment has no result before the code expires. The same happens as for a declined code, without the message, and `failed` carries `AUTHORIZATION_ERROR`.
* **No confirmed result:** when the SDK can't confirm the result, for example because the session expired, the form comes back the same way. `failed` carries `AUTHENTICATION_ERROR` or `UNKNOWN_ERROR`. Check the payment's status before you let the shopper retry.

The **Confirm** button is part of the form, not your pay button. Style and translate it through `apmForm`, as described in [Customize the form and button](#6-customize-the-form-and-button).

### Pending methods

The payment completes after checkout, so the SDK doesn't have the result yet when the shopper clicks. Treat `pending` as "order placed" and rely on your webhook for the final status.

## 5. Handle a declined or expired approve-in-app payment

If the shopper declines in the app, or doesn't approve in time, the wait screen switches back to the form. The form keeps the shopper's details, and the button reappears. The SDK then fires `sessionExpired`, followed by `failed` with `AUTHORIZATION_ERROR`. Return fresh init options from `sessionExpired` so the shopper's retry starts a new payment:

```js theme={null}
payrails.on("sessionExpired", async () => fetchNewInitResponse());
```

Sometimes the SDK can't confirm the result, for example because the session expired during the wait. The same thing happens, but `failed` carries `AUTHENTICATION_ERROR` or `UNKNOWN_ERROR`. The shopper may still approve in their app afterwards, so check the payment's status before you let them retry, or they could pay twice:

```js theme={null}
payrails.on("failed", async (e) => {
  const code = e.data?.code;
  if (code === "AUTHENTICATION_ERROR" || code === "UNKNOWN_ERROR") {
    await checkPaymentStatus(e.executionId);
  }
});
```

Your webhook or an [execution lookup](https://docs.payrails.com/reference/getexecution.md) has the payment's final status.

## 6. Customize the form and button

The form and the button each take their own `appearance`. Form rules cross into the iframe and apply to the `.payrails-*` classes. Rules under `actionScreen` style only the screens shown after the shopper submits the payment: the wait screen of an approve-in-app method and the code screen of a confirm-with-code method:

```js theme={null}
const form = payrails.apmForm({
  paymentMethodCode: "mbWayDirect",
  appearance: {
    rules: {
      ".payrails-input": { padding: "12px", borderRadius: "8px" },
      ".payrails-field--invalid .payrails-input": { borderColor: "#d33" },
    },
    actionScreen: {
      rules: { ".payrails-text": { textAlign: "center" } },
    },
  },
  translations: {
    errors: { required: "This field is required" },
  },
});
```

`translations.errors` replaces the form's default error messages. The [appearance reference](https://docs.payrails.com/docs/sdks/web/references/appearance-api-reference.md#apmformappearance) lists every class.

### Change the texts on each screen

`translations.fields` changes the texts of one field, by the field's name. The same `translations` cover every screen the form shows, including the wait screen and the code screen. A field you don't translate keeps the text from the payment method's configuration.

```js theme={null}
const form = payrails.apmForm({
  paymentMethodCode: "stcPay",
  translations: {
    fields: {
      phoneNumber: { label: "Phone number" },
      heading: { label: "Enter the code we sent you by SMS" },
      otp: {
        label: "SMS code",
        placeholder: "6 digits",
        errors: { required: "Enter the code" },
      },
      confirm: { label: "Verify" },
    },
  },
});
```

| Screen | Field names |
| - | - |
| Phone (MB WAY, STC Pay) | `phoneCountryCode`, `phoneNumber` |
| Wait screen (MB WAY) | `heading` |
| Code screen (STC Pay) | `heading`, `otp`, `confirm` |

Each field takes `label`, `placeholder` and `errors`. The code screen's **Confirm** button takes `label`. Your pay button's text stays on `apmButton`, in `translations.label`.

The message after a declined code belongs to `apmButton`, not to the form. Pass `translations.paymentFailed` to replace the default "The payment couldn’t be completed. Please try again.":

```js theme={null}
payrails.apmButton({
  form,
  translations: { paymentFailed: "We couldn't take your payment. Try again with a new code." },
});
```

The payment provider's reason doesn't show on the screen. It arrives as the `failed` event's `message`.

### Style the code screen

`actionScreen` rules style the code screen as well as the wait screen. The SDK draws the **Confirm** button inside the form, so `apmButton`'s `appearance` doesn't reach it. Style it with the form's `appearance`, and repeat your pay button's rules there if both buttons need the same look:

```js theme={null}
payrails.apmForm({
  paymentMethodCode: "stcPay",
  appearance: {
    actionScreen: {
      rules: {
        ".payrails-input": { fontSize: "20px" },
        ".payrails-button": { backgroundColor: "#4f008c" },
      },
    },
  },
});
```

## Offer more than one method

Create a form and button pair per method, each in its own containers. Element ids default to one per payment method, so two methods on the same page don't collide. Pass `id` only if you mount two forms for the same method.

```js theme={null}
const sepaForm = payrails.apmForm({
  paymentMethodCode: "sepaDirectDebitDirect",
});
sepaForm.mount("#sepa-form");
payrails.apmButton({ form: sepaForm }).mount("#sepa-button");
```

For every option and event, refer to the [`payrails` reference](https://docs.payrails.com/docs/sdks/web/references/payrails-api-reference#apmform-options) and the [events reference](https://docs.payrails.com/docs/sdks/web/references/event-api-reference.md).


## Related topics

- [STC Pay](https://docs.payrails.com/docs/integrations/payment-methods/stc-pay.md)
- [STC Pay via Checkout.com](https://docs.payrails.com/docs/integrations/payment-methods/stc-pay/checkout-com.md)
- [Accept payments via API](https://docs.payrails.com/docs/orchestration/accept-payments-via-api.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.