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.
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
https://api.foundrypay.io
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",
"returnUrl": "https://yourapp.com/checkout/return",
"billingAddress": {
"street": "123 Main St",
"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
}
]
}
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 |
No | 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". |
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". |
returnUrl |
No | Where the iframe redirects after payment on mobile. Not required for embedded desktop flows. |
billingAddress |
No | Forwarded to the card network for AVS and Level 3 data. |
lineItems |
No | Order line items for Level 3 interchange qualification. Amounts in cents (e.g. 25000 = $250.00). |
Response
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"token": "abc123xyz...",
"iframeUrl": "https://pay.foundrypay.io/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://pay.foundrypay.io/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".
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 Adyen 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 with a zero-dollar authorization. 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"
}
"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 payments are always immediate — the bank debit occurs within the payment session. You can request ACH explicitly or let the buyer choose when using immediate capture:
{
"referenceId": "ORD-001",
"buyerEmail": "buyer@example.com",
"amount": 312.50,
"acceptedPaymentMethod": "ach"
}
Note: ACH settlement is asynchronous at the bank level (
resultCode: "Received"). Thepayment_request.completedwebhook fires immediately when the debit is initiated. Actual bank settlement typically occurs within 1–3 business days. Chargebacks can occur up to 60 days after the debit.
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 Adyen 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 in Adyen. |
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://pay.foundrypay.io/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://pay.foundrypay.io/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.
Refunds
Refunds are initiated through the Foundry Pay merchant portal (Transactions → select payment → Refund). Refund API access for automated workflows is available on request.
Testing
Use these Adyen 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.
Full Adyen test card list: https://docs.adyen.com/development-resources/testing/test-card-numbers/
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@foundrypay.io with your
referenceId for any payment-specific inquiries.