Partner Integration Guide

Getting started with the Foundry Pay hosted payment service

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

  1. Receive your API key from Foundry Pay (one key per vendor/merchant).
  2. POST /v1/payment-requests → receive iframeUrl.
  3. Show the iframe to the buyer.
  4. Receive the payment_request.completed webhook 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, the payment_request.completed webhook 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

  1. Your server calls POST /v1/embed-sessions (with your API key) and receives a short-lived embed token.
  2. Your frontend passes that token to the JS SDK — the API key never touches the browser.
  3. 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

Transaction history component preview

{
  "type": "transactions",
  "referenceId": "ORD-20240601-001",
  "expiresInSeconds": 3600
}

Payment method management session

Payment method management component preview

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

Merchant dashboard session

Merchant dashboard component preview

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

Onboarding status component preview

{
  "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


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):


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.