Partner API Reference
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
- Every request must include an
X-Merchant-Idheader naming which merchant that specific call is for — the key alone no longer determines it. A request missing this header gets400; a request naming a merchant not linked to your partner account gets403. - One exception: discovering which merchants you have access to (below) doesn't require the header — you need it to find out what to put in the header in the first place.
- Or name the merchant by your own ID: send
X-External-Merchant-Id: <your id for the merchant>instead ofX-Merchant-Id. Foundry looks it up among your merchants only (the ID shown asexternalMerchantIdin the list below), then applies the same checks. Send one header or the other: both is400withcode: "conflicting_merchant_headers". An ID none of your merchants has is404withcode: "merchant_not_found". If two of your merchants somehow share an ID, the call is refused with409andcode: "ambiguous_external_merchant_id"; useX-Merchant-Idfor those.
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.enrolledwebhook, or from the buyer in your own records — go buyer-first, card-second. There is deliberately no lookup from apaymentMethodIdback 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.
- Only for vendors set up for CAD. Anyone else gets
400withcode: "currency_not_enabled". Ask us to enable a vendor; until then, send USD. - Card only. ACH is USD only:
acceptedPaymentMethod: "ach"with CAD is400withcode: "ach_requires_usd", and a CAD request withoutacceptedPaymentMethodoffers card only. - Charging a stored card in CAD needs a
billingAddress(paymentMethodIdwith no buyer at the checkout); without one it's422withcode: "billing_address_required". The checkout collects the address itself. - Amounts stay in CAD: the request, captures, refunds and the
payment.capture_updatedwebhook all use CAD, withcurrency: "CAD". The vendor is paid out in USD at Adyen's rate on the day of capture. - A capture can be refused for a moment with
502withcode: "exchange_rate_unavailable"if Adyen's rate service doesn't answer. Retry the capture shortly. - Any currency other than USD and CAD is
400withcode: "currency_not_supported".
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:
- The source payment request's stored card is replaced with the new one.
- A
card.updatedwebhook is delivered to your endpoint. - Re-call
POST /v1/paymentswith the samepaymentRequestId— the new card is used automatically.
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.
Send Payment Link
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:
- MarketTime Payments charges through HostedPCI. Use
POST /v1/paymentsand the capture, void, refund and batch endpoints exactly as for any other merchant. Foundry sends each call through HostedPCI to the vendor's own gateway, where the money settles. Results are final in the response: a capture comes backConfirmedand a refundProcessed, with no later webhook. Partial and multiple captures are accepted when the vendor's gateway accepts them. Cards only.POST /v1/payment-requests/{id}/gateway-tokenreturns409(hostedpci_processing_mode) for these merchants. - The merchant charges a Gateway Token itself (the default). Payment requests never reach
Authorized/Captured/etc. through Foundry:POST /v1/paymentsand the batch endpoints return409. You retrieve a Gateway Token for the collected card (below), keep it with the order and pass it to the merchant, who charges it on their own downstream gateway (Authorize.Net, Stripe, etc.) — through their ERP or the gateway's portal. Neither you nor Foundry processes the charge.
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.
- The key is scoped per merchant and per endpoint — the same key value can be reused across different endpoints (e.g. Authorize and Capture) without colliding.
- Reusing a key with a different request body is rejected with
409 Conflict— this catches a key accidentally reused for a genuinely different operation rather than silently returning the wrong cached response. - A second request with the same key while the first is still being processed also gets
409— wait for the first to complete (or time out) before retrying, rather than firing both at once. - Keys aren't cleaned up automatically yet — reusing a very old key still returns its original cached response rather than expiring. Use a fresh key (e.g. a UUID) per logical operation, not a constant.
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.
- Nothing captured yet: the authorization is cancelled and the payment becomes
Voided. - After one or more partial captures: only the uncaptured remainder is released. What was
captured stands, settles and stays refundable. The payment becomes
Captured, withreleasedAmount(what was released),releasedAt, andremainingAmount: 0. - Proof the hold was released:
releaseConfirmedAtis set when Adyen confirms the cancel (itsCANCELLATIONnotification), usually within minutes. Read it withGET /v1/payments/{id}. If Adyen refuses, the release is undone: the payment returns toPartiallyCapturedwith the remainder open to capture again.
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.
- A partial capture moves the payment to
PartiallyCaptured; submit the same reference again in a future batch to capture the next shipment. - If
amountexceeds the remaining uncaptured balance the item fails with a clear error.
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.
- The first time you send an
externalBuyerId, it's linked to the wallet of thebuyerEmailsent with it, creating the wallet if there's none. That wallet may already have other IDs of yours: if you have the same buyer under several IDs, each one sent with the buyer's email reaches the same wallet and cards. So never send one email for different buyers, such as a shared accounts-payable inbox. - After that, the
externalBuyerIdfinds that wallet. IfbuyerEmailis new to it, the wallet takes it as its current email, cards included, and keeps the old one too. Partners don't share email changes, so another partner still sending the old email reaches the same wallet and the same cards. buyerEmailis still required on every call.- Without
externalBuyerId, nothing changes: the wallet is found by email, as before.
| 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.
- Only for a manual-capture request (
captureModeomitted or"manual") that is still pending. For an immediate charge of a saved card, create the request withpaymentMethodIdandcaptureMode: "immediate". - The token expires with its payment request, whichever comes first.
- A new card can't be entered here: only the cardholder can enter one. Email a payment link sends
the buyer this request's link, as Send Payment Link does (resendable while the
request is pending), and raises
foundry:payment-link-sent(onPaymentLinkSent). Use a new card raisesfoundry:new-card-requested(onNewCardRequested), so you can open the checkout for a buyer who's present. - A pick sends
foundry:card-selected(onCardSelected) withpaymentRequestIdand the card summary.
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 }
}
- Lines and fields use the same names and units (cents) as on the payment request.
- The capture's lines replace the request's. Its
enhancedSchemeDatafields replace the request's same fields; the request'scustomerReference, destination, ship-from and order date still apply. - For Level 3,
amountIncludingTaxacross the lines plusfreightAmountplustotalTaxAmountmust equal the captureamount(here 375.00 + 37.50 + 0). Tax may be zero for Level 3; Level 2 needs it above zero. - It's optional. A partial capture without them still captures, with no Level 2/3. Lines that don't
add up are still captured; Adyen just won't apply Level 3. Only malformed values are refused, with
400and"code": "invalid_level_two_three"(a failed item inbatch-capture), before anything is sent. - On a full capture you may send them too, for example when the order changed before it shipped; without them, the payment request's data is used as before.
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).