Partner Integration Guide
Foundry Pay is a hosted payment service. Your platform creates a payment request, the buyer completes payment inside a Foundry-hosted iframe (card entry, stored cards, or ACH), and Foundry notifies you via webhook when the payment is captured. Card data never passes through your servers.
For a concise endpoint-by-endpoint reference see the Partner API Reference.
Quick Start
- Receive your API key from Foundry Pay (one key per vendor/merchant).
POST /v1/payment-requests→ receiveiframeUrl.- Show the iframe to the buyer.
- Receive the
payment_request.completedwebhook when payment is captured.
Base URL
| Environment | Base URL |
|---|---|
| Production | https://api.pay.emun1.com |
| Nonprod (testing) | https://nonprod.pay.emun1.com |
All requests require:
Authorization: ApiKey <your-api-key>
Content-Type: application/json
Authentication
API keys are per-merchant. Every request made with your key is automatically scoped to your merchant account — you do not need to pass a merchant ID.
API keys are generated in the Foundry Pay merchant portal under Settings → API Keys.
Creating a Payment Request
POST /v1/payment-requests
Request body
{
"referenceId": "ORD-20240601-001",
"buyerEmail": "buyer@example.com",
"amount": 312.50,
"currency": "USD",
"captureMode": "manual",
"acceptedPaymentMethod": "card",
"buyerCompany": "Acme Corp",
"returnUrl": "https://yourapp.com/checkout/return",
"billingAddress": {
"street": "123 Main St",
"houseNumberOrName": "Suite 400",
"city": "Chicago",
"stateOrProvince": "IL",
"postalCode": "60601",
"country": "US"
},
"lineItems": [
{
"id": "SKU-001",
"description": "Widget A",
"quantity": 2,
"amountExcludingTax": 25000,
"amountIncludingTax": 25000,
"taxAmount": 0,
"taxPercentage": 0,
"commodity": "widgets",
"upc": "012345678905",
"brand": "Acme",
"imageUrl": "https://yourapp.com/images/widget-a.png",
"productUrl": "https://yourapp.com/products/widget-a"
}
]
}
Fields
| Field | Required | Description |
|---|---|---|
referenceId |
Yes | Your order/invoice number. Returned in all webhooks. |
amount |
Yes | Payment amount in dollars (e.g. 312.50). |
buyerEmail |
Yes | Buyer's email address. Used as a shopper identifier so previously saved cards appear in the drop-in for returning buyers. |
currency |
No | ISO 4217 code. Defaults to "USD". "CAD" is also supported. |
captureMode |
No | "manual" (default) or "immediate". See Capture Modes. |
acceptedPaymentMethod |
No | "card", "ach", or omit to allow the buyer to choose. Omitting is only recommended with captureMode: "immediate". |
buyerCompany |
No | Buyer's company name, shown alongside the payment request in the portal. |
returnUrl |
No | Where the iframe redirects after payment on mobile. Not required for embedded desktop flows. |
billingAddress |
No | Pre-populated in the card entry form (buyer can edit) and forwarded to the card network for AVS and Level 3 data. All fields optional; street, houseNumberOrName, city, stateOrProvince, postalCode, country. |
lineItems |
No | Order line items for Level 3 interchange qualification. Amounts in cents (e.g. 25000 = $250.00). Your referenceId is automatically included as the purchase order reference for L2/L3 enrichment. Only id, description, quantity, amountIncludingTax, amountExcludingTax, taxAmount, taxPercentage, commodity, upc, brand, imageUrl, and productUrl are forwarded to Adyen — other fields are stored but not sent. |
Response
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"token": "abc123xyz...",
"iframeUrl": "https://checkout.pay.emun1.com/enter/abc123xyz...",
"status": "Pending",
"expiresAt": "2024-06-01T13:00:00Z"
}
Payment requests expire after 60 minutes. Create a new one if the buyer does not complete payment in time.
Showing the Payment UI
Desktop — embedded iframe
<iframe
src="https://checkout.pay.emun1.com/enter/{token}"
width="100%"
height="600"
frameborder="0"
allow="payment"
></iframe>
Poll GET /v1/payment-requests/{token} every few seconds and close/hide the iframe when
status changes to "Collected".
First-time storage consent screen. The first time a given buyer stores a payment method with your merchant account, the iframe shows a brief, non-skippable consent screen (a disclosure plus a checkbox) before the card/ACH entry form — Foundry stores the payment method on every transaction, so this can't be Adyen's own optional Drop-in checkbox. Later payments from that same buyer skip it. If you embed with a fixed height rather than sizing dynamically, account for this extra step when testing the first-time flow.
Mobile — full-page redirect
window.location.href = iframeUrl;
Set returnUrl in the payment request so the buyer is redirected back to your app after
payment completes. Add a query parameter to your return URL (e.g. ?foundryComplete=1) so
your page knows to poll for the final status on load.
Checking Payment Status
GET /v1/payment-requests/{token}
Returns the current status of the payment request.
{
"id": "3fa85f64-...",
"referenceId": "ORD-20240601-001",
"status": "Collected",
"completedAt": "2024-06-01T12:34:56Z"
}
| Status | Meaning |
|---|---|
Pending |
Waiting for buyer to complete payment. |
Collected |
Payment captured. Safe to fulfill. |
Expired |
60-minute window passed without payment. |
Cancelled |
Cancelled by the platform. |
Recommendation: Use webhooks as the primary completion signal and polling only as a fallback for local development or environments without a public webhook URL.
Webhooks
Foundry Pay sends a POST to your configured webhook URL when payment events occur.
Configure your webhook URL
Set it in the merchant portal under Settings → Webhook.
Envelope
Every webhook is wrapped in a common envelope:
{
"eventType": "payment_request.completed",
"createdAt": "2024-06-01T12:34:56Z",
"data": { … }
}
Events
payment_request.completed
Fired when a buyer completes card entry in the iframe (manual or immediate capture mode).
{
"eventType": "payment_request.completed",
"createdAt": "…",
"data": {
"referenceId": "ORD-20240601-001",
"status": "collected",
"paymentMethodId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
}
paymentMethodId is the Foundry-internal identifier for the stored card. You can pass it to
POST /v1/payments for future charges. It is not a card number or payment network token.
card.enrolled
Fired when a buyer completes a card enrollment (from POST /v1/card-enrollments).
{
"eventType": "card.enrolled",
"createdAt": "…",
"data": {
"buyerReference": "BUYER-4521",
"buyerEmail": "buyer@example.com",
"paymentMethodId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"last4": "1111",
"brand": "visa"
}
}
card.updated
Fired when a buyer completes a card refresh (from POST /v1/payment-requests/{id}/card-update).
The referenced payment request now has the new card stored and is ready to re-authorize.
{
"eventType": "card.updated",
"createdAt": "…",
"data": {
"referenceId": "ORD-20240601-001",
"paymentMethodId": "7c4a9e1d-...",
"last4": "4242",
"brand": "mastercard"
}
}
payment_request.expired
Fired when a payment request reaches its 60-minute expiry without being completed.
{
"eventType": "payment_request.expired",
"createdAt": "…",
"data": {
"referenceId": "ORD-20240601-001"
}
}
Verifying webhook signatures
Every webhook includes an X-Foundry-Signature header:
X-Foundry-Signature: sha256=<hex-digest>
Verify it with your webhook secret (shown once when you configure the URL — store it securely):
import hmac, hashlib
def verify_signature(secret: str, body: bytes, header: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)
bool VerifySignature(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));
}
Always use a constant-time comparison to prevent timing attacks. Reject requests where the signature does not match.
Webhook delivery
Foundry Pay retries failed webhook deliveries 3 times with exponential backoff. Respond with
any 2xx status to acknowledge receipt. If your endpoint is temporarily unavailable, delivery
will be retried over several minutes.
Capture Modes
"manual" (default — order checkout)
The iframe tokenizes the card or bank account with a zero-dollar authorization — this works the same way for both card and ACH. No money moves until your merchant account explicitly authorizes and captures the payment in the Foundry Pay portal.
Use this for order workflows where you want to review and fulfill before charging.
{
"referenceId": "ORD-001",
"buyerEmail": "buyer@example.com",
"amount": 312.50,
"captureMode": "manual",
"acceptedPaymentMethod": "card"
}
The same works for ACH — just set "acceptedPaymentMethod": "ach" (or omit it to let the buyer
choose either at payment time). The bank account is tokenized with no debit; your merchant account
authorizes and captures later, exactly like the card flow above.
Split-shipment multi-capture
For orders shipped in multiple stages, the full authorized amount can be captured in multiple partial captures — one per shipment. The payment moves to Partially Captured after each partial capture and becomes Captured once the full authorized amount is settled.
From the portal: click Capture on an authorized or partially captured payment, enter the amount for the current shipment, add an optional shipment reference, and confirm. Repeat for each subsequent shipment.
Via the batch-capture API: include amount and optionally shipmentReference per item:
{
"items": [
{ "referenceId": "ORD-001", "amount": 150.00, "shipmentReference": "SHIP-A" },
{ "referenceId": "ORD-002", "amount": 312.50 }
]
}
"immediate" (invoice payments)
The iframe performs authorization and capture in a single step. Money moves when the buyer submits — no portal action required. Use this for invoice payments or any flow where you want funds captured at the point of collection.
{
"referenceId": "INV-2024-005",
"buyerEmail": "buyer@example.com",
"amount": 875.00,
"captureMode": "immediate"
}
When captureMode is "immediate" and acceptedPaymentMethod is omitted, the drop-in
presents both card and ACH options. The buyer chooses at payment time — both are settled
immediately.
ACH direct debit
ACH follows the same two capture modes as card (see above) — "manual" (default, zero-dollar
tokenize; your merchant authorizes and captures later) or "immediate" (bank debit occurs within
the payment session, no portal action needed). Request ACH explicitly, or omit
acceptedPaymentMethod to let the buyer choose either:
{
"referenceId": "ORD-001",
"buyerEmail": "buyer@example.com",
"amount": 312.50,
"acceptedPaymentMethod": "ach"
}
Note: ACH settlement is asynchronous at the bank level (
resultCode: "Received"). For immediate capture, thepayment_request.completedwebhook fires as soon as the debit is initiated, not once it actually settles — actual bank settlement typically occurs within 1–3 business days, and chargebacks can occur up to 60 days after the debit. For manual capture, the same async settlement applies once your merchant account clicks Authorize and Capture.
Stored Payment Methods
When buyerEmail is provided, the buyer's previously saved cards appear in the drop-in for
one-click payment on return visits. The same email address must be passed consistently across
payment requests for the same buyer.
Stored payment methods are managed entirely within Foundry Pay — your platform never sees card numbers, CVVs, or payment network tokens.
Card Enrollment (Store a Card Without an Order)
Use card enrollment when you want to save a buyer's card on file before any specific order or invoice exists — for example, when onboarding a new account or when a sales rep wants to set up payment for a buyer in advance.
POST /v1/card-enrollments
Request body
{
"buyerEmail": "buyer@example.com",
"buyerReference": "BUYER-4521",
"buyerCompany": "Acme Wholesale",
"billingAddress": {
"street": "123 Main St",
"city": "Chicago",
"stateOrProvince": "IL",
"postalCode": "60601",
"country": "US"
}
}
| Field | Required | Description |
|---|---|---|
buyerEmail |
Yes | Buyer's email address. Used as the shopper identifier for returning buyers. |
buyerReference |
No | Your own buyer or account ID. Returned in the card.enrolled webhook for correlation. |
buyerCompany |
No | Company name (for display purposes). |
billingAddress |
No | Pre-populated in the card entry form; buyer can edit. |
returnUrl |
No | Where the iframe redirects after completion (mobile flow). |
Response
{
"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 in the same way as a payment request. When the buyer submits
their card, you receive a card.enrolled webhook containing the paymentMethodId. Use that ID
in future POST /v1/payments calls to charge the card.
Enrollment sessions expire after 60 minutes.
Card Refresh (Update a Card After Authorization Failure)
When an authorization attempt fails because the stored card has expired or been declined, you can send the buyer a link to enter a replacement card. Once submitted, the new card is automatically associated with the original payment request so you can immediately retry authorization.
POST /v1/payment-requests/{id}/card-update
| Field | Required | Description |
|---|---|---|
returnUrl |
No | Where the iframe redirects after completion (mobile flow). |
Response
{
"id": "9b1b3a2e-...",
"token": "xyz789...",
"iframeUrl": "https://checkout.pay.emun1.com/enter/xyz789...",
"expiresAt": "2024-06-01T14:00:00Z"
}
Send the iframeUrl to the buyer via email or SMS. When the buyer submits the new card:
1. The payment request's stored card is replaced with the new one.
2. You receive a card.updated webhook.
3. Re-call POST /v1/payments (or batch-authorize) with the same reference — the new card is
used automatically.
Card update sessions expire after 60 minutes. Only Collected payment requests are eligible.
Embeddable UI Components
Foundry Pay provides embeddable components you can drop into your own application UI without writing any data-fetching or display code. They render inside a Foundry-hosted iframe (same cross-origin model as the payment iframe), so your platform stays out of PCI scope.
| Component | What it shows |
|---|---|
| Transaction history | All payments for a given referenceId — status, amount, card brand/last4, date |
| Payment method management | Stored cards for a given buyer — view, remove, and add a new card |
| Merchant dashboard | The merchant's full dashboard — captured/net volume, balance, daily volume chart, payment method mix, and the transactions/disputes/payouts/reports components |
| Onboarding status | The merchant's onboarding progress, and a button to (re)launch Adyen's hosted onboarding if it isn't finished |
How it works
- Your server calls
POST /v1/embed-sessions(with your API key) and receives a short-lived embed token. - Your frontend passes that token to the JS SDK — the API key never touches the browser.
- The SDK creates the iframe and handles resize and event forwarding automatically.
Your server ──► POST /v1/embed-sessions ──► { token: "em_abc..." }
Your browser ──► FoundryEmbed.mount('#el', 'em_abc...')
└─ <iframe src="https://checkout.pay.emun1.com/embed/em_abc...">
Creating an embed session
POST /v1/embed-sessions
Authorization: ApiKey <your-api-key>
Transaction history session

{
"type": "transactions",
"referenceId": "ORD-20240601-001",
"expiresInSeconds": 3600
}
Payment method management session

{
"type": "payment-methods",
"buyerEmail": "buyer@example.com",
"expiresInSeconds": 3600
}
Merchant dashboard session

The dashboard also embeds Adyen's hosted Transactions, Disputes, Payouts, and Reports overviews below the summary shown above — omitted here since they render only once a merchant's balance account is fully connected.
{
"type": "dashboard",
"returnUrl": "https://yourapp.example.com/settings/payments",
"expiresInSeconds": 3600
}
Onboarding status session

{
"type": "onboarding-status",
"returnUrl": "https://yourapp.example.com/settings/payments",
"expiresInSeconds": 3600
}
returnUrl is where the merchant lands (in the new tab Adyen's hosted onboarding opens) once
they finish or exit onboarding — set it here, at mint time, rather than trusting it from the
browser on each "continue onboarding" click.
Fields
| Field | Required | Description |
|---|---|---|
type |
Yes | "transactions", "payment-methods", "dashboard", or "onboarding-status" |
referenceId |
Required for transactions |
Your order/invoice number to look up. |
buyerEmail |
Required for payment-methods |
Buyer whose stored cards to show. |
returnUrl |
Required for dashboard/onboarding-status |
Where the merchant lands after Adyen's hosted onboarding. |
expiresInSeconds |
No | Token lifetime in seconds. Default 3600 (1 hour). |
Response
{
"token": "em_abc123...",
"type": "transactions",
"expiresAt": "2024-06-01T13:00:00Z"
}
JS SDK
Include the SDK from the Foundry Pay CDN and call FoundryEmbed.mount:
<script src="https://checkout.pay.emun1.com/foundry-embed.js"></script>
<script>
const embed = FoundryEmbed.mount('#container', 'em_abc123...', {
onReady: () => console.log('component loaded'),
onCardAdded: () => refreshBuyerProfile(),
onCardRemoved: () => refreshBuyerProfile(),
onError: (msg) => showError(msg),
});
// To remove the component later:
embed.unmount();
</script>
The #container element can be any block-level element. The SDK inserts a borderless iframe
that resizes automatically to fit its content.
Options
| Option | Description |
|---|---|
onReady |
Called once when the component has fully loaded and rendered. |
onCardAdded |
Called when the buyer successfully adds a new card. |
onCardRemoved |
Called when the buyer removes a saved card. |
onError |
Called when the component fails to load or encounters a render error. |
Server webhook on card add
When a buyer adds a card through the payment method management component, Foundry Pay fires
the standard card.enrolled webhook to your configured URL. The
paymentMethodId in that webhook can be used immediately for POST /v1/payments charges —
no additional API call is needed.
Security
- Never create embed tokens in the browser.
POST /v1/embed-sessionsrequires your API key — always call it server-side and pass only the resulting token to your frontend. - Each token is scoped to a single type,
referenceIdorbuyerEmail, and merchant. A token forreferenceId: "ORD-001"cannot be used to view transactions for any other reference. - The component iframe is hosted on the Foundry Pay domain. Your JavaScript cannot read its contents across origins — the same cross-origin boundary as the payment iframe.
- Tokens expire. Generate a fresh one each time a user loads a page that contains the embed.
Refunds
Refunds can be issued from the Foundry Pay merchant portal (Transactions → select payment →
Refund), or via the API — POST /v1/payments/{id}/refund, in full or in part, using the same
API key as your other calls. See Refund in the API
Reference for the full request/response shape. Capture and Void are also available via API —
see Capture and Void —
as an alternative to triggering them manually from the portal.
Testing
Use these test cards in the iframe when pointed at the test environment:
| Scenario | Card Number | Expiry | CVV |
|---|---|---|---|
| Visa — success | 4111 1111 1111 1111 |
Any future date | Any 3 digits |
| Mastercard — success | 5500 0000 0000 0004 |
Any future date | Any 3 digits |
| Declined | 4000 0000 0000 0002 |
Any future date | Any 3 digits |
| Insufficient funds | 4000 0000 0000 9995 |
Any future date | Any 3 digits |
For ACH testing, use routing number 021000021 and any 10-digit account number.
PCI Scope
Foundry Pay is designed to keep your platform at SAQ-A (the lowest PCI DSS compliance tier):
- Card numbers and CVVs are entered directly into the Foundry-hosted iframe on a separate domain.
- Your servers never receive, transmit, or store card data.
- The only values your platform handles are:
referenceId(your own order number),amount, andbuyerEmail. - The
payment_request.completedwebhook contains no card data.
Errors
All errors return a JSON body with an error field:
{ "error": "Payment request not found or not in collected state" }
| HTTP Status | Meaning |
|---|---|
400 |
Bad request — missing required field or invalid value. |
401 |
Invalid or missing API key. |
404 |
Resource not found. |
409 |
Conflict — e.g. payment request already collected or expired. |
422 |
Unprocessable — payment could not be processed (e.g. declined). |
502 |
Upstream error from the payment network. Retry after a short delay. |
Support
Contact your Foundry Pay account manager or email support@markettime.com with your
referenceId for any payment-specific inquiries.