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

# Embed Adyen Drop-in in your checkout

> Accept payments on your storefront with Firmhouse carts and Adyen Drop-in, including retries, cart changes, and late-payment recovery.

Use Adyen Drop-in to keep the payment step inside your own storefront. You build the cart, customer details, and confirmation pages. Adyen renders the payment form, and Firmhouse creates payment sessions and processes payment notifications, orders, and subscriptions.

The hosted checkout remains available if you want Firmhouse to provide the payment page. Embedded checkout gives you control over the surrounding layout and journey, with Adyen's supported styling and payment-method configuration for the form. Bank authentication and some payment methods can still redirect shoppers away and back.

> [!NOTE]
>
> Embedded Adyen checkout must be enabled for your project by Firmhouse. Contact us before implementing it. This guide covers initial checkout payments, not recurring invoice payments or changing an existing customer's payment method.

## Before you start

* Follow [Create a storefront app](https://docs.firmhouse.com/developers/guides/creating-a-storefront-app) to create a cart, add products or a plan, and collect customer details.
* [Connect Adyen](https://docs.firmhouse.com/configure/payment-providers/using-adyen-as-payment-provider.md), including the client key and a working, HMAC-verified webhook to Firmhouse. Configure test and live accounts separately.
* In your own Adyen Customer Area, add your storefront's exact origin to the API credential's allowed origins. A local development origin and a staging domain need their own entries. Configuring the Firmhouse hosted checkout origin does not authorize your storefront.
* Configure payment methods and country-specific availability in Adyen. In Firmhouse, leave all payment methods unselected to use Adyen's available methods supported by Sessions, or select methods to narrow that result. Complete wallet requirements for your storefront domain, including Apple Pay domain registration and applicable Google Pay configuration. These settings belong to your Adyen account; Firmhouse cannot configure your domains through the checkout API.
* Install Adyen Web and its stylesheet using [Adyen's Sessions integration guide](https://docs.adyen.com/online-payments/build-your-integration/sessions-flow?integration=Drop-in\&platform=Web).

Supported session methods are cards, iDEAL, Bancontact, Klarna Pay Now, Klarna Pay Later (including B2B), Apple Pay, Google Pay, PayPal, and Pay by Bank. Actual availability depends on the enabled account methods, country, currency, device, and Adyen requirements. SEPA Direct Debit and TWINT are not included in this embedded session flow. Confirm recurring-payment eligibility with Firmhouse for the methods you intend to offer; a method appearing in Drop-in does not by itself establish that it can be used for subsequent subscription charges.

Payment-method availability is checked when a new session is created. Existing sessions keep their methods until replaced or expired, so test configuration changes with a fresh checkout.

## Authenticate the cart

Send GraphQL requests to `https://portal.firmhouse.com/graphql` with:

```http theme={null}
Content-Type: application/json
X-Project-Access-Token: YOUR_STOREFRONT_ACCESS_TOKEN
X-Subscription-Token: CURRENT_CART_TOKEN
```

Use a project token with **Storefront** access, together with the subscription token returned when creating the cart. The session, payment-status, and abandonment operations require this combination, and the payment must belong to that cart and project. A payment token identifies an attempt; it does not replace the cart's authentication.

Never put a project Read or Write access token, an Adyen API key, or a webhook HMAC key in browser code. Keep the cart token scoped to that shopper's checkout and out of analytics or shared logs. Adyen's client key is intended for browser use.

The examples below use GraphQL directly, so they do not depend on a particular SDK release. Check both top-level GraphQL `errors` and mutation payload `errors` before using a response. Mutation payload errors provide `attribute` and `message`. Top-level GraphQL errors provide `message` and may include `path` or `extensions`; do not assume they have an `attribute`. Do not depend on a dedicated checkout error code.

## 1. Finalise the cart

After saving the shopper's details and cart contents, call:

```graphql theme={null}
mutation FinaliseCheckout($input: CreateSubscriptionFromCartInput!) {
  createSubscriptionFromCart(input: $input) {
    payment { token }
    returnUrl
    errors { attribute message }
  }
}
```

```json theme={null}
{
  "input": {
    "paymentPageUrl": "https://shop.example.com/checkout",
    "returnUrl": "https://shop.example.com/checkout/confirmed"
  }
}
```

These URLs have different purposes:

| URL | Purpose |
| - | - |
| `paymentPageUrl` | Your checkout page. Adyen sends shoppers back here after bank or 3D Secure authentication. It must restore the original cart and payment attempt. |
| `returnUrl` | Your order confirmation page, used after Firmhouse confirms successful payment. Omitting it uses Firmhouse's confirmation page. |

Retain the returned `payment.token` with the current cart. If finalisation succeeds without a payment, no payment is required: follow the returned `returnUrl` instead of creating an Adyen session.

## 2. Create or reuse the payment session

```graphql theme={null}
mutation CheckoutSession($paymentToken: ID!) {
  createAdyenPaymentSession(input: { paymentToken: $paymentToken }) {
    session {
      sessionId
      sessionData
      expiresAt
      clientKey
      environment
      locale
      amountCents
      currency
      countryCode
      cardConfiguration { hasHolderName holderNameRequired }
      googlePayConfiguration {
        authJwt
        merchantOrigin
        merchantName
        merchantId
        gatewayMerchantId
      }
      paypalConfiguration { vault }
    }
    errors { attribute message }
  }
}
```

Pass the payment token from finalisation. Treat `sessionData` as opaque; Firmhouse supplies the amount, currency, payment reference, and line items. Configure Adyen Web with the returned client key, environment, locale, country, `amount: { value: amountCents, currency }`, and `session: { id: sessionId, sessionData }`. Pass `amountCents` directly in minor units, without dividing by 100, so Drop-in displays the correct payment total. Map `cardConfiguration` to Drop-in's card configuration, `googlePayConfiguration` (when present) to Google Pay's `configuration`, and `paypalConfiguration` to PayPal's configuration. Keep these settings aligned with Firmhouse rather than hardcoding different cardholder-name or payment-storage behavior.

You can request the session again when reopening the normal payment page for the same eligible, unchanged payment. Firmhouse reuses an unexpired session and creates a fresh session when the stored session expires. Use `expiresAt`; the default Adyen lifetime is about one hour from creation, not from the shopper's most recent page load. Session expiry alone does not currently move the Firmhouse payment to `EXPIRED`.

Mount Drop-in once per active session. Avoid duplicate mounts from frontend renders. Do not also open the hosted `paymentUrl` for that payment: embedded Sessions and hosted payment processing are mutually exclusive once a flow has started. Handle a flow-conflict error rather than attempting to bypass it with another cart.

## 3. Handle normal entry and redirect returns

Your checkout page has two entry modes:

* **Normal entry:** restore the cart and current payment token, check its status if an attempt already exists, and create or reuse a session when payment is still needed.
* **Adyen redirect return:** when the URL contains `sessionId` and `redirectResult`, restore the original attempt and let Adyen Web complete that session. Do not finalise again or create a replacement session before handling the redirect.

Use Adyen Web's redirect handling for your installed version: initialize it with the returned `sessionId` and submit the `redirectResult` through `submitDetails`. See [Adyen's redirect instructions](https://docs.adyen.com/online-payments/build-your-integration/sessions-flow?integration=Drop-in\&platform=Web). Afterwards, check Firmhouse's payment status below. Do not treat the redirect parameters as proof of payment.

## 4. Confirm payment through Firmhouse

Adyen sends payment notifications directly to Firmhouse. Browser callbacks such as `onPaymentCompleted` are useful for showing progress, but they are not the authority for confirming the order.

```graphql theme={null}
query CheckoutOutcome($paymentToken: ID!) {
  getCheckoutPaymentStatus(paymentToken: $paymentToken) {
    paymentStatus
    failureReason
    subscriptionStatus
    successUrl
    abandonment { status }
  }
}
```

Poll for the **current payment token** after submission or a redirect. Stop polling when the outcome is known or the shopper leaves the payment flow. If confirmation takes longer than your UI's waiting period, show a pending message and allow status to be checked again; a timeout or network failure is not permission to submit another payment.

| Response | Storefront behavior |
| - | - |
| `abandonment` is present | This attempt is no longer a valid checkout. Ignore it for order confirmation, even if `paymentStatus` is `PAID` or `PAIDOUT`. |
| `PAID` or `PAIDOUT`, without abandonment | Show confirmation using the status response's `successUrl`. Subscription activation can follow the project's separate activation rules. |
| `OPEN` or `PENDING` | Show that confirmation is still pending. Do not create another payable cart just because a webhook is delayed. |
| `FAILED` | Show the failure and allow an eligible retry. `failureReason` can be null. |
| `CANCELLED`, `EXPIRED`, or a refund/chargeback status | Do not show a new successful order. Recover or investigate the original attempt before taking another payment. |

An existing `successUrl` is not itself proof of success. Read it from the status response after confirming payment. For an order-backed checkout it includes `order_id` and `order_value`, preserving your existing query parameters, so your confirmation page can record the purchase. `order_value` is a decimal amount, for example `123.45`, not an integer number of cents. Deduplicate purchase analytics by order ID.

## Retries and cart changes

For a refused payment on an unchanged cart, request `createAdyenPaymentSession` again with the same payment token. Firmhouse permits eligible failed initial payments to reopen, reusing an active session or obtaining a fresh one after expiry. It rejects already settled or abandoned payments. A slow or unknown payment outcome is not a refusal: check status before retrying.

When the shopper edits their cart or details after opening payment:

1. Stop using the current Drop-in and save the edits on the **same cart**.
2. Call `createSubscriptionFromCart` again, using the same cart authentication and your checkout URLs.
3. Check errors, then use the returned payment token for the session request and all subsequent status polling. Mount Drop-in with that session.

Firmhouse determines whether the checkout changed. An unchanged checkout reuses the payment and token. A changed, eligible checkout automatically abandons the old attempt and creates its replacement. You do not need to compare cart fields, copy the cart, or call abandonment separately for this flow.

Serialize finalisation requests. If the cart is already paid, processing, or cannot safely be replaced, finalisation returns errors. Do not fall back to creating another cart automatically. Keep the existing reference and check its outcome.

Cancel old polling and ignore stale callbacks when switching tokens. A response for the previous attempt must not complete the current checkout.

## Optional: explicitly abandon a checkout

Most integrations do not need a separate abandonment call. When shoppers edit the same cart and you finalise it again, Firmhouse handles abandonment and replacement automatically when needed.

Use `abandonAdyenCheckout` when your integration deliberately discards a checkout or creates a separate replacement cart. For a separate replacement cart, successfully abandon the original first using its own cart authentication:

```graphql theme={null}
mutation AbandonCheckout($paymentToken: ID!) {
  abandonAdyenCheckout(input: { paymentToken: $paymentToken }) {
    abandonment { status createdAt }
    errors { attribute message }
  }
}
```

Stop using the old form and wait for a successful response before continuing with a new cart. On failure, handle the error and check the original payment instead. Firmhouse cannot infer that two different carts replace each other from a shared email address.

## Late payments on abandoned checkouts

The following applies whether Firmhouse automatically abandons an attempt during same-cart replacement or your integration explicitly abandons it.

Abandonment is recorded in Firmhouse; it does not immediately invalidate the old Adyen session. If a successful authorisation arrives for an abandoned attempt, Firmhouse requests cancellation or refund asynchronously. The trigger is the payment notification, not a timer after session expiry. Funds may be held temporarily, and reversal requests can fail or need attention.

| Abandonment status | Meaning |
| - | - |
| `AWAITING_PAYMENT_OUTCOME` | Abandoned; no successful authorisation has been received. This is not a refund in progress. |
| `REVERSAL_PENDING` | An authorisation arrived and reversal confirmation is pending. |
| `REVERSED` | Adyen acknowledged reversal of all authorisations received so far. Later failure notifications are still possible. |
| `REQUIRES_ATTENTION` | A reversal failed or confirmation is missing. Contact Firmhouse with the payment reference. |

The portal retains abandoned payment and reversal history for investigation, including an abandoned checkout detail page for draft checkouts. This does not activate the abandoned subscription. An unused session can expire without a payment notification; do not expect a refund or an automatic terminal Firmhouse status for every unused attempt.

## Verify before going live

Use your Adyen TEST account to verify a successful payment and webhook confirmation, a refused-payment retry, a redirect return, an unchanged finalisation retry, and a same-cart edit that replaces the payment. Confirm that the old attempt cannot trigger order confirmation in your UI and that purchase analytics receives the new order ID. Repeat the methods and wallet/domain setup you actually intend to offer in production.

If Drop-in cannot load, check the client key's environment and allowed origin. If the browser reports success but Firmhouse remains pending, check webhook delivery and HMAC configuration. For an unresolved reversal, use its payment reference and portal history when contacting Firmhouse.


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