How To: Implement ACH Payments

Developers can implement ACH payments with Coinflow via checkout link, SDK, or API, including API order-type enforcement and authorization records.

🏦 ACH Overview

ACH payments settle to your chosen settlement location (bank account or USDC).

Flow:

  1. Merchant submits transaction.
  2. User initiates ACH purchase.
  3. Coinflow processes ACH (≈3 business days).
  4. On completion, Coinflow settles to your configured settlement location.

Implementing ACH via SDK (React)

  1. Generate session key
$ curl -X GET https://api-sandbox.coinflow.cash/api/auth/session-key \
> -H "Authorization: YOUR_API_KEY" \
> -H "x-coinflow-auth-user-id: user123"
  1. Tokenize checkout parameters
$ curl -X POST https://api-sandbox.coinflow.cash/api/checkout/jwt-token \
> -H "Authorization: YOUR_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "subtotal": {"currency":"USD","cents":500},
> "email":"user@gmail.com"
> }'
  1. Implement <CoinflowPurchase> Component
<CoinflowPurchase
sessionKey="SESSION_KEY"
merchantId="MERCHANT_ID"
env="sandbox"
subtotal={{cents:500, currency:Currency.USD}}
email="user@email.com"
jwtToken="CHECKOUT_JWT_TOKEN"
onSuccess={(...args)=>console.log('Success', args)}
/>

Merchants settling to a contract may pass a transaction prop. See the advanced settlement docs for details.

<CoinflowPurchase> already shows an ACH-authorization checkbox before the purchase button for existing bank accounts. Checking it creates a one_time_purchase ACH authorization record (via the shopper’s session key) and the SDK forwards the resulting achAuthorizationId/orderType on checkout automatically — no additional props or integration changes needed. See ACH Order Type Enforcement and Authorization Records below for what this backs and when it’s enforced.

Implementing ACH via API

  1. Tokenize payment data
$curl -X POST https://api-sandbox.coinflow.cash/api/checkout/jwt-token \
> -H "Authorization: YOUR_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "subtotal":{"currency":"USD","cents":500},
> "email":"payer@gmail.com"
> }'
  1. Get totals
$curl -X POST https://api-sandbox.coinflow.cash/api/checkout/totals/MERCHANT_ID \
> -H "x-coinflow-auth-session-key: SESSION_KEY" \
> -d '{
> "subtotal":{"currency":"USD","cents":500},
> "jwtToken":"CHECKOUT_JWT_TOKEN"
> }'
  1. Add customer bank account
$curl -X POST https://api-sandbox.coinflow.cash/api/customer/v2/bankAccount \
> -H "Authorization: YOUR_API_KEY" \
> -H "x-coinflow-auth-user-id: user123" \
> -d '{
> "type":"checking",
> "email":"payer@gmail.com",
> "alias":"My Checking",
> "routingNumber":"0123456789",
> "account_number":"0987654321"
> }'

You can also have the customer link their own bank account through Coinflow’s bank authentication UI, which returns the same reusable token — see How To: Link a Bank Account for ACH Checkout.

  1. Get tokenized bank account
$curl -X GET https://api-sandbox.coinflow.cash/api/customer/v2 \
> -H "x-coinflow-auth-session-key: SESSION_KEY"

5.Send ACH checkout

$curl -X POST https://api-sandbox.coinflow.cash/api/checkout/ach/MERCHANT_ID \
> -H "x-coinflow-auth-session-key: SESSION_KEY" \
> -d '{
> "subtotal":{"cents":500},
> "jwtToken":"CHECKOUT_JWT_TOKEN",
> "token":"BANK_ACCOUNT_TOKEN"
> }'

token is required — it’s the customer’s bank account token, returned by Step 4 above at customer.bankAccounts[].token or by either bank-linking flow in How To: Link a Bank Account for ACH Checkout.

If your merchant account has allowedOrderTypes configured, add orderType and achAuthorizationId to this request body — see the next section. achAuthorizationId must reference an authorization record you created before this checkout call; there’s no way to attach one after the fact.

orderType is becoming required for all API integrations

Coinflow will soon require orderType on every ACH checkout for all merchants integrating via the API, regardless of whether allowedOrderTypes is configured. If you haven’t already, start passing orderType on this request now to avoid disruption when this takes effect.


ACH Order Type Enforcement and Authorization Records

Some ACH use cases — recurring debits, balance top-ups, limit orders, and fair-market-value fills — need a complete, auditable record of what the customer agreed to at the time they authorized the debit: the exact disclosure text they saw, when they consented, and the structured terms of the order (frequency and amount for a subscription, bid price and quantity for a limit order, and so on).

ACH Authorization Records capture that consent with Coinflow before the debit happens, and let you report status changes (like the order being fully filled or archived) — or revoke the record entirely — as things change afterward. Coinflow reconciles the authorization against the real customer and payment server-side, so the record can’t drift from what was actually charged.

Is this required for my integration?

orderType will soon be required on every ACH checkout for all API integrations — see the notice above. Separately, the allowedOrderTypes merchant setting restricts which orderType values are accepted, and configuring it (with at least one value) also requires a valid achAuthorizationId on every ACH checkout. It’s opt-in — contact your Coinflow integration representative to enable it for your account.

<CoinflowPurchase> handles the one_time_purchase case automatically: its existing ACH-authorization checkbox creates the authorization record (via the shopper’s session key) as soon as it’s checked, and the SDK forwards orderType/achAuthorizationId on checkout — no integration changes required. Recurring subscriptions, balance top-ups, limit orders, and fair-market-value fills remain API-only; <CoinflowPurchase> does not expose orderType/achAuthorizationId as props for those cases, so merchants needing them must integrate directly against /checkout/ach/{merchantId} (Step 1 below) as described in this section.

How It Works

1

Step 1: Create the Authorization Record

Before initiating checkout, create the authorization record from wherever you’re capturing consent — this must happen first, since the record has to exist before the checkout call that references it.

POST
/api/checkout/ach-authorization
1curl -X POST https://api-sandbox.coinflow.cash/api/checkout/ach-authorization \
2 -H "x-coinflow-auth-session-key: <apiKey>" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "renderedAuthorizationText": "string",
6 "consentTimestamp": "2024-01-15T09:30:00Z",
7 "sessionAuthRef": "string",
8 "idempotencyKey": "string",
9 "token": "string",
10 "orderType": "one_time_purchase",
11 "structuredTerms": {}
12}'
Response
1{
2 "createdAt": "2024-01-15T09:30:00Z",
3 "revoked": true,
4 "statusHistory": [
5 {
6 "eventType": "archived",
7 "occurredAt": "2024-01-15T09:30:00Z",
8 "recordedAt": "2024-01-15T09:30:00Z"
9 }
10 ],
11 "accountLastFour": "string",
12 "routingNumber": "string",
13 "renderedAuthorizationText": "string",
14 "consentTimestamp": "2024-01-15T09:30:00Z",
15 "sessionAuthRef": "string",
16 "merchant": "string",
17 "customerId": "string",
18 "idempotencyKey": "string",
19 "_id": "string",
20 "revokedAt": "2024-01-15T09:30:00Z",
21 "status": "archived",
22 "paymentId": "string",
23 "orderType": "one_time_purchase",
24 "structuredTerms": {}
25}

Save the _id from the response — that’s the achAuthorizationId you’ll pass to every checkout call this authorization backs, starting with Step 2.

Two ways to authenticate this call

  • Customer session key — pass the same session key your integration already uses for checkout. The customer is resolved from the session; omit customerId.
  • Admin-scoped API key — pass customerId explicitly in the body. Never expose your admin API key to a client.

routingNumber and accountLastFour are derived server-side from the bank account behind the token you send — they’re not part of the request.

idempotencyKey is required

Pass a string unique to this intended checkout (a UUID you generate is fine). Calling this endpoint again with the same key returns the original authorization unchanged instead of creating a duplicate, so retrying after a network failure is always safe.

Step 2: Complete ACH Checkout Referencing the Authorization

Pass the authorization’s _id from Step 1 as achAuthorizationId, along with orderType, on POST /checkout/ach/{merchantId}. If your account has allowedOrderTypes configured, orderType must be one of the configured values, and achAuthorizationId is required and must reference an authorization belonging to this customer and merchant.

POST
/api/checkout/ach/:merchantId
1curl -X POST https://api-sandbox.coinflow.cash/api/checkout/ach/merchantId \
2 -H "x-coinflow-auth-session-key: <apiKey>" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "subtotal": {
6 "cents": 1
7 },
8 "token": "string"
9}'
Response
1{
2 "paymentId": "string"
3}

On success, Coinflow links this call’s paymentId onto the authorization record as its founding payment.

Step 3 (Optional): Report Lifecycle Status Events

As the order’s status changes on your side — filled, archived, reopened, or expired without shipment — report it so the authorization record stays current for audits.

PUT
/api/checkout/ach-authorization/:authorizationId/status
1curl -X PUT https://api-sandbox.coinflow.cash/api/checkout/ach-authorization/authorizationId/status \
2 -H "Authorization: <apiKey>" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "eventTimestamp": "2024-01-15T09:30:00Z",
6 "eventType": "archived"
7}'

Every event you report is appended to the record’s status history — nothing is overwritten, so the full timeline of an authorization is preserved.

Step 4: Reference the Same Authorization for Later Debits

For order types with more than one debit against the same authorization — recurring subscriptions, balance top-ups, limit orders, and fair-market-value fills — pass the same achAuthorizationId from Step 1 on each subsequent ACH checkout call so it’s traceable back to the original consent record.

POST
/api/checkout/ach/:merchantId
1curl -X POST https://api-sandbox.coinflow.cash/api/checkout/ach/merchantId \
2 -H "x-coinflow-auth-session-key: <apiKey>" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "subtotal": {
6 "cents": 1
7 },
8 "token": "string"
9}'

Coinflow validates achAuthorizationId against the real authorization record at checkout time on every call, not just the founding one — it doesn’t re-link the payment once one is already set.

Step 5 (Optional): Revoke an Authorization

If the customer’s consent should no longer be honored — they withdraw authorization, or you determine the record was created in error — revoke it by ID. Once revoked, Coinflow rejects any further checkout call that references this achAuthorizationId, even if it was previously valid.

PATCH
/api/checkout/ach-authorization/:authorizationId/revoke
1curl -X PATCH https://api-sandbox.coinflow.cash/api/checkout/ach-authorization/authorizationId/revoke \
2 -H "Authorization: <apiKey>"

Revoking is permanent — there’s no way to un-revoke an authorization through this endpoint. Revoking an already-revoked authorization succeeds as a no-op and doesn’t change its original revokedAt timestamp.


Order Types and Structured Terms

structuredTerms is shaped by orderType — send only the fields for the order type you’re submitting.

Order TypeUse CasestructuredTerms Fields
one_time_purchaseA single, non-recurring debitnone — send {}
recurring_subscriptionFixed recurring billingfrequency (daily | weekly | monthly | yearly), subscriptionAmount
recurring_balance_top_upAuto-refill when a balance drops below a thresholdtopUpTriggerAmount, topUpMaxAmount
limit_orderAn order that fills at a specified price and quantity rangebidPrice, minQuantity, maxQuantity, maximumTotal
fair_market_valueAn order that fills at prevailing market value within a rangeminQuantity, minFairValue (optional), maxFairValue, maximumTotal

All amount fields (subscriptionAmount, topUpTriggerAmount, topUpMaxAmount, bidPrice, maximumTotal, minFairValue, maxFairValue) are cents objects, e.g. {"cents": 5000}.

Status Event Types

eventTypeMeaning
fully_filledThe order this authorization backs has been completely filled.
archivedThe authorization is no longer active.
reopenedA previously archived authorization is active again.
deadline_passed_no_shipmentA fulfillment deadline passed without the order shipping.

Error Handling

When creating an authorization with an admin-scoped API key (Step 1), the customerId you passed doesn’t match any customer on your merchant account.

Creating an authorization with an admin-scoped API key requires customerId in the body — there’s no session to resolve a customer from.

You passed a customer session key and a customerId in the body, and they disagree. Omit customerId when authenticating with a session key — the customer is already resolved from it.

Coinflow couldn’t find the bank account behind the token you sent when creating the authorization (Step 1). Double-check the token came from a completed bank-linking step for this customer.

Your merchant account has allowedOrderTypes configured, and the checkout call omitted achAuthorizationId. Create an authorization record (Step 1) before this checkout call and pass its _id.

The achAuthorizationId you passed at checkout doesn’t match any authorization record on your merchant account. Double-check you’re passing the _id returned from Step 1, not a paymentId.

The authorization record exists, but it was created for a different customer than the one checking out. Authorizations can’t be reused across customers.

Your merchant account has allowedOrderTypes configured, and the ACH checkout request either omitted orderType or sent a value outside the configured list. Pass one of the allowed order types.

Reporting a status event (Step 3) or revoking an authorization (Step 5) requires that an authorization record already exists for the given id, and that it belongs to your merchant account. Double-check you’re using the _id from Step 1, not a paymentId.

The achAuthorizationId you passed at checkout has been revoked (Step 5) and can no longer back any payment. Create a new authorization record if the customer wants to proceed.


Next Steps