If your frontend can host the Drop-in
SDK — the SDK that
drives the full payment lifecycle from the client — choose that pattern
instead. Use the Drop-in SDK when your frontend owns the customer session and
Payrails should drive the full payment lifecycle. Use the server-to-server
pattern when your backend owns order creation, fraud checks, and customer
messaging, or when your tech stack cannot host a single-page application.
When to choose this pattern
Choose the server-to-server architecture when one or more of the following holds:- You use an e-commerce engine that relies on a synchronous auth decision. Magento, Salesforce Commerce Cloud.
- You require control over the decision making on when to do the authorization based on backend decisions, e.g. running fraud checks, inventory checks etc. Payrails fits into that flow rather than taking ownership of it.
- You need explicit control over the 3-D Secure challenge UX, such as iframe or modal rendering on your existing checkout page.
Architecture
This diagram shows the systems involved and the direction of data flow between them. The customer browser hosts only the Payrails Web SDK for tokenization; every other call from the browser routes through your backend. Read the diagram in two passes. The three solid arrows trace the main request path. The customer places an order against your backend. Your backend creates the execution and authorizes it against Payrails. Payrails routes the authorization through one or more Payment Service Providers (PSPs). The three dashed arrows are the exceptions to that path. The browser tokenizes card data directly with Payrails through the Vault so the PAN never reaches your servers. The PSP routes a 3-D Secure challenge to the customer’s browser when the issuer requires one. Payrails sends anexecutionActionCompleted webhook to your backend once the authorization resolves.
The Payrails platform handles four jobs in this architecture: card tokenization through the Vault, workflow orchestration including the PSP cascade, 3-D Secure handoff, and webhook delivery. The PSP cascade is the workflow-configured chain of providers Payrails retries the authorize against on a decline. Your backend orchestrates everything else.
Frictionless authorization flow
A frictionless authorization completes without a 3-D Secure challenge. The customer never touches a Payrails-hosted URL. This sequence diagram shows the full path from card entry to order confirmation.3-D Secure authorization flow
The authorize call itself returns an acknowledgment. Your backend learns that a challenge is required via theexecutionActionPending webhook or by long-polling GET /executions/{id}. When the execution moves to status authorizePending, the state carries actionRequired: "3ds" and a requiredAction object whose href is the hosted challenge URL. Your backend returns the URL to the storefront, which renders the challenge in your preferred container — iframe, modal, new tab, or full-page redirect. Payrails returns the customer to your returnInfo.success URL once the challenge resolves.
If the primary PSP declines after the challenge resolves, Payrails passes the cryptogram to the next PSP in the cascade. The challenge runs once per transaction, not once per PSP.
Frontend implementation
The frontend mounts a Payrails Web SDK element, collects a payment method, and hands apaymentInstrumentId to your backend. The element handles every PCI-scoped field. The PAN, the CVV, and the BIN never reach your servers. Render the 3-D Secure container separately; the same container is shared across every element.
Five elements support this server-to-server pattern. All five end the frontend flow with a paymentInstrumentId your backend passes to POST /executions/{id}/authorize:
- Secure Fields — three separate iframes (number, expiry, CVV) for full layout control.
- Card Form Element — a pre-composed card form. Faster to integrate, single mount point.
- Card List Element — renders the customer’s saved cards for repeat purchases.
- Apple Pay Element — the Apple Pay button for wallet checkout on Safari and iOS.
- Google Pay Element — the Google Pay button for wallet checkout.
client/init.type setting controls which elements the SDK exposes. Use "secureFields" for Secure Fields or Card Form. Use "tokenization" for Card List and the wallet buttons. Do not use "dropIn" for this pattern — that mode lets the SDK drive authorize and bypasses your backend.
Post to your backend
Every element ends the same way: hand thepaymentInstrumentId and paymentMethodCode to your backend. This shared helper is referenced by every tab in the next section:
client/init, call Payrails.init, then mount the chosen element. The SDK returns a SaveInstrumentResponse from direct tokenize calls (Secure Fields, Card Form) and a StoredPaymentInstrument from the Card List onCardChange callback. The id field on either is the value to pass as paymentInstrumentId.
Mount and tokenize
- Secure Fields
- Card Form
- Card List
- Apple Pay
- Google Pay
Mount three iframes (one per PCI-scoped field), call
cardContainer.tokenize() on form submission, and hand the resulting id to your backend.storeInstrument: false keeps the instrument ephemeral. Pass true when the customer has opted in to saving the card on the profile. Requires client/init.type: "secureFields".Render the 3-D Secure container
The 3-D Secure container is the same across every element. When thepostToBackend response carries actionRequired: "3ds" and a redirectUrl, render the URL in your chosen container — iframe, modal, new tab, or full-page redirect. The customer completes the challenge. Payrails returns them to your returnInfo.success URL. The return page posts back to the opener so the checkout page can route to the confirmation step.
Backend implementation
The backend authenticates with Payrails, creates the execution, calls authorize against the tokenizedpaymentInstrumentId, and returns a clean two-case contract to the storefront. This code uses Python and the requests library; the same shape applies to any HTTP client.
Authentication
The Payrails OAuth token endpoint returns a token valid for one hour. Cache the token in memory and refresh on expiry.- Python
- Java
- TypeScript
- C#
- Go
Initialize the SDK session
This call returns the SDK configuration the frontend uses to boot. The storefront’s/payments/init endpoint on your backend proxies the call so the OAuth token stays on the server. The body passes workflowCode, holderReference, workspaceId, and the type that matches the element your storefront is about to mount. Use "secureFields" for Secure Fields and Card Form. Use "tokenization" for Card List, Apple Pay, and Google Pay.
The response is the payload the frontend hands to Payrails.init(...) — return it from /payments/init verbatim.
- Python
- Java
- TypeScript
- C#
- Go
Create the execution
The first call creates the execution and locks the metadata. The body carriesmerchantReference, holderReference, workspaceId, and the meta block. The body does not carry amount, paymentComposition, or initialActions — those live on the authorize call.
- Python
- Java
- TypeScript
- C#
- Go
Authorize the execution
The second call dispatches the authorize against the tokenizedpaymentInstrumentId. The HTTP response is an acknowledgment; the actual outcome (frictionless approval, 3-D Secure required, or decline) arrives via long-poll or webhook.
- Python
- Java
- TypeScript
- C#
- Go
Storefront-facing endpoint
The endpoint the storefront calls runs the two Payrails calls, long-polls for the result, and returns a clean two-case contract. OnactionRequired === "3ds", the storefront opens the 3-D Secure container; otherwise it renders the order confirmation page.
The authorize call returns an acknowledgment; the actual outcome (frictionless approval, 3-D Secure required, or decline) arrives via long-poll or webhook. This endpoint uses the long-poll inline so the storefront receives a single resolved response. The webhook still fires in parallel and remains the source of truth for order reconciliation; the Webhook reconciliation section covers the handler.
- Python
- Java
- TypeScript
- C#
- Go
3-D Secure handling
3-D Secure runs only when the issuer requires a challenge. Your backend learns of this through one of two channels — both are valid:- The
executionActionPendingwebhook. Preferred at scale. Requires a customnotifystep on the workflow paused branch witheventType: "executionActionPending". The defaultpayment-acceptancetemplate does not emit a webhook on the 3-D Secure pause. - Long-polling
GET /executions/{id}. Available without workflow configuration. UsewaitWhile[status]=["created","authorizeRequested"]to block until the execution leaves the pre-authorize states.
actionRequired or requiredAction.href — those fields surface on the polled execution state (or the webhook body), not necessarily on the authorize response.
Once your backend has the resolved state, the canonical field to read is requiredAction.href. On a polled execution, the same URL is mirrored as links.redirect. The legacy field links["3ds"] only appears for the deprecated 3DS action type and is often empty; read requiredAction.href instead.
The choice of rendering container — iframe, modal, new tab, or full-page redirect — is yours. The only requirements: the container loads external URLs, and returnInfo.success is HTTPS and on your own domain.
On the frictionless path, the execution moves straight to status authorized, actionRequired stays empty, and the customer never touches a Payrails-hosted URL.
Webhook reconciliation
Your backend confirms every order on theexecutionActionCompleted webhook, not on the storefront event. The storefront’s state is optimistic; the webhook is the source of truth.
Every webhook delivery carries an HMAC signature header. Verify the signature before processing the body; the Receive notifications page documents the algorithm and header name.
- Python
- Java
- TypeScript
- C#
- Go
200 as soon as the handler records the event. Anything that can fail — order-database updates, downstream messaging, analytics — runs asynchronously after the response returns. A slow handler causes Payrails to retry the delivery, which forces your idempotency logic to do extra work.
Implement verify_payrails_signature per the Receive notifications documentation.
Long-poll patterns
A long-poll onGET /executions/{id}?waitWhile[status]=... blocks until the execution leaves the listed pre-terminal states. Two cases use it in this architecture:
- Inside the place-order request, to wait for the 3-D Secure decision. Pre-terminal states:
created,authorizeRequested. The poll resolves either toauthorized(frictionless) or toauthorizePending(3-D Secure required,requiredAction.hrefpopulated). - On the payment-return page after the challenge resolves, to wait for the final outcome. Pre-terminal states:
authorizePending. The poll resolves toauthorizedor to a failure state.
- Python
- Java
- TypeScript
- C#
- Go
Recommendations
Idempotency keys
Every write call to Payrails carries anx-idempotency-key header. The value is a fresh UUIDv4. Payrails dedupes retries against the same key, so a network-retry of the same logical call is safe.
The idempotency key must be a valid UUID. Composite strings such as
order-123_attempt-1 are rejected by the Payrails API as malformed. Allocate
a fresh UUIDv4 per logical operation and persist it alongside the operation so
retries reuse the same key.Metadata immutability
Themeta block on the create-execution call locks once the authorize action completes. Anything that needs to surface on dashboards, webhooks, settlement reports, or PSP portals must be on the create-execution body. Treat the second call (authorize) as payment-specific only.
The meta.order.reference and meta.customer.reference fields are not updatable after authorize. If your system assigns the internal order identifier after the Payrails call, keep the join in your own database. Example: the cart identifier is known at checkout time, while the order identifier becomes available only after authorization.
Frictionless authorizations show no Payrails UX
On the frictionless path, the customer never touches a Payrails-hosted URL. The authorize response carries an emptyactionRequired and your backend renders the order confirmation page directly. The 3-D Secure container only opens when actionRequired === "3ds".
The 3-D Secure handling section explains the reason to read requiredAction.href rather than links.consumerWait. Payrails populates consumerWait on every authorize response for use by SDK-driven flows; requiredAction.href exists only when a challenge is required, so frictionless paths skip Payrails-hosted UX entirely.
Next steps
- Review the Secure Fields reference for the per-field event surface (
READY,CHANGE,FOCUS,BLUR) and the styling options available on the iframes. - Read Receive notifications for the canonical HMAC signing format and the full event catalog.
- See Payment status for the meaning of every status value your reconciliation logic must handle, including
Unknown(which routes to manual reconciliation, not automatic retry). - Use Test payments to exercise both the frictionless and 3-D Secure paths during testing, including the Test PSP for simulating declines and challenges.