Partner API Reference

Complete endpoint reference for partner integrations

Quick-reference for all partner-accessible endpoints. For flow diagrams, webhook verification examples, and PCI scope notes see PARTNER_GUIDE.html.


Base URL and Authentication

Environment API base URL Checkout/iframe host
Production https://pay.emun1.com https://checkout.pay.emun1.com
Nonprod (testing) https://nonprod.pay.emun1.com https://checkout.nonprod.pay.emun1.com

iframeUrl values below resolve against the checkout/iframe host, not the API base URL.

Authorization: ApiKey <your-api-key>
Content-Type: application/json

Every request is automatically scoped to your merchant account. API keys are generated in the portal under API Keys — or, once you have one working key, entirely via the API below.

Partner-scoped keys (issued by Foundry, not self-service — see Partner-Scoped Keys) work differently: they aren't tied to one merchant, so every request must also include an X-Merchant-Id header naming which of your merchants that specific call is for.


API Keys

Issue, list, or revoke your own API keys. This is what lets you rotate credentials (e.g. from a secrets-manager rotation job) without a portal login: mint a new key with your current one, store it, then revoke the old one — the whole cycle is API-driven.

One exception: a brand-new merchant's very first key still has to come from the portal, since there's no key yet to authenticate this endpoint with. Everything after that is self-service.

List API Keys

GET /v1/api-keys

Response 200 — one row per active (non-revoked) key. The raw key value is never returned here — only the prefix, for identifying which key is which.

[
  {
    "id":        "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "prefix":    "fpy_3f9a",
    "label":     "Production — rotation 2026-08",
    "createdAt": "2026-08-01T12:00:00Z",
    "expiresAt": null
  }
]

Create an API Key

POST /v1/api-keys

Request body

Field Type Required Description
label string Yes Your own name for this key (e.g. which system or rotation cycle it's for).

Response 200

{
  "id":        "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "prefix":    "fpy_3f9a",
  "label":     "Production — rotation 2026-08",
  "createdAt": "2026-08-01T12:00:00Z",
  "rawKey":    "fpy_3f9a2b1c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a"
}

rawKey is returned exactly once, in this response only — Foundry never stores or displays the raw value again afterward. Store it immediately (e.g. directly into your secrets manager).


Revoke an API Key

DELETE /v1/api-keys/{id}

Response 204 on success. Revocation is immediate and cannot be undone — any request already using this key starts failing right away. 404 if the key doesn't exist or doesn't belong to your merchant.


Partner-Scoped Keys

If you operate multiple software platforms that each need to transact against overlapping sets of merchants (e.g. two separate apps both need access to the same merchant), a single merchant-scoped key per merchant per app doesn't fit — you'd end up managing one credential per (app, merchant) pair. A partner-scoped key solves this: one key, issued once, that can act across every merchant Foundry has linked it to, instead of exactly one.

This is not self-service — Foundry provisions your partner account, links your merchants to it, and issues the key, the same way your very first merchant-scoped key is provisioned today. Reach out if this is a better fit than per-merchant keys.

How it's different from a merchant-scoped key

List Your Merchants

GET /v1/partner/merchants

The one endpoint that works without X-Merchant-Id — returns every active merchant your partner account is linked to, so you can populate your own ID-mapping table. These are plain Foundry merchant IDs, not credentials — safe to store in an ordinary database column, no secrets-manager handling needed.

Each entry is { "id", "name", "externalMerchantId" }. externalMerchantId is your ID for the merchant as recorded in Foundry (null if none was set), the same one your webhooks carry and X-External-Merchant-Id accepts. A merchant that also works with another platform can have a different ID there; you only ever see yours.

A merchant that Foundry has deactivated drops out of this list, because a partner-scoped key naming one is refused with 403 — the list is a map of what your key can actually reach, so it agrees with that gate rather than advertising a merchant you would then be refused for. If a merchant you were using disappears from this list, that is what happened; talk to us rather than treating it as a broken link.

Re-read this list periodically rather than only at setup. Foundry can deactivate a merchant at any time when it ends the relationship, and your mapping table will otherwise keep an entry you can no longer transact against — the 403 applies to every call for that merchant, including captures and refunds on earlier payments.

A merchant whose payment provider suspends them is different: they stay on this list and your key still reaches them. Captures, voids and refunds against existing payments continue to work; only new payment requests and authorizations are refused, with 409 and "status": "OnboardingIncomplete" (see Error Responses).

Response 200

[
  { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "Acme Wholesale" },
  { "id": "7c4a9e1d-8f3b-4a1e-9c2d-1a2b3c4d5e6f", "name": "Example Distributors" }
]

Publishable Keys

A publishable key is a partner-scoped key restricted to one thing: creating add-card embed sessions (see "Embeddable UI Components" in the Partner Guide). It's the only key type safe to embed directly in client-side JavaScript — it cannot list or delete a buyer's stored cards, cannot act on any other endpoint, and is checked against a fixed list of allowed origins you register it with. Use the same X-Merchant-Id header as any other partner-scoped key. Not self-service in this release — contact Foundry with the domain(s) to register.

AllowedOrigins limits misuse and accidental exposure; it is not an authentication mechanism — Origin is a client-supplied header any non-browser caller can set arbitrarily. Don't rely on it to stop a determined attacker who already has the key value.


Payment Requests

Create a Payment Request

POST /v1/payment-requests

Request body

Field Type Required Description
referenceId string Yes Your order/invoice number. Returned in all webhooks. No length or format limit: it can be a machine key, but see customerReference and displayReference.
customerReference string No The purchase-order or document number sent to Adyen for Level 2/3 interchange, in place of referenceId. 1–25 ASCII characters, not starting with a space, not all spaces or zeros; anything else is 400 with code: "invalid_customer_reference". Without it, referenceId is sent when it meets the same rule and left out when it doesn't. Adyen requires this field for Level 2/3, so a payment without a valid one gets no Level 2/3 enrichment at all: send customerReference whenever your referenceId can be longer than 25 characters. See Enhanced Scheme Data.
displayReference string No Up to 100 characters. What the buyer sees as the order reference on the checkout page and in payment-link, receipt, failed-payment and refund emails, in place of referenceId. Webhooks and the iframe's completion message still carry referenceId.
buyerEmail string Yes Buyer email. Enables saved-card recall for returning buyers.
externalBuyerId string No Your own ID for the buyer, 3–100 characters; any secret API key. See Your Own Buyer IDs.
amount number Yes Amount in dollars (e.g. 312.50). Range: 0.01 – 1,000,000.
currency string No ISO 4217. Default: "USD". "CAD" for vendors set up for CAD; see CAD payments. Any other code is 400 with code: "currency_not_supported".
captureMode string No "manual" (default) or "immediate".
acceptedPaymentMethod string No "card", "ach", or omit to let buyer choose.
achAccountHolderType string No "business" (default) or "personal": whose bank account the buyer pays from, which sets the ACH SEC code when the account is charged — CCD for business, WEB for personal. Applies when the request is paid by ACH; ignored for cards. Anything else is 400 with code: "invalid_ach_account_holder_type".
buyerCompany string No Company name shown in the portal.
returnUrl string No Redirect after payment on mobile.
billingAddress object No See Billing Address.
lineItems array No See Line Items. Amounts in cents.
enhancedSchemeData object No Order-level Level 2/3 fields. See Enhanced Scheme Data.
paymentMethodId string (UUID) No Reuse a stored card — from an earlier card.enrolled webhook — instead of collecting one. buyerEmail must be the email of the buyer that card belongs to; that match is the authorization, and a mismatch is a 400. The card does not need to have been collected or consented by you: a stored card belongs to the buyer, not to the merchant who collected it, so any merchant may charge it given the right buyerEmail. Not compatible with acceptedPaymentMethod: "ach" (400). Combine with captureMode: "immediate" to charge the card in this same call — see below.

Response 200

{
  "id":        "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "token":     "abc123xyz...",
  "iframeUrl": "https://checkout.pay.emun1.com/enter/abc123xyz...",
  "status":    "Pending",
  "expiresAt": "2024-06-01T13:00:00Z"
}

Which card ids can you use? Take them from a card.enrolled webhook, or from the buyer in your own records — go buyer-first, card-second. There is deliberately no lookup from a paymentMethodId back to its owner: since cards are platform-wide, such a lookup would let anyone holding a card id read the email addresses of buyers who have never transacted with them. Knowing the email is the check, so a card id carried over from another context cannot be resolved after the fact.

Expires after 60 minutes, or 7 days from when its payment link is emailed (Send Payment Link). When paymentMethodId is supplied and valid, status comes back "Collected" immediately instead of "Pending" — no buyer interaction needed; go straight to POST /v1/payments. No payment_request.completed webhook is sent for a request created this way.

Refused with 409 and "status": "OnboardingIncomplete" while the merchant hasn't finished onboarding — see Error Responses.

Charging a stored card in one call. Send paymentMethodId together with captureMode: "immediate" and Foundry authorizes and captures the charge synchronously inside this same call — one request instead of the usual Create → Authorize → Capture sequence for a vault/token charge.

{
  "id":            "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "token":         "abc123xyz...",
  "iframeUrl":     "https://checkout.pay.emun1.com/enter/abc123xyz...",
  "status":        "Collected",
  "expiresAt":     "2024-06-01T13:00:00Z",
  "paymentId":     "9b1b3a2e-...",
  "paymentStatus": "Captured",
  "resultCode":    null,
  "refusalReason": null
}

paymentId/paymentStatus/resultCode/refusalReason are present only for this combination — every other Create call gets null for all four, nothing to report yet. A decline (Adyen's real decline shape — see Authorize's 422 below) returns the same 422 body Authorize would, with paymentRequestId and paymentId added so you can look either up afterward — the payment request and a Failed payment are both still persisted, exactly as if you'd made the other two calls yourself. A hard Adyen service error (not a decline) returns 502; the payment request stays Collected with no payment attached, and you retry with the normal POST /v1/payments call. Supports the same Idempotency-Key header as Authorize/Capture/Void/Refund — worth using here, since this single call can now move real money.

CAD payments

"currency": "CAD" charges the buyer the CAD amount you send.


Get Payment Request Status

GET /v1/payment-requests/{token}

Pass the string token (not the UUID) returned from the create call. Like every endpoint here, this requires your API key — call it from your own server, never directly from the buyer's browser.

Response 200

{
  "id":                    "3fa85f64-...",
  "referenceId":           "ORD-20240601-001",
  "buyerEmail":            "buyer@example.com",
  "buyerCompany":          "Acme Wholesale",
  "amount":                312.50,
  "currency":              "USD",
  "status":                "Collected",
  "createdAt":             "2024-06-01T12:00:00Z",
  "expiresAt":             "2024-06-01T13:00:00Z",
  "completedAt":           "2024-06-01T12:34:56Z",
  "acceptedPaymentMethod": "card",
  "customerReference":     null,
  "displayReference":      null
}
Status Meaning
Pending Waiting for buyer to complete payment.
Collected Card entered. Ready to authorize.
Expired 60-minute window elapsed (7 days from the latest emailed link, if one was sent).
Cancelled Cancelled by the platform.

Get Payment Request Detail

GET /v1/payment-requests/{id}

Takes the UUID id (not the string token) — the fuller counterpart to Get Payment Request Status above. Use this one specifically if you need the card on file (brand, last 4, holder name, expiry) — it's available as soon as status is Collected, before you've called Authorize, since it comes straight off the payment request's own record rather than a payment/transaction. This is also the endpoint to poll for paymentMethodId as a read-side complement to the card.enrolled webhook (a card enrollment created via POST /v1/card-enrollments is a payment request under the hood, addressed by the same id that endpoint returns).

Response 200

{
  "id":              "3fa85f64-...",
  "referenceId":      "ORD-20240601-001",
  "buyerEmail":       "buyer@example.com",
  "buyerCompany":     "Acme Wholesale",
  "amount":           312.50,
  "totalAmount":      312.50,
  "currency":         "USD",
  "status":           "Collected",
  "iframeUrl":        "https://checkout.pay.emun1.com/enter/abc123xyz...",
  "createdAt":        "2024-06-01T12:00:00Z",
  "expiresAt":        "2024-06-01T13:00:00Z",
  "completedAt":      "2024-06-01T12:34:56Z",
  "hasPaymentMethod": true,
  "paymentMethodId":  "9c1e2b3a-...",
  "cardBrand":        "mc",
  "cardLast4":        "4242",
  "cardHolderName":   "Jane Doe",
  "cardExpiryMonth":  8,
  "cardExpiryYear":   2027,
  "billingAddress":   null,
  "lineItems":        null,
  "enhancedSchemeData": null,
  "additionalCharges": null,
  "captureMode":      "manual",
  "acceptedPaymentMethod": "card",
  "isHostedPci":      false,
  "bankName":         null,
  "customerReference": null,
  "displayReference":  null,
  "chargedWithGatewayToken": false
}

customerReference and displayReference are null unless you sent them at create. paymentMethodId/cardBrand/cardLast4/cardHolderName/cardExpiryMonth/cardExpiryYear are all null until hasPaymentMethod is true. For a bank account, bankName names the bank and cardLast4 can be "****" when no account number is available; bankName is null for cards. isHostedPci is true only for a card collected through the retired HostedPCI iframe — a card collected today for a HostedPCI-mode merchant reads false. chargedWithGatewayToken is the one to read: true when this request is settled with a Gateway Token rather than charged through POST /v1/payments.


List Payment Requests

GET /v1/payment-requests

Query parameters

Parameter Type Description
status string Filter by status (Pending, Collected, Expired, Cancelled). An unrecognized value is ignored rather than rejected.
search string Case-insensitive match on reference ID, buyer email, or buyer company.
from ISO 8601 Earliest creation date.
to ISO 8601 Latest creation date.
minAmount number Minimum amount.
maxAmount number Maximum amount.
page integer Page number (1-based).
pageSize integer Results per page (default 50, maximum 200).

Response 200 — a plain array, newest first, of the same object Get Payment Request Status returns. There is no total or other envelope: a page with fewer than pageSize items is the last one.

[
  {
    "id":                    "3fa85f64-...",
    "referenceId":           "ORD-20240601-001",
    "buyerEmail":            "buyer@example.com",
    "buyerCompany":          "Acme Wholesale",
    "amount":                312.50,
    "currency":              "USD",
    "status":                "Collected",
    "createdAt":             "2024-06-01T12:00:00Z",
    "expiresAt":             "2024-06-01T13:00:00Z",
    "completedAt":           "2024-06-01T12:34:56Z",
    "acceptedPaymentMethod": "card",
    "customerReference":     null,
    "displayReference":      null
  }
]

amount is the order amount — it does not include charges; use Get Payment Request Detail for totalAmount. The list also includes card enrollments (amount 0, referenceId set to the enrollment's buyerReference) and card refresh requests (which carry the same referenceId as the request they refresh). A Pending request past its expiresAt reads Expired here once Foundry has expired it, about an hour later (see payment_request.expired).


Export Payment Requests (CSV)

GET /v1/payment-requests/export

Query parameters — status, search, from, to, minAmount and maxAmount, with the same meaning as List Payment Requests above. No paging: every matching request is exported, newest first. At most one year per export, with the same defaults and 400 as Export Transactions.

Response 200 — text/csv, filename payment-requests.csv, with these columns:

Column Description
Reference ID Your order/invoice number.
Buyer Email Buyer email.
Company Buyer company, if set.
Status Pending, Collected, Expired, or Cancelled.
Amount Order amount, excluding any charges.
Currency ISO 4217 currency code.
Payment Method The acceptedPaymentMethod set at creation — card, ach, or any when it was omitted. Not the method the buyer actually used.
Created At UTC, yyyy-MM-dd HH:mm:ss.
Expires At UTC, yyyy-MM-dd HH:mm:ss.
Completed At UTC, yyyy-MM-dd HH:mm:ss. Empty until the request is Collected.

Amend the Amount

Corrects the order total on a payment request whose card has been collected but which has not been authorized yet.

PATCH /v1/payment-requests/{id}

Foundry binds the amount at collection time and authorizes against it. When an order changes between checkout and shipment — quantities move, lines come and go — this keeps one payment request correct rather than re-raising a second one against the same reference.

Request body

Field Type Required Description
amount decimal Yes The corrected order total. Replaces the previous amount; must be greater than zero.
{ "amount": 900.00 }

Response 200 OK

{
  "totalAmount": 945.00,
  "amount":      900.00,
  "charges":     [ { "label": "Freight", "amount": 45.00 } ]
}

amount is the order total; charges are separate costs added since collection and are not touched by an amendment. totalAmount is the sum, and is what will be authorized.

Refused with 409 when:

Condition Why
A payment already exists for this request The amount is committed to Adyen. An immediate-capture merchant has already moved money — you find out here rather than at a shortfall on capture.
The request is not Collected A pending request has no card yet; create a new one at the right amount instead.
The request is a card enrollment or card refresh Neither has an order behind it, so there is no total to correct.

Amending to the amount already set succeeds and changes nothing, so a retry after an ambiguous response is safe.

This is not the same as adding a charge. A charge is a new cost discovered after checkout and carries a label saying what it is; an amendment restates what the order itself is worth. Keeping them apart is what lets a merchant tell "the order changed" from "we added freight" afterwards.

Add a Charge

Adds a labelled cost discovered after checkout (freight, handling) to a collected payment request that has not been authorized yet.

POST /v1/payment-requests/{id}/charges

Request body

Field Type Required Description
label string Yes What the charge is for, e.g. "Freight".
amount decimal Yes The charge amount. Range: 0.01 – 1,000,000.

Response 200 OK

{
  "totalAmount": 945.00,
  "charges":     [ { "label": "Freight", "amount": 45.00 } ]
}

totalAmount (the order amount plus every charge) is what Authorize will authorize. Refused with 409 when the request is not Collected or a payment already exists for it.

Cancel a Payment Request

Retires a request that will never be authorized — an abandoned checkout, a cancelled order, a request superseded by another.

POST /v1/payment-requests/{id}/cancel

No request body.

Response 200 OK

{
  "id":          "01a0d890-c74d-7b8c-8089-69ed35729c08",
  "status":      "Cancelled",
  "cancelledAt": "2026-09-26T14:02:11Z"
}

Works on a Pending or Collected request. Cancelling an already-cancelled one succeeds and keeps the original cancelledAt, so a retry after an ambiguous response is safe.

Refused with 409 when:

Condition Why
A payment already exists for this request It is not abandoned. Void or refund the payment instead — cancelling here would leave the request and the payment disagreeing about whether the order is live.
The request has already expired Already retired. Refusing keeps the record of when it actually lapsed.

It retires the request, not the card. A card collected against a cancelled request stays stored and consented — it was collected with the buyer's agreement and may be in use on another request. Cancelling is not a way to revoke a stored card.

A cancelled request cannot be collected against: creating a session for its token is refused, so a stale iframe link cannot walk it back into a checkout.


Request Card Refresh

When a stored card has expired or been declined during authorization, send the buyer a link to enter a replacement card.

POST /v1/payment-requests/{id}/card-update

The {id} is the UUID of the source payment request (status must be Collected).

Request body

Field Type Required Description
returnUrl string No Mobile redirect after card entry.

Response 200

{
  "id":        "9b1b3a2e-...",
  "token":     "xyz789...",
  "iframeUrl": "https://checkout.pay.emun1.com/enter/xyz789...",
  "expiresAt": "2024-06-01T14:00:00Z"
}

Show or send the iframeUrl to the buyer. On completion:

Expires after 60 minutes, or 7 days from when it's emailed: Send Payment Link with this response's id emails the buyer an update-card link naming the order, instead of a payment request.


Email the buyer a link to a hosted, invoice-style payment page — line items, amount, and your branding (from PUT /v1/settings/branding) — where they can pick or enter a payment method. Foundry's equivalent of Stripe's Payment Links.

POST /v1/payment-requests/{id}/send-link

The {id} is the UUID of the payment request (status must be Pending). No request body.

Response 200

{ "sent": true }

Can be called more than once — resending a lost link is fine, no dedupe window applies. Returns 409 if the payment request is no longer Pending (already collected or expired).

For a card enrollment or a card refresh, the {id} is the one those calls return, and the email asks the buyer to save or update a card rather than to pay; nothing is charged. It stays open 7 days the same way, without reminders.

Sending keeps the request open 7 days. A request otherwise expires 60 minutes after it's created, which suits a live checkout but not an email the buyer opens later, so each send moves expiresAt to 7 days from then (it never shortens it). Send the link within the first hour: a request already expired can't be revived. While it stays unpaid the buyer gets two reminders, 3 days after the latest send and a day before it expires, unless the merchant turns them off in Settings. GET /v1/payment-requests/{id} returns linkSentAt, linkRemindersSent and checkoutOpenedAt (when the buyer first opened the checkout). The link opens the same iframeUrl returned by POST /v1/payment-requests — if lineItems was set at creation, the page renders them; otherwise it's the same bare card-entry form used everywhere else.


HostedPCI Card Collection

A small number of merchants haven't migrated their actual payment processing to Adyen yet and still use a third-party vendor, HostedPCI, for PCI-compliant card storage. For these merchants the buyer enters their card in the same checkout, through the same iframeUrl you already embed — the POST /v1/payment-requests and GET /v1/payment-requests/{token} contracts above are unchanged, and the payment request still reaches Collected the same way. captureMode: "immediate" is not available for these merchants (400, immediate_capture_unavailable); use "manual".

After collection, each HostedPCI merchant is set up for one of two ways to charge, never both:

Either way, the card is placed in HostedPCI's vault the first time it is charged or a Gateway Token is requested for it.

Retrieve Gateway Token

POST /v1/payment-requests/{id}/gateway-token

The {id} is the UUID of a Collected payment request for a HostedPCI-mode merchant.

Response 200

{
  "gatewayToken":         "a7f3c9e2b1d84f06",
  "gatewayResponseStatus": "Approved",
  "gatewayResponseCode":   "00",
  "gatewaySubToken1":      null,
  "gatewaySubToken2":      null
}
Field Description
gatewayToken The vendor-specific token to send to your own downstream gateway. Can be empty when the vendor has no separate gateway configured (HostedPCI itself is the terminal processor) — this is expected, not an error.
gatewayResponseStatus / gatewayResponseCode The downstream gateway's own status/code for this tokenization, if any.
gatewaySubToken1 / gatewaySubToken2 Set only when the vendor's gateway returns two tokens, such as a customer and a card. gatewayToken then holds both joined by _TKNSEP_ (e.g. "CUST123_TKNSEP_PAY456"), and these hold each one, in that order. null for a gateway that returns a single token. Pass on whichever the merchant's gateway needs.

Call this whenever you need the token — right after the payment_request.completed webhook fires, or later. It's independent of card collection itself. A 409 means the payment request isn't a Collected HostedPCI-mode request, or that the merchant is charged through Foundry (hostedpci_processing_mode); a 502 means either placing the card in HostedPCI's vault failed (safe to retry) or the vendor's gateway tokenization itself failed (detail carries the underlying error).

The same card gets the same token. Foundry keeps the Gateway Token it got for a card under the vendor's HostedPCI profile and returns it again, for this payment request or any later one charging the same card for the same vendor; each new token would otherwise leave another copy of the card at the vendor's gateway. A new token is fetched when the card's expiry date changes, or when you ask:

POST /v1/payment-requests/{id}/gateway-token?refresh=true

Use refresh=true when the merchant reports that their gateway no longer accepts the token (for example, they deleted the customer there). The new token replaces the old one for later calls.

Retrieve a Gateway Token for a Saved Card

POST /v1/payment-methods/{id}/gateway-token[?refresh=true]

The same Gateway Token for a stored card directly, so you don't need a payment request to hang it on — for example, to fetch one token per card per vendor when a buyer saves a card. {id} is a paymentMethodId; the vendor is the merchant your key (or X-Merchant-Id) names, which must be a HostedPCI-mode merchant.

Field Type Required Description
buyerEmail string One of the two The email of the buyer the card belongs to, matched without regard to case — the same check as reusing a card on Create a Payment Request.
externalBuyerId string One of the two Your own ID for the buyer the card belongs to, with a secret API key; see Your Own Buyer IDs. Tried first when both are sent.
referenceId string No Your reference for this tokenization, up to 100 characters, passed to the vendor's gateway. Defaults to the paymentMethodId.
buyerCompany string No The buyer's company name, passed to the vendor's gateway.
billingAddress object No Same shape as on Create a Payment Request (street, city, stateOrProvince, postalCode, country), passed to the vendor's gateway.

The response, refresh=true, the "same card gets the same token" behaviour and the 409/502 cases are as for the payment-request version above: a token fetched either way is the one returned by both. A 404 means no active card with that id belongs to that externalBuyerId or buyerEmail; a 400 means neither was sent.


Card Migration (HostedPCI → Adyen)

Moves cards your buyers saved in your HostedPCI vault onto Adyen, so a merchant moving to Adyen keeps its buyers' cards on file. Requires a partner-scoped key; a partner can only migrate from its own vault, and only for merchants linked to it, named by X-Merchant-Id or by your own ID for the merchant in X-External-Merchant-Id. The HostedPCI token is untouched, so nothing changes on your side if you roll back.

Migrate One Card

POST /v1/card-migrations
Field Type Required Description
hostedPciToken string Yes The card's token in your HostedPCI vault.
buyerEmail string Yes The buyer's email. The card lands in that buyer's wallet.
externalBuyerId string No Your own ID for the buyer, 3–100 characters; any secret API key. See Your Own Buyer IDs.
expiryMonth / expiryYear integer Yes Card expiry (1–12, four-digit year). An expired card is skipped.
holderName string Yes Cardholder name.
billingAddress object Yes street, city, postalCode, country required; stateOrProvince optional. HostedPCI requires it.
externalReference string No Your own ID for the card, returned as-is so you can match results.
source string No Your vault. Leave it out; naming another partner's vault is refused.
hostedPciProfileId string No Leave it out to use the migration profile configured for your vault.

Response — 200 for every outcome except Failed, which is 422 with the same body:

{
  "migrationId":       "…",
  "status":            "Succeeded",
  "paymentMethodId":   "…",
  "last4":             "1111",
  "brand":             "visa",
  "failureCode":       null,
  "failureMessage":    null,
  "externalReference": "your-card-123"
}
status Meaning
Succeeded The card is now on Adyen. paymentMethodId is the card to charge from now on
Merged The buyer already had this card; the migration joined it. Use the returned paymentMethodId
Skipped Expired card, or already on Adyen (failureCode says which)
Failed Not migrated; failureCode / failureMessage say why. Safe to retry

Repeating a call for a card that already succeeded, merged or was skipped returns the same result without contacting HostedPCI again. The response never contains the Adyen token.

Migrate Cards in Bulk

POST /v1/card-migrations/batch

{ "items": [ … ] }, each item shaped like the single request, up to 100 per call (50 in nonprod), all for the one merchant named by X-Merchant-Id or X-External-Merchant-Id (split a vendor with more cards over several calls). Always 200 with a result per card, in order:

{ "items": [ { "hostedPciToken": "…", "success": true, "result": { …as above… }, "error": null } ] }

success is true when the card can now be charged through Adyen. An item that fails validation, or names another vault, gets success: false and an error without stopping the rest. The batch stops at the first card Adyen can't find after migrating it (failureCode readback_miss), since that almost always means a misconfigured HostedPCI profile that would fail every remaining card; the rest come back "Not attempted". Contact us before retrying.


Card Enrollment

Store a card on file without tying it to any specific order or invoice.

POST /v1/card-enrollments

Request body

Field Type Required Description
buyerEmail string Yes Buyer email. Finds the buyer's wallet for returning buyers.
externalBuyerId string No Your own ID for the buyer, 3–100 characters; any secret API key. See Your Own Buyer IDs.
buyerReference string No Your own buyer/account ID — a fully opaque string. Foundry never parses, validates, or attaches any meaning to it; it's stored as-is and returned verbatim in the card.enrolled webhook so you can correlate the new card back to whatever it identifies on your side.
buyerCompany string No Company name (display only).
returnUrl string No Mobile redirect after card entry.
billingAddress object No Pre-populated in the form. See Billing Address.

Response 200

{
  "id":        "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "token":     "abc123xyz...",
  "iframeUrl": "https://checkout.pay.emun1.com/enter/abc123xyz...",
  "expiresAt": "2024-06-01T13:00:00Z"
}

Show the iframeUrl to the buyer. On completion you receive a card.enrolled webhook with the paymentMethodId — a stable reference for "this buyer has a card on file." To collect payment later, create a payment request (POST /v1/payment-requests) with the same buyerEmail and pass that paymentMethodId to charge the card with no buyer interaction; or omit it, and the buyer sees their enrolled card as a one-click option in the iframe instead of entering it again.

Enrollment collects cards only. If you omit buyerReference, Foundry generates a random one and returns it in the webhook.

Expires after 60 minutes, or 7 days from when it's emailed: Send Payment Link with the enrollment's id emails the buyer a save-card link ("Save a card with …", nothing charged) instead of a payment request.


Payments

Idempotency

POST /v1/payment-requests (Create), POST /v1/payments (Authorize), POST /v1/payments/{id}/capture, POST /v1/payments/{id}/void, and POST /v1/payments/{id}/refund all accept an optional Idempotency-Key header. Send one on any call you might need to retry — a network timeout, no response received, an ambiguous error — and a retry with the same key and the same request body returns the original response instead of re-running the operation against the card network a second time. Omit the header and nothing changes; retry safety is opt-in.

POST /v1/payments
Idempotency-Key: 3f29a1c4-8b7e-4e2a-9c1d-6a5b4c3d2e1f

End-to-End Example: Create → Collect → Authorize → Capture

The "manual" capture-mode flow (the default) involves two different Foundry-generated IDs at two different steps — the most common point of confusion, so worth spelling out end to end before diving into each endpoint individually.

1. Create the payment request.

POST /v1/payment-requests
{ "referenceId": "ORD-20240601-001", "buyerEmail": "buyer@example.com", "amount": 312.50 }
{ "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "token": "abc123xyz...", "iframeUrl": "...", "status": "Pending" }

Store id as paymentRequestId — you'll need it in step 3.

2. Buyer enters their card via iframeUrl. You're notified by the payment_request.completed webhook once the payment request reaches Collected. Nothing new to store here.

3. Authorize (place the hold).

POST /v1/payments
{ "paymentRequestId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }
{ "id": "7c4a9e1d-8f3b-4a1e-9c2d-1a2b3c4d5e6f", "status": "Authorized", "authorizedAmount": 312.50, "referenceId": "ORD-20240601-001" }

Store this response's id as paymentId — a different UUID from paymentRequestId above. This is the one you'll use for every step from here on (capture, void, refund, GET — see Get Payment — and reconciliation) — paymentRequestId isn't used again.

4. Capture (settle the charge).

POST /v1/payments/7c4a9e1d-8f3b-4a1e-9c2d-1a2b3c4d5e6f/capture
{}
{ "id": "7c4a9e1d-8f3b-4a1e-9c2d-1a2b3c4d5e6f", "status": "Captured", "capturedAmount": 312.50, "remainingAmount": 0 }

The URL uses paymentId from step 3 — not paymentRequestId. An empty/omitted body captures the full remaining balance; pass {"amount": ...} for a partial/split-shipment capture, and optionally shipmentReference to tag which shipment it covers — it is returned in the capture history, which is what makes a capture reconcilable against a shipment.

ID From Used for
paymentRequestId Create response (step 1) Authorize (step 3) only
paymentId (the Authorize response's id) Authorize response (step 3) Capture, Void, Refund, Get Payment, reconciliation

If juggling two Foundry-generated UUIDs is more bookkeeping than you want, Batch Authorize and Batch Capture accept your own referenceId directly instead of either one — worth using even for a single payment if you'd rather key everything off your own IDs.


Authorize

Place an authorization hold on the funds for a Collected payment request. This is the API equivalent of clicking Authorize on the payment request's detail page in the portal — useful if you'd rather trigger it from your own systems than have someone do it manually. Only applies to "manual" capture-mode payment requests — "immediate" ones are already authorized (and captured) automatically as soon as the buyer submits the payment form, and calling this endpoint on one returns an error.

POST /v1/payments

Request body

Field Type Required Description
paymentRequestId UUID Yes ID of the Collected payment request.

Response 200

{
  "id":               "7c4a9e1d-...",
  "status":           "Authorized",
  "authorizedAmount": 312.50,
  "capturedAmount":   0,
  "remainingAmount":  312.50,
  "currency":         "USD",
  "referenceId":      "ORD-20240601-001",
  "paymentRequestId": "3fa85f64-...",
  "authorizedAt":     "2024-06-01T12:35:00Z",
  "resultCode":       null,
  "refusalReason":    null
}

Use id (or your own referenceId) as your stable identifier. An authorization left uncaptured is voided automatically after a few days — see Uncaptured Authorizations below.

Refused with 404 if the payment request isn't Collected, 400 for an "immediate" request, and 409 with "status": "OnboardingIncomplete" while the merchant hasn't finished onboarding.

Payment Status Meaning
Authorized Funds held. Capture via the API or portal to settle.
PartiallyCaptured One or more partial captures settled; remaining authorized balance is still open.
Captured Capture is finished: the full authorized amount, or partial captures followed by a void that released the rest (releasedAmount).
Failed Authorization declined.
Voided Authorization cancelled with nothing captured — by you, or automatically when left uncaptured.
Refunded Post-capture refund issued.
ChargedBack A chargeback (or, for ACH, a returned debit) was received. Moves back to Captured if the chargeback is reversed. Sent as payment.charged_back and payment.chargeback_reversed.

Declined authorization — 422

{
  "error":         "Adyen declined the payment",
  "resultCode":    "Refused",
  "refusalReason": "Insufficient funds"
}

resultCode is the card network's own result code, passed through unchanged — common values are Refused (generic decline), Error (a processing problem, not necessarily the card itself), and Blocked/Cancelled. refusalReason is a short, human-readable string when the network provides one; it can be null even on a genuine decline. Both fields are also null on a successful response — they're specific to the failure path. Batch Authorize surfaces the same two fields per item instead of a top-level error.


Get Payment

Fetch a single payment by the id returned from Authorize (or any capture/void/refund call) — the same identifier every other /v1/payments/{id}/... action already addresses it by. Use this when you only have the payment id on hand (e.g. from a webhook) and need its current status, rather than filtering List Transactions by paymentRequestId.

GET /v1/payments/{id}

Response 200 — same shape as every other action on this resource (Authorize, Capture, Void, Refund) — full captures[]/refunds[] arrays, capturedAmount/refundedAmount/remainingAmount, and authorizationEvents[] (see Uncaptured Authorizations).

404 if id doesn't exist, or belongs to a different merchant than your API key's.


Capture

Capture (settle) an authorized payment — the API equivalent of clicking Capture on the payment's detail page in the portal. Only Authorized or PartiallyCaptured payments can be captured.

POST /v1/payments/{id}/capture

Request body (optional — omit entirely to capture the full remaining balance)

Field Type Required Description
amount number No Amount to capture. Defaults to the full remaining authorized balance. Use for split-shipment workflows — capture again later with the remaining amount once the rest ships. Cards only: an ACH payment can only be captured in full (400, "code": "ach_partial_capture").
shipmentReference string No Optional tag for this capture (e.g. "SHIP-A"). Returned in the capture history for reconciliation.
lineItems array No Optional Level 2/3 lines for what this capture covers, in the payment request's lineItems shape. Lets a partial capture qualify for lower card interchange. See Level 2/3 on partial captures.
enhancedSchemeData object No Optional order-level Level 2/3 fields for this capture, typically its freightAmount and totalTaxAmount. They replace the payment request's same fields; the request's others still apply.

Response 200

{
  "id":              "7c4a9e1d-...",
  "status":          "Captured",
  "authorizedAmount": 312.50,
  "capturedAmount":   312.50,
  "remainingAmount":  0,
  "currency":        "USD",
  "referenceId":     "ORD-20240601-001",
  "capturedAt":      "2024-06-01T13:00:00Z"
}

A capture smaller than the remaining authorized amount moves status to PartiallyCaptured rather than Captured — submit another Capture call against the same id later for the rest. If you're capturing many payments at once, Batch Capture below does the same thing across a whole list in one call, including partial/split-shipment amounts.

A CAD capture is 502 with code: "exchange_rate_unavailable" if Adyen's rate service doesn't answer; retry it shortly. See CAD payments.


Void

Cancel the outstanding, uncaptured portion of an authorization — the API equivalent of clicking Void (or Release rest) in the portal. Only Authorized or PartiallyCaptured payments can be voided.

POST /v1/payments/{id}/void

No request body.

Response 200

{
  "id":              "7c4a9e1d-...",
  "status":          "Voided",
  "authorizedAmount": 312.50,
  "capturedAmount":   0,
  "currency":        "USD",
  "referenceId":     "ORD-20240601-001"
}

After partial captures (a $1,000 authorization, $900 captured):

{
  "id":                 "7c4a9e1d-...",
  "status":             "Captured",
  "authorizedAmount":   1000.00,
  "capturedAmount":     900.00,
  "remainingAmount":    0,
  "releasedAmount":     100.00,
  "releasedAt":         "2026-10-13T14:16:58Z",
  "releaseConfirmedAt": null,
  "currency":           "USD",
  "referenceId":        "ORD-20240601-001"
}

releaseConfirmedAt fills in once Adyen confirms.

Response 409, code: "capture_pending": a capture on the payment hasn't been confirmed by the processor yet (its status is still Pending). Releasing then would cancel that capture's money too, so wait for it to confirm, usually within a minute (payment.capture_updated, or poll the payment), and release again.


Refund

Refund a captured payment, in full or in part, up to what was captured. That includes a PartiallyCaptured payment (it stays PartiallyCaptured while part of the authorization is still open) and one whose remainder was released. Refunds settle asynchronously — the response reflects the refund request being accepted, not final confirmation from the card network.

POST /v1/payments/{id}/refund

Request body

Field Type Required Description
reason string Yes One of RequestedByCustomer, IssueWithItemSold, Fraudulent, Duplicate, Other.
amount number No Amount to refund. Defaults to the full refundable balance (captured amount minus any prior refunds).

Response 200

{
  "id":              "7c4a9e1d-...",
  "status":          "Refunded",
  "authorizedAmount": 312.50,
  "capturedAmount":   312.50,
  "refundedAmount":   312.50,
  "currency":        "USD",
  "referenceId":     "ORD-20240601-001",
  "refunds": [
    {
      "id":            "5d2e8a1f-...",
      "amount":        312.50,
      "currency":      "USD",
      "reason":        "RequestedByCustomer",
      "status":        "Pending",
      "createdAt":     "2024-06-02T09:00:00Z",
      "processedAt":   null,
      "failureReason": null
    }
  ]
}

status moves to Refunded only once the refunds cover the full captured amount — a partial refund leaves status as Captured, with the partial refund listed in refunds and counted in refundedAmount. Only Captured payments (including partially refunded ones) can be refunded — not PartiallyCaptured.

Each refund starts Pending and moves to Processed or Failed (with failureReason) once the card network confirms it. No webhook is sent for this — read it from Get Payment. A failed refund that had moved the payment to Refunded moves it back to Captured, and a failed refund's amount no longer counts in refundedAmount.


Batch Authorize

Authorize multiple payment requests in one call. Always returns HTTP 200; inspect each item's success field — the batch status code does not indicate per-item success.

POST /v1/payments/batch-authorize

Request body

{
  "items": [
    { "referenceId": "ORD-001" },
    { "referenceId": "ORD-002", "amount": 150.00 }
  ]
}
Field Type Required Description
items[].referenceId string Yes Your order/invoice reference.
items[].amount number No Override the payment request amount (advanced use). Defaults to the payment request's amount plus any charges, the same as single Authorize.

Response 200

{
  "results": [
    { "referenceId": "ORD-001", "success": true,  "paymentId": "7c4a9e1d-..." },
    { "referenceId": "ORD-002", "success": false, "error": "Payment request not found or not in collected state" },
    { "referenceId": "ORD-003", "success": false, "paymentId": "9b1b3a2e-...", "error": "Adyen declined the payment", "resultCode": "Refused", "refusalReason": "Insufficient funds" }
  ],
  "summary": { "total": 3, "succeeded": 1, "failed": 2 }
}

resultCode/refusalReason are only present on an item declined by the card network specifically (same meaning as Authorize's decline response) — null/omitted for every other kind of per-item failure (validation errors, a payment request in the wrong state, etc.).

An item whose payment request already has an Authorized payment succeeds with that existing paymentId rather than authorizing again — but only while that payment is still Authorized; after any capture or a void, the same item authorizes again. Items with a blank referenceId are skipped. A batch larger than the size limit is rejected whole with 400.


Batch Capture

Capture multiple authorized (or partially captured) payments in one call. Supports partial amounts and per-capture shipment references for split-shipment workflows.

POST /v1/payments/batch-capture

Request body

{
  "items": [
    { "referenceId": "ORD-001" },
    { "referenceId": "ORD-002", "amount": 150.00, "shipmentReference": "SHIP-A" }
  ]
}
Field Type Required Description
items[].referenceId string Yes Your order/invoice reference.
items[].amount number No Amount to capture; defaults to the full remaining balance. Use for split-shipment workflows.
items[].shipmentReference string No Optional tag for this capture (e.g. "SHIP-A"). Returned in capture history for reconciliation.
items[].lineItems array No Optional Level 2/3 lines for this capture, as on the single capture.
items[].enhancedSchemeData object No Optional order-level Level 2/3 fields for this capture, as on the single capture.

Response 200 — same per-item { referenceId, success, paymentId, error } format as Batch Authorize. Always HTTP 200; inspect each item's success field.


Uncaptured Authorizations

Manual-capture card authorizations are not held indefinitely, and Foundry does not extend or re-authorize them. If a card payment is still Authorized or PartiallyCaptured about three days after authorization, the merchant is reminded to capture it; at about five days, whatever remains uncaptured is voided automatically. The payment moves to Voided, and anything already captured stands. To charge the buyer after that, authorize the payment request again. ACH payments are not affected.

Payment Status Meaning
Expired The card network expired the authorization before capture. Nothing was captured. A partially-captured payment whose remaining balance suffers this stays PartiallyCaptured — only the un-captured remainder is affected.

GET /v1/payments/{id} and GET /v1/transactions include an authorizationEvents array per payment recording these steps — eventType (CaptureReminderSent, AutoVoided, ExpiredWebhookReceived), outcome, amount and time. Older payments can also carry ExtensionAttempted or ReauthorizationAttempted entries from before this rule.


Transactions

List Transactions

GET /v1/transactions

Query parameters

Parameter Type Description
status string Filter by payment status (e.g. Authorized, Captured, Expired).
search string Case-insensitive match on reference ID or buyer company.
paymentRequestId UUID Return only transactions for a specific payment request.
externalBuyerId string Only this buyer's transactions, by your own ID for them (see Your Own Buyer IDs). For a customer-record view in your own system.
buyerEmail string Only this buyer's transactions, by any email their wallet has had. Used when externalBuyerId isn't sent or doesn't match. A buyer neither finds returns no transactions.
from ISO 8601 Earliest transaction date.
to ISO 8601 Latest transaction date.
minAmount number Minimum authorized amount.
maxAmount number Maximum authorized amount.
page integer Page number (1-based).
pageSize integer Results per page (default 25).

Response 200 — { total, page, pageSize, items }, where each item is a payment object with the same amounts as Get Payment (authorizedAmount, capturedAmount, remainingAmount, refundedAmount), its captures, refunds and authorizationEvents arrays — see Uncaptured Authorizations above — and, when a payment method is on file, cardBrand, cardLast4, bankName, cardHolderName, cardExpiryMonth, cardExpiryYear. chargebackReasonCode and chargebackReason carry the reason for the latest chargeback (for an ACH return, the R code). On a Failed payment, resultCode and refusalReason say why it was declined, as Authorize returned them; both are null otherwise, and refusalReason can be null on a genuine decline.


Export Transactions (CSV)

GET /v1/transactions/export

Query parameters — status, search, from, to, minAmount and maxAmount, with the same meaning as List Transactions above. paymentRequestId is not supported here. No paging: every matching transaction is exported, newest first.

An export covers at most one year. With no to, it ends now; with no from, it starts a year before to. So an export with no dates returns the last year. A from–to range longer than a year returns 400; export longer periods in parts.

Response 200 — text/csv, filename transactions.csv, with these columns:

Column Description
Reference ID Your order/invoice number.
Company Buyer company, if set.
Status Payment status (see Authorize).
Card Brand Brand of the payment method on file, if any.
Card Last4 Last 4 digits of the card or bank account on file, if any.
Authorized Amount Amount authorized.
Captured Amount Amount captured so far.
Refunded Amount Sum of refunds that haven't failed.
Currency ISO 4217 currency code.
Created At UTC, yyyy-MM-dd HH:mm:ss.
Authorized At UTC, yyyy-MM-dd HH:mm:ss. Empty if never authorized.
Captured At UTC, yyyy-MM-dd HH:mm:ss. Empty if nothing has been captured.

An export made with an API key never includes the payment processor's own reference for a transaction — like card data, it stays outside your platform. Use Reference ID to reconcile.


Customers

Look up a buyer's payment history and saved cards on file — useful for cross-referencing your own records (e.g. flagging orders whose card is close to expiring, or showing which card was used on a past order) without ever needing raw card data or a token. savedPaymentMethods only ever includes cards saved with, or charged by, your own merchant — even though a buyer's card can be shared across multiple merchants on the platform, you only ever see your own relationship with them. A buyer appears here as soon as they have a payment or a saved card with you — including a buyer who's only ever completed a card enrollment (POST /v1/card-enrollments) and never been charged.

Your Own Buyer IDs

A buyer's saved cards are one wallet, shared by every merchant and partner, and found by the buyer's email, ignoring capitalisation and surrounding spaces: Bob@Shop.com and bob@shop.com reach the same wallet. That is what lets a card saved with one vendor be used with another. If your buyers can change their email, also send externalBuyerId, your own ID for the buyer, on payment requests, card enrollments, card migrations and embed sessions. It works with any secret API key, partner or merchant: an ERP integration can send its own customer ID. Your IDs are yours alone: another partner or merchant using the same ID means a different buyer.

Status code When
400 external_buyer_id_needs_api_key Sent from a portal session or with a publishable key
400 invalid_external_buyer_id Shorter than 3 or longer than 100 characters
409 email_in_use The new email already belongs to another wallet, so the two can't be merged

Change a Buyer's Email

POST /v1/customers/change-email

Replaces one of a wallet's emails with another, so the old address no longer reaches the buyer's cards: use it when a buyer reports an address they no longer control. The saved cards stay, and the wallet's open payment requests move to the new email. Partner keys only (403 with code: "partner_key_required" otherwise), because the wallet is shared by every merchant.

{ "currentEmail": "old@buyer.test", "newEmail": "new@buyer.test" }

Response 200: { "buyerId": "…", "email": "new@buyer.test" }. 404 when no wallet has currentEmail; 409 with code: "email_in_use" when newEmail belongs to another wallet.

List Customers

GET /v1/customers

Query parameters

Parameter Type Description
search string Full-text search on buyer email or company name. Matches any email the buyer's wallet has had, though each customer is listed under its current one.
page integer Page number (1-based).
pageSize integer Results per page (default 25).

Response 200 — one row per buyer you've transacted with:

{
  "total": 132,
  "page": 1,
  "pageSize": 25,
  "items": [
    {
      "buyerId":       "9d1c5f2a-6b3e-4f1a-8c2d-1a2b3c4d5e6f",
      "email":         "buyer@example.com",
      "company":       "Acme Wholesale",
      "paymentCount":  14,
      "totalCaptured": 4820.00,
      "totalCapturedCad": 0.00,
      "lastActivityAt": "2026-08-20T15:04:00Z"
    }
  ]
}

totalCaptured is USD. A vendor enabled for CAD gets its Canadian-dollar sales in totalCapturedCad, never added into the USD total.


Get Customer Detail

GET /v1/customers/{buyerId}

{buyerId} comes from the List Customers response above. Returns 404 if the buyer doesn't exist at all, or if they exist but only through a relationship with a different merchant — both cases look identical on purpose, so no information about another merchant's buyers ever leaks through.

Response 200

{
  "buyerId":         "9d1c5f2a-6b3e-4f1a-8c2d-1a2b3c4d5e6f",
  "email":           "buyer@example.com",
  "company":         "Acme Wholesale",
  "paymentCount":    14,
  "totalCaptured":   4820.00,
  "totalCapturedCad": 0.00,
  "savedPaymentMethods": [
    {
      "paymentMethodId": "9c1e2b3a-...",
      "last4":       "4242",
      "brand":       "mc",
      "holderName":  "Jane Doe",
      "bankName":    null,
      "expiryMonth": 8,
      "expiryYear":  2027,
      "consentedAt": "2026-03-01T10:00:00Z"
    }
  ],
  "payments": [
    {
      "id":              "7c4a9e1d-8f3b-4a1e-9c2d-1a2b3c4d5e6f",
      "referenceId":     "ORD-20240601-001",
      "status":          "Captured",
      "authorizedAmount": 312.50,
      "capturedAmount":   312.50,
      "currency":        "USD",
      "createdAt":       "2024-06-01T12:35:00Z"
    }
  ]
}

savedPaymentMethods never includes a card number or the underlying processor token — paymentMethodId is Foundry's own reference (the same one returned by card.enrolled and accepted by POST /v1/payment-requests's paymentMethodId field to reuse a card), alongside what's needed to recognize which card is on file and whether it's expiring soon.


Payment Methods

List a Buyer's Saved Payment Methods

GET /v1/payment-methods?buyerEmail={buyerEmail}
GET /v1/payment-methods?externalBuyerId={externalBuyerId}

Every active card and bank account in the buyer's wallet, whichever vendor or partner it was saved through: use it to fill your own card picker, for example when a rep picks a saved card for an order. Expired cards are left out (a card is good through the end of its expiry month), so you don't need to archive them yourself. They stay in the buyer's wallet, and a replacement card appears here once the buyer enters it. Charge one by passing its paymentMethodId to Create a Payment Request. This differs from a customer's savedPaymentMethods on Get Customer Detail, which shows only cards saved with, or charged by, your merchant.

Parameter Type Required Description
buyerEmail string One of the two Any email the buyer's wallet has had.
externalBuyerId string One of the two Your own ID for the buyer; see Your Own Buyer IDs. Tried first when both are sent.

Any secret key can call it, merchant-scoped or partner-scoped; a partner-scoped key needs no X-Merchant-Id here, because the wallet isn't one merchant's. A portal session gets 403 with code: "api_key_required", and a publishable key is refused.

Response 200. Newest first, with items empty and buyerId null when no wallet matches. bankName is set for bank accounts.

{
  "buyerId": "9d1c5f2a-6b3e-4f1a-8c2d-1a2b3c4d5e6f",
  "items": [
    {
      "paymentMethodId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "brand":       "visa",
      "last4":       "1111",
      "expiryMonth": 3,
      "expiryYear":  2030,
      "holderName":  "Jane Doe",
      "bankName":    null,
      "savedAt":     "2026-10-23T18:04:11Z"
    }
  ]
}

400 when neither parameter is sent.

Remove a Saved Card

DELETE /v1/payment-methods/{id}?buyerEmail={buyerEmail}

{id} is a paymentMethodId — from a card.enrolled webhook, Get Payment Request Detail, or a customer's savedPaymentMethods. Use this when the buyer asks you to remove a card they've saved. No request body.

Query parameters

Parameter Type Required Description
buyerEmail string Yes The email of the buyer the card belongs to, matched without regard to case — the same check as reusing a card on Create a Payment Request.

Any merchant-scoped or partner-scoped secret key can call it; a partner-scoped key must send X-Merchant-Id as usual. A publishable key cannot (403).

A stored card belongs to the buyer, not to the merchant who collected it, so removing it removes it for every merchant, not just yours: the card is deleted from the payment processor's store, is no longer offered to the buyer at checkout, and passing its paymentMethodId to POST /v1/payment-requests returns 400. Payments already made with the card are unaffected.

Status When
204 Removed. Removing a card that was already removed also returns 204, so a retry is safe.
400 buyerEmail is missing.
404 No payment method with that id belongs to that buyerEmail.
500 The payment processor refused the deletion. The card stays active; retry later.

Payouts

Deposit reconciliation — what was paid into your bank account, and which sales, refunds, and chargebacks made up each deposit. A different question from Transactions above (which is per-transaction economics) — see the portal's own Payouts page for the equivalent UI.

List Payouts

GET /v1/payouts

Query parameters

Parameter Type Required Description
from ISO 8601 date Yes Earliest deposit date.
to ISO 8601 date Yes Latest deposit date.
search string No Matches the order/invoice reference of a transaction within the deposit.
minAmount number No Minimum deposit (net) amount.
maxAmount number No Maximum deposit (net) amount.
page integer No Page number (1-based).
pageSize integer No Results per page (default 25).

Response 200

{
  "total": 5,
  "page": 1,
  "pageSize": 25,
  "items": [
    {
      "id":               "0c7d41e2-...",
      "settlementId":     "5b2e9f80-...",
      "payoutDate":       "2024-06-16T07:00:25Z",
      "grossAmount":      13318.96,
      "deductionsAmount": 145.00,
      "netAmount":        13173.96,
      "currency":         "USD",
      "transactionCount": 6
    }
  ]
}
Field Description
id The deposit's own ID. It never changes, including when the processor re-sends its report. Pass it to Get Payout Detail as payoutId.
settlementId Groups the deposits and lines from one daily payout report for this merchant. See Reconciling a settlement.
payoutDate The time the payout was booked, in UTC. Detail also accepts it, matched exactly, but prefer id. (Before 27 Oct 2026 it was reported 1–2 hours late, in Amsterdam time marked as UTC; values saved before then no longer match.)

Reconciling a settlement

The processor reports payouts once a day, and within one report a merchant's lines and deposits net to zero: the lines' amounts add up to the deposits' netAmounts. That includes the Fee lines (our separately charged fees, collected from the balance) and TopUp lines (bank top-ups into it); see Get Payout Detail. Usually that's one deposit and its lines. But the processor can pay one balance out in two transfers, an hour or so apart, and attach every line to the first; the second then has transactionCount: 0 and no lines of its own. Both deposits and all the lines share a settlementId, so reconcile by settlementId, not one deposit at a time. For example: lines of $0.96 and $0.96, against deposits of $1.84 and $0.08.


Get Payout Detail

Every transaction, refund, and chargeback that composed one specific deposit.

GET /v1/payouts/detail

Query parameters

Parameter Type Required Description
payoutId UUID One of the two The id of a List Payouts item.
payoutDate ISO 8601 One of the two The exact payoutDate of a List Payouts item.

Response 200

{
  "items": [
    {
      "id":            "8f2a1c3e-...",
      "payoutId":      "0c7d41e2-...",
      "settlementId":  "5b2e9f80-...",
      "lineType":      "Capture",
      "description":   "Seller split",
      "amount":        9412.80,
      "currency":      "USD",
      "paymentId":     "3c0f9a14-...",
      "captureId":     "b71e2d05-...",
      "refundId":      null,
      "fee":           87.20,
      "topUpId":       null,
      "feeCharges":    null,
      "referenceId":   "ORD-1005",
      "customerName":  "Acme Wholesale"
    },
    {
      "id":            "4d1b7a90-...",
      "payoutId":      "0c7d41e2-...",
      "settlementId":  "5b2e9f80-...",
      "lineType":      "Fee",
      "description":   "Fees: Monthly fee October 2026, Refund fee (2)",
      "amount":        -25.20,
      "currency":      "USD",
      "paymentId":     null,
      "captureId":     null,
      "refundId":      null,
      "fee":           null,
      "topUpId":       null,
      "feeCharges": [
        { "id": "5d2e8c1a-...", "type": "Monthly", "amount": 25.00, "currency": "USD",
          "paymentId": null, "refundId": null, "periodStart": "2026-10-01T00:00:00Z" },
        { "id": "7a01f3c4-...", "type": "Refund", "amount": 0.10, "currency": "USD",
          "paymentId": "3c0f9a14-...", "refundId": "e81c55d2-...", "periodStart": null },
        { "id": "7a01f3c5-...", "type": "Refund", "amount": 0.10, "currency": "USD",
          "paymentId": "9b4e02aa-...", "refundId": "c20d1e77-...", "periodStart": null }
      ],
      "referenceId":   null,
      "customerName":  null
    }
  ]
}
lineType Meaning
Capture A sale's net proceeds credited toward this deposit.
Refund A refund debited from this deposit.
Chargeback A chargeback debited from this deposit.
ChargebackReversed A reversed chargeback credited back toward this deposit.
Fee Our separately charged fees (monthly, chargeback, refund, ACH verification, ACH return, notice of change, settlement, Amex Direct, and chargeback recoveries), collected from the balance in one transfer and listed in feeCharges. Negative; positive when a collected fee or recovery is returned.
TopUp A bank top-up: a debit of the merchant's bank account into their balance, when the balance couldn't cover what they owed.
Other Any other movement that contributed to this deposit.
Field Description
payoutId The deposit this line was paid in: the id of its List Payouts item.
settlementId As on the deposit. See Reconciling a settlement.
paymentId The payment this line belongs to, as on Get Payment and the payment webhooks.
captureId On a Capture line, the capture it is (a payment can be captured in several parts). null for a payment captured in the same step as its authorization, which has no separate capture.
refundId On a Refund line, the refund it is. The line's amount is the whole refund: our fee on the original sale isn't returned, and our refund fee is collected separately (a Fee line).
fee On a Capture line, our fee on that capture, already taken out of amount. For cost-plus merchants, the card network and processing cost is deducted separately and is not included.
feeCharges On a Fee line, every fee its transfer collected, which add up to the line's amount: as in List Separately Charged Fees, each with its type, amount, the paymentId/refundId it is for, and periodStart for a monthly fee. null on other lines.
topUpId On a TopUp line, the bank top-up it is.

paymentId, referenceId and customerName are null when the line couldn't be matched back to one of your own payments (rare). 404 if there is no deposit with that payoutId or at that exact payoutDate; 400 if neither is given.

A deposit can have an empty items list: the second of two transfers, as under Reconciling a settlement.


Get Payout Summary

Aggregate totals for a date range — the same figures shown on the portal's Payouts page.

GET /v1/payouts/summary

Query parameters

Parameter Type Required Description
from ISO 8601 date Yes Start of range.
to ISO 8601 date Yes End of range.

Response 200

{
  "depositsAmount":    13173.96,
  "depositsCount":     1,
  "transactionCount":  6,
  "chargebacksAmount": 0,
  "refundsAmount":     145.00,
  "feesAmount":        25.20,
  "topUpsAmount":      0,
  "totalDeductions":   170.20,
  "netPayout":         13173.96
}

feesAmount is our separately charged fees collected from the balance in the range, less any returned; topUpsAmount is bank top-ups in. totalDeductions is refunds, chargebacks and those fees. transactionCount counts sales, refunds and chargebacks, not fee collections or top-ups.

A chargeback or refund can be booked in a different deposit period than its original sale — totalDeductions and the change in depositsAmount may not tie out exactly for a given range for that reason; this is normal payout-reconciliation timing, not a data error.


Get Payout Transfer Status

The live status of each recent payout to the merchant's bank account — whether it is on its way, has been sent, or failed.

GET /v1/payouts/transfer-status

Use this to answer "is my latest payout on its way?" or to spot a payout that didn't arrive. The endpoints above are built from the payout report, which lists only deposits that have already settled, so a payout in progress or one that failed never appears there; this feed shows both, as the status changes. It is a recent-activity feed rather than a history: no date filter, most recently updated first. There is no field linking an entry here to a deposit in List Payouts.

Query parameters

Parameter Type Required Description
page integer No Page number (1-based).
pageSize integer No Results per page (default 25).

Response 200

{
  "total": 1,
  "page": 1,
  "pageSize": 25,
  "items": [
    {
      "id":           "5e8d2c1a-7b4f-4e3a-9d2c-0a1b2c3d4e5f",
      "transferId":   "EXAMPLETRANSFER01",
      "status":       "Booked",
      "amount":       13173.96,
      "currency":     "USD",
      "reference":    "EXAMPLE-PAYOUT-REF",
      "reason":       "approved",
      "reasonDetail": null,
      "createdAt":    "2026-10-08T07:00:12Z",
      "updatedAt":    "2026-10-08T07:02:40Z"
    }
  ]
}
status Meaning
Received / Authorised In progress.
Booked Sent to the bank account.
Refused / Failed / Returned / Cancelled The money did not reach the bank account. reason, and reasonDetail when the bank gave one, say why.

transferId stays the same for one payout across every status change. createdAt is when Foundry first saw the payout; updatedAt is its latest status change.


Export Payouts (CSV)

GET /v1/payouts/export

Query parameters

Parameter Type Required Description
from ISO 8601 date Yes Start of range.
to ISO 8601 date Yes End of range.
mode string No "payouts" (default) — one row per deposit. "details" — one row per transaction/refund/chargeback, with its parent deposit's date/amount repeated on each row.
search string No As on List Payouts: only deposits containing a line whose order/invoice or PSP reference matches.
minAmount decimal No As on List Payouts: only deposits with a net amount of at least this.
maxAmount decimal No As on List Payouts: only deposits with a net amount of at most this.

Response 200 — text/csv, filename payouts-{mode}.csv. A from–to range longer than a year returns 400; export longer periods in parts.

List Payouts Across All Your Merchants

GET /v1/partner/payouts?from=2026-10-01&to=2026-10-31&page=1&pageSize=100
GET /v1/partner/payouts/lines?from=2026-10-01&to=2026-10-31&page=1&pageSize=500

Partner-scoped keys only, and no X-Merchant-Id: these cover every merchant linked to your key in one paged call, so a payout job doesn't need a call per merchant. Merchants that have been deactivated are included, since their deposits and chargebacks can keep arriving after they stop trading and the per-merchant routes refuse them. from/to are dates, inclusive, as on List Payouts.

/v1/partner/payouts returns the deposits (pageSize up to 500), each with the fields of a List Payouts item (id and settlementId included) plus merchantId and externalMerchantId (your own ID for the merchant, when you've set one). /v1/partner/payouts/lines returns every line of those deposits (pageSize up to 1,000), each with the fields of a Get Payout Detail line plus merchantId, externalMerchantId and payoutDate. Both return { total, page, pageSize, items }; page until you've read total items.

Get Fee Summary

GET /v1/fee-reports/summary?from=2026-10-01&to=2026-10-31&bucket=day

Partner-scoped keys only (with X-Merchant-Id): one merchant's processing economics from the payment processor's settlement report, totalled for the range and in day, week or month buckets. Dated on the settlement date, so it lags a sale by a day or two and won't tie out exactly to payouts for the same range.

Response 200

{
  "grossVolume": 48210.00, "interchange": 702.15, "schemeFees": 61.30, "totalFees": 18.40,
  "foundryMarkup": 482.10, "netAmount": 46946.05,
  "buckets": [
    { "bucketStart": "2026-10-01", "grossVolume": 1520.00, "interchange": 22.10, "schemeFees": 1.95,
      "totalFees": 0.60, "foundryMarkup": 15.20, "netAmount": 1480.15 }
  ]
}

interchange and schemeFees are the card networks' costs, totalFees the processor's own fees, foundryMarkup our markup (null when no line in the range carries one) and netAmount what the merchant received. Merchant keys get 403.

List Separately Charged Fees

Fees that aren't taken out of a sale: they're collected from the merchant's balance by a separate transfer, which appears in the payout it reduced as a Fee line listing them. Newest first, including those not collected yet.

GET /v1/settings/fee-charges?page=1&pageSize=50

pageSize is at most 100. A partner-scoped key sends X-Merchant-Id as usual; there's one call per merchant.

Response 200

{
  "total": 3, "page": 1, "pageSize": 50,
  "items": [
    {
      "id":            "5d2e8c1a-...",
      "type":          "Monthly",
      "amount":        25.00,
      "currency":      "USD",
      "paymentId":     null,
      "refundId":      null,
      "periodStart":   "2026-10-01T00:00:00Z",
      "status":        "Booked",
      "reason":        null,
      "attempts":      1,
      "lastAttemptAt": "2026-10-02T06:00:11Z",
      "abandonedAt":   null,
      "createdAt":     "2026-10-02T06:00:04Z",
      "payoutId":      "0c7d41e2-...",
      "settlementId":  "5b2e9f80-..."
    }
  ]
}
Field Description
type Monthly, Chargeback, ChargebackRecovery (a chargeback amount paid on the merchant's behalf and collected back), AchReturn, UnauthorizedAchReturn, NoticeOfChange, AchVerification, Refund or Settlement.
paymentId / refundId The payment or refund the fee is for, when it's for one. periodStart is set for a monthly fee.
status null until a collection is attempted, Booked once collected.
reason / attempts / lastAttemptAt / abandonedAt Why the last attempt didn't collect (for example, not enough balance), how many tries so far, and when collection was given up.
payoutId / settlementId The payout the fee came out of, once collected and reported (usually the next day), as on List Payouts. null until then.

Embedded Components

Foundry hosts small, self-contained UI components you can drop into your own page in an iframe: card entry, wallet management, transaction history, a merchant dashboard, and onboarding status. Your server mints a short-lived, single-purpose token; your page iframes it.

Visual previews of each component, the FoundryEmbed JS SDK and its callbacks are in PARTNER_GUIDE.html under "Embeddable UI Components". This section is the endpoint contract.

Create an Embed Session

POST /v1/embed-sessions
Authorization: ApiKey <your-api-key>
X-Merchant-Id: <merchant-uuid>     # partner-scoped keys; see "Merchant scoping" below
{
  "type": "dashboard",
  "returnUrl": "https://yourapp.example.com/settings/payments",
  "expiresInSeconds": 3600
}

Response 200

{
  "token": "em_9Kd2s...",
  "type": "dashboard",
  "expiresAt": "2026-09-21T18:30:00Z"
}

The token is what you iframe:

<iframe src="https://checkout.pay.emun1.com/embed/em_9Kd2s..."></iframe>

A token is scoped to one type and one subject — a transactions token for ORD-001 cannot be used to view any other reference, and an add-card token cannot list or delete the buyer's existing cards. Mint a fresh one each time a page loads a component rather than caching.

Session types

type Renders Also required
transactions Payment history for one of your orders referenceId
payment-methods The buyer's stored cards, with remove buyerEmail
add-card Card entry only — cannot list or delete existing cards buyerEmail
dashboard Merchant summary, plus Adyen's hosted Transactions, Disputes, Payouts and Reports overviews returnUrl
onboarding-status Where the merchant is in Adyen onboarding, what they still need to do, and a link to continue returnUrl
select-card Rep mode: the buyer's saved cards, from their whole wallet, for a sales rep to pick one for an order with the buyer not present paymentRequestId

The Adyen overviews inside dashboard render only once that merchant's balance account is fully connected. Before then the summary shows and the overviews do not — expected for a merchant still onboarding, not a failure.

Fields

Field Required Description
type Yes One of the six above.
paymentRequestId For select-card A pending, manual-capture payment request of yours. Its buyerEmail names the wallet.
referenceId For transactions Your order or invoice number.
buyerEmail For payment-methods and add-card The buyer whose wallet this session acts on.
externalBuyerId No payment-methods and add-card only, with a secret key (a publishable key is refused). See Your Own Buyer IDs.
returnUrl For dashboard and onboarding-status Where the merchant lands after Adyen's hosted onboarding. Set here at mint time rather than trusted from the browser.
expiresInSeconds No Token lifetime. Default 3600 (1 hour).

Rep mode (select-card)

For a sales rep entering an order in your CRM who picks one of the buyer's saved cards, with the buyer not present. The rep sees every active, unexpired card in the buyer's wallet, whichever vendor or platform it was saved through (bank accounts aren't offered). Picking one attaches it to the payment request exactly as paymentMethodId on Create a Payment Request does for manual capture: the request turns Collected, you receive payment_request.completed, and you authorize and capture as usual. That's a charge of a card on file, covered by the buyer's storage consent; no CVC or 3DS is asked of the rep, and no card reference needs to be kept on your side.

Merchant scoping

X-Merchant-Id is required for transactions, dashboard and onboarding-status, which are all about one merchant's own data.

It is optional for add-card and payment-methods. A stored card is a platform-wide wallet entry rather than something scoped to whichever merchant tokenized it, so wallet management does not have to happen inside an active order with a particular merchant. Supply the header anyway if you have it — it is still ownership-checked when present.

Which keys can mint which types

Key May mint
Merchant-scoped secret key All six, for that merchant
Partner-scoped secret key All six, for any merchant linked to you
Publishable key add-card only, and only from a registered origin

A publishable key is the only key type meant to be called from the browser, and add-card is the only session it can mint. That is deliberate rather than an omission: a payment-methods session reads and deletes a buyer's whole wallet, and both session types are minted against a buyerEmail supplied by the caller. Your backend knows which user is logged in; a key that ships in your page source does not. See Publishable Keys above.

expiresInSeconds is silently capped at 3600 for a publishable key.

Errors

Status When
400 type not one of the six; a required field for that type missing; X-Merchant-Id missing for a type that needs it
404 select-card: no payment request with that id for this merchant
409 select-card: the request can't take a card now. code says why: payment_request_not_pending, payment_request_expired, immediate_capture_unsupported, ach_unsupported, billing_address_required (CAD), not_a_payment_request
403 Publishable key asked for any type but add-card, or its Origin is missing or not registered
403 Partner-scoped key naming a merchant not linked to it
{ "error": "referenceId is required for type 'transactions'" }

Shared Object Schemas

Billing Address

{
  "street":          "123 Main St",
  "houseNumberOrName": "Suite 400",
  "city":            "Chicago",
  "stateOrProvince": "IL",
  "postalCode":      "60601",
  "country":         "US"
}

All fields optional; any provided are forwarded to the card network for AVS.

Line Items

Array of objects for Level 2/3 interchange qualification. Amounts in cents.

[
  {
    "id":                   "SKU-001",
    "description":          "Widget A",
    "quantity":             2,
    "amountExcludingTax":   25000,
    "amountIncludingTax":   25000,
    "taxAmount":            0,
    "taxPercentage":        0,
    "upc":                  "012345678905",
    "brand":                "Acme",
    "imageUrl":             "https://yourapp.example.com/images/widget-a.png",
    "productUrl":           "https://yourapp.example.com/products/widget-a",
    "commodityCode":        "44101800",
    "productCode":          "INTERNAL-123",
    "discountAmount":       0,
    "unitOfMeasure":        "EA",
    "unitPrice":            12500
  }
]

id through productUrl are forwarded as-is on Adyen's own lineItems[] array (shown on invoices and required by a few payment methods). Do not send commodity — Adyen rejects that field name outright with a 400; use commodityCode instead.

commodityCode, productCode, discountAmount, unitOfMeasure, and unitPrice are for Level 2/3 commercial-card interchange enrichment specifically — they're not valid on Adyen's lineItems[] array, so Foundry forwards them separately as part of the enhanced scheme data sent with the real authorization (see Enhanced Scheme Data). quantity, description, and amountIncludingTax are reused for that same purpose automatically — no need to repeat them anywhere else.

Any other field you include is stored (visible in the portal and via GET /v1/payment-requests/{id}) but not sent to Adyen in any form.


Enhanced Scheme Data

Optional order-level fields for Level 2/3 interchange enrichment — freight, duty, tax, and shipping-destination details that apply to the whole order rather than one line item. Sent only with the real authorization — when you authorize a "manual" payment, or within the checkout itself for an "immediate" card payment — never when a "manual" request's card is collected, since no real charge happens then. It qualifies card interchange only; it has no effect on ACH.

{
  "dutyAmount":              500,
  "freightAmount":           750,
  "totalTaxAmount":          2000,
  "orderDate":               "010624",
  "shipFromPostalCode":      "60601",
  "destinationCountryCode":  "USA",
  "destinationPostalCode":   "10001",
  "destinationStateProvinceCode": "NY"
}

All fields optional; all amounts in cents. orderDate uses ddMMyy format. The purchase-order reference (customerReference) is set from the payment request: its customerReference if you sent one, otherwise referenceId. Adyen requires it for Level 2/3, limits it to 25 ASCII characters, and drops all Level 2/3 data on a payment where it is missing or breaks that rule. A referenceId longer than 25 characters is left out rather than sent invalid, which still means no Level 2/3 data: set the payment request's customerReference instead (not here).

Any other fields you include are stored but not sent to Adyen.

How far your Level 2/3 data qualifies

POST /v1/payment-requests and GET /v1/payment-requests/{id} return a levelTwoThree object reporting how far the request's data qualifies for lower card interchange. Incomplete data is never refused; this tells you what to add. Malformed data is refused (below).

"levelTwoThree": {
  "level": "level2",
  "missingForLevel2": [],
  "missingForLevel3": ["line 1: commodityCode, unitOfMeasure"],
  "problems": ["orderDate: \"2026-10-17\" isn't a ddMMyy date"]
}
Field Meaning
level level3, level2, none, or notApplicable (an ACH-only request)
missingForLevel2 Absent for Level 2, which needs a valid customerReference and a totalTaxAmount above zero
missingForLevel3 Absent for Level 3, which needs a valid customerReference, a totalTaxAmount (zero allowed) and line items each carrying productCode, description, quantity, unitOfMeasure, commodityCode, amountIncludingTax and unitPrice
problems Always empty on a request you create: malformed values are refused (below). May be non-empty only on requests created before that rule

Level 2/3 applies to a card captured in full, or to a partial capture that sends its own lines (below); the payment works either way. Destination and ship-from fields also matter for a US corporate card shipping physical goods, but don't affect level, since the card isn't known when the request is created.

Level 2/3 on partial captures

A payment request's line items describe the whole order, so a partial capture (one shipment of several) can't use them: Visa requires a capture's line totals plus freight plus tax to equal the amount captured. Optionally, send the lines this capture covers, with its freight and tax, on POST /v1/payments/{id}/capture or each batch-capture item:

{
  "amount": 412.50,
  "shipmentReference": "SHIP-2",
  "lineItems": [
    { "productCode": "CHAIR-UPH-4PK", "description": "Dining Chair 4pk", "quantity": 2,
      "unitOfMeasure": "EA", "commodityCode": "56101500", "unitPrice": 18750, "amountIncludingTax": 37500 }
  ],
  "enhancedSchemeData": { "freightAmount": 3750, "totalTaxAmount": 0 }
}

Malformed Level 2/3 data is refused. POST /v1/payment-requests returns 400 with code: "invalid_level_two_three" when any lineItems or enhancedSchemeData value is malformed, naming each one in fields (and in error):

{
  "error":  "Invalid Level 2/3 data: orderDate: \"2026-10-17\" isn't a ddMMyy date; line 1 quantity: \"two\" isn't a whole number",
  "code":   "invalid_level_two_three",
  "fields": ["orderDate: \"2026-10-17\" isn't a ddMMyy date", "line 1 quantity: \"two\" isn't a whole number"]
}

Malformed means: an amount, quantity or unit price that isn't a whole number (cents), an orderDate not in ddMMyy, a code or description that isn't text, a line description over 26 characters, lineItems that isn't an array (or a line that isn't an object), or enhancedSchemeData that isn't an object. The lineItems and enhancedSchemeData shapes are checked on ACH requests too.


Webhooks

You can configure multiple webhook endpoints per merchant — each event fans out to every active endpoint, with an identical payload and its own independent signature. See the Partner Guide's "Webhooks" section for how to add/manage endpoints.

Envelope

All webhooks share a common envelope:

{
  "eventType":  "payment_request.completed",
  "createdAt":  "2024-06-01T12:34:56Z",
  "merchantId": "3f2a9c1e-5b7d-4e8a-9c21-7d4e5f6a8b90",
  "data":       { }
}

merchantId is which of your merchants the event belongs to — load-bearing on a partner-scoped endpoint, whose stream carries every linked merchant. It sits inside the envelope, not in a header, so that it is covered by the signature.

Event Payloads

payment_request.completed

{
  "referenceId":          "ORD-20240601-001",
  "paymentRequestId":     "01a0d890-c74d-7b8c-8089-69ed35729c08",
  "status":               "collected",
  "paymentMethodId":      "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "last4":                "4242",
  "brand":                "mc",
  "holderName":           "Jane Doe",
  "expiryMonth":          8,
  "expiryYear":           2027,
  "paymentMethodVariant": null
}

paymentRequestId identifies the specific payment request this event reports on. It matters when more than one request exists against the same referenceId — which happens whenever an order is re-raised after its card was collected, since the amount is fixed at collection time. Attribute on paymentRequestId rather than on referenceId alone, and you never need a lookup to work out which request a webhook is about.

Not sent for a request created already Collected from a paymentMethodId.

payment_request.expired

{
  "referenceId":      "ORD-20240601-001",
  "paymentRequestId": "01a0d890-c74d-7b8c-8089-69ed35729c08",
  "status":           "expired",
  "expiresAt":        "2024-06-01T15:00:00Z"
}

Sent once, about an hour after expiresAt (a buyer who opened checkout just before then can still finish), so it is final. Not sent for card-enrollment or card-update requests.

card.enrolled

{
  "buyerReference":       "BUYER-4521",
  "paymentRequestId":     "01a0d890-c74d-7b8c-8089-69ed35729c08",
  "buyerEmail":           "buyer@example.com",
  "paymentMethodId":      "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "last4":                "1111",
  "brand":                "visa",
  "holderName":           "Jane Doe",
  "expiryMonth":          3,
  "expiryYear":           2030,
  "paymentMethodVariant": null
}

card.updated

{
  "referenceId":          "ORD-20240601-001",
  "paymentRequestId":     "01a0d890-c74d-7b8c-8089-69ed35729c08",
  "paymentMethodId":      "7c4a9e1d-...",
  "last4":                "4242",
  "brand":                "mastercard",
  "holderName":           "Jane Doe",
  "expiryMonth":          8,
  "expiryYear":           2027,
  "paymentMethodVariant": null
}

holderName, expiryMonth, expiryYear, and paymentMethodVariant (Adyen's premium/commercial card sub-type, e.g. "visabusiness") are consistent across all three card-carrying events — none of these are PCI-sensitive on their own (no PAN, no CVV, no track data); Foundry never stores the full card number. paymentMethodVariant is null when Adyen doesn't report one for the card.

payment.capture_updated

Fired when Adyen settles a capture, confirming or refusing it. This is the event that tells you a capture you were told succeeded did not.

{
  "eventType": "payment.capture_updated",
  "createdAt": "…",
  "merchantId": "3f2a9c1e-5b7d-4e8a-9c21-7d4e5f6a8b90",
  "data": {
    "referenceId":       "ORD-20240601-001",
    "paymentId":         "01a0d892-...",
    "captureId":         "9b1f3e77-...",
    "status":            "failed",
    "amount":            90.50,
    "currency":          "USD",
    "shipmentReference": "SHIP-2",
    "failureReason":     "Authorisation already cancelled",
    "paymentStatus":     "PartiallyCaptured",
    "capturedAmount":    55.50,
    "remainingAmount":   160.50
  }
}

status is "confirmed" or "failed". capturedAmount, remainingAmount and paymentStatus describe the payment after this capture was accounted for, so a single message tells you both what changed and what is now true — you don't need to call back for the totals.

failureReason carries the processor's own wording when status is "failed", and is null otherwise. It is passed through verbatim rather than mapped to a Foundry code, so it matches what support sees when asked to trace the capture. It is also returned on each capture in GET /v1/payments/{id}. Treat it as a human-readable diagnostic, not a stable enum — the set of values is the processor's, and it can change without notice.

A repeated partial capture against one authorization needs to be enabled by the processor. Without it, the unclaimed balance is released after the first capture and every later capture against that authorization fails — the first succeeds, the rest do not, regardless of amount. If you see that pattern, it is configuration rather than a per-transaction decline; tell us and we will raise it.

Both outcomes fire. A stream carrying only failures can't distinguish "confirmed" from "not heard back yet".

POST /capture returning 200 is not the end of the story. A 200 means Adyen accepted the capture request and Foundry recorded it; Adyen settles it asynchronously and can still refuse it minutes or hours later. The capturedAmount in a capture response is therefore a running total of captures accepted so far, not captures that have settled.

If you compute shipment increments as (value shipped) − (already captured), subtract capturedAmount, not a sum of settled captures. capturedAmount counts every capture that has not been refused, including one still in flight, so an increment computed from it cannot re-capture money that is already on its way. It also self-corrects: when Adyen refuses a capture, that amount drops back out, and the next shipment's increment naturally re-captures it with no special handling.

Summing only confirmed captures would exclude an in-flight one and double-capture inside the settlement window — the worse direction to fail in.

payment.charged_back

Fired when the processor reports a dispute on a captured payment: a card chargeback, or a returned ACH debit. The payment reads ChargedBack from the first of these.

{
  "eventType": "payment.charged_back",
  "createdAt": "…",
  "merchantId": "3f2a9c1e-5b7d-4e8a-9c21-7d4e5f6a8b90",
  "data": {
    "referenceId":    "ORD-20240601-001",
    "paymentId":      "01a0d892-...",
    "kind":           "chargeback",
    "stage":          "chargeback",
    "amount":         312.50,
    "currency":       "USD",
    "reasonCode":     "10.4",
    "reason":         "Other Fraud-Card Absent Environment",
    "paymentStatus":  "ChargedBack",
    "capturedAmount": 312.50
  }
}
Field Description
kind chargeback for a card dispute, ach_return for a returned bank debit.
stage notice (an early warning that may not become a chargeback), chargeback (the funds were taken back), or second_chargeback (a further stage of the same dispute). Each stage is sent once.
amount / currency The disputed amount.
reasonCode / reason The card network's reason code, or for ACH the return code (R01, R10, ...), with its description. Either can be null.

payment.chargeback_reversed

Fired when a chargeback is reversed in the merchant's favour. Same fields as payment.charged_back, with stage: "reversed"; the payment reads Captured again.

merchant.onboarded

Fired when a merchant completes Adyen onboarding and can transact.

{
  "merchantId":         "3f2a9c1e-5b7d-4e8a-9c21-7d4e5f6a8b90",
  "externalMerchantId": "MT-VENDOR-4410",
  "status":             "active"
}

merchant.action_required

Fired when onboarding moves to a state needing the merchant's attention, typically further documentation for Adyen.

{
  "merchantId":         "3f2a9c1e-5b7d-4e8a-9c21-7d4e5f6a8b90",
  "externalMerchantId": "MT-VENDOR-4410",
  "status":             "pending"
}

Both fire on every onboarding status change, so expect more than one per merchant. They are the only two that repeat merchantId inside data; every event carries it in the envelope, and the rest identify their subject by your referenceId or by buyerEmail. Both also carry externalMerchantId, your own ID for the merchant as set on it in Foundry, or null if none was set. A merchant working with two platforms can have a different ID on each: each platform's endpoints receive their own.

Capture statuses

GET /v1/payments/{id} returns a captures array whose entries each carry one of:

Status Meaning
Pending Adyen accepted the capture. It has not settled yet and can still be refused.
Confirmed Adyen settled it. confirmedAt is when.
Failed Adyen refused it. The amount is no longer in capturedAmount.

Use these to explain a number, not to compute one — capturedAmount is the figure to do arithmetic with, for the reason above. A capture sits in Pending for as long as Adyen takes to settle it.

Signature Verification

Every webhook includes X-Foundry-Signature: sha256=<hex-digest>.

import hmac, hashlib

def verify(secret: str, body: bytes, header: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, header)
bool Verify(string secret, byte[] body, string header)
{
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var digest = "sha256=" + Convert.ToHexString(hmac.ComputeHash(body)).ToLower();
    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(digest),
        Encoding.UTF8.GetBytes(header));
}

Respond with any 2xx within 10 seconds to acknowledge. Each event gets up to 3 delivery attempts per endpoint, about three minutes apart.

There are no webhook events for refunds or chargebacks — read refund status and a ChargedBack payment status from Get Payment or List Transactions.


Error Responses

{ "error": "Payment request not found or not in collected state" }
HTTP Status Meaning
400 Missing required field or invalid value.
401 Invalid or missing API key.
403 Key not permitted for this action — e.g. a publishable key outside add-card, or a merchant not linked to your partner account.
404 Resource not found.
409 Conflict — e.g. payment request already collected or expired, the merchant can't take payments yet (status: "OnboardingIncomplete", below), or the merchant has no usable fee structure (code: "split_configuration", below).
422 Payment could not be processed (e.g. declined).
429 Rate limit exceeded for your key. Retry after the number of seconds in the Retry-After header.
502 Upstream error from payment network. Retry after a short delay.

A few 403/404 responses have an empty body rather than an error field.

OnboardingIncomplete — merchant can't take payments yet

Create a Payment Request and Authorize (single and batch) are refused while the merchant hasn't finished onboarding — 409 on the single endpoints, a failed item in a batch:

{ "error": "This merchant hasn't completed onboarding yet", "status": "OnboardingIncomplete" }

Wait for the merchant.onboarded webhook. Captures, voids and refunds on existing payments are not affected.

split_configuration — merchant not configured for payments

A 409 carrying "code": "split_configuration" means the merchant has no usable fee structure, so Foundry cannot work out how to divide the payment between the merchant and the platform:

{ "error": "No fee rule matches this payment", "code": "split_configuration" }

Foundry refuses the payment rather than sending it without split instructions, because Adyen books an unsplit transaction entirely to the platform — the merchant would be paid nothing. It can be returned by payment creation, authorize and capture.

This is a merchant configuration problem, not a request problem — retrying the same call won't help, and the fix is on Foundry's side. Report it with the merchant's id and your referenceId.


Test Cards

Adyen's test platform requires these specific values — an arbitrary future expiry/CVV will not produce a successful authorization.

Card Number Expiry CVV
Visa 4111 1111 1111 1111 03/30 737
Mastercard 5555 5555 5555 4444 03/30 737

To simulate a decline or other outcome, use a success card above with one of these values as the cardholder name — Adyen's test environment inspects that field:

Cardholder name Result
DECLINED Generic decline
NOT_ENOUGH_BALANCE Insufficient funds
CARD_EXPIRED Expired card
FRAUD Suspected fraud
ISSUER_UNAVAILABLE Issuer unavailable

Full list: Adyen test result codes.

ACH testing: routing 011000138 or 121000358, any correctly formatted account number (e.g. 123456789).