Gating payment orders at the API boundary: an API key is not an intent
Payment APIs made treasury programmable, which was the right move. They also collapsed two different things into one credential: the authority to move money, and the intent to move this money.
Why is an API key not proof of payment intent?
Because it authenticates a caller, not a decision. Programmatic treasury platforms such as Modern Treasury accept a payment order from whoever holds the key, and there is no cryptographic distinction between a scheduled batch run and an attacker who has the same key on a compromised server.
- An API key authenticates a caller. It carries no information about whether a human intended this specific payment, and the API cannot distinguish a batch job from a curl command.
- IP allowlisting and velocity limits are perimeter and rate controls. They do not address a correctly-formed request from an authorised source.
- A gate at the payment order boundary verifies a human authorisation receipt before the order is created, and fails closed on absence.
Part of Payment release authorization
What the API key proves
A payment platform API authenticates the caller by bearer credential. On a valid key with the right permissions, the platform creates the payment order. That is the intended behaviour and it is what makes programmatic treasury useful.
What the key does not carry is any assertion about intent. Consider three calls that are byte-identical at the API boundary:
# 1. Scheduled vendor payment run, approved in the ERP
POST /api/payment_orders Authorization: Bearer $KEY
# 2. Developer testing against production by mistake
POST /api/payment_orders Authorization: Bearer $KEY
# 3. Attacker with a key from a leaked .env in a repository
POST /api/payment_orders Authorization: Bearer $KEY
The platform cannot distinguish them, and should not be expected to. The distinguishing information lives upstream, in a human decision that never reached the API.
Why the usual compensating controls fall short
| Control | Addresses | Does not address |
|---|---|---|
| IP allowlisting | Calls from unexpected networks | A compromised server inside the allowlist |
| Velocity and amount limits | High-volume drain | A single large payment inside the limit |
| Separate keys per environment | Test/production confusion | Production key compromise |
| Key rotation | Long-lived exposure | The window between leak and rotation |
| Approval in the ERP | Human intent, upstream | Not conveyed to the API |
The last row is the gap. The organisation does perform a human approval — in the ERP, in a spend platform, in a workflow tool — and that approval is not cryptographically connected to the API call that executes it.
The gate
A thin middleware layer at the payment order boundary. It does not replace the platform's authentication; it adds a second requirement that an API key alone cannot satisfy.
// Pseudocode — the shape matters more than the language
async function createPaymentOrder(order, receipt) {
// 1. Canonicalise the order's material terms
const stmt = canonicalise({
action: 'authorise_payment_order',
amount: order.amount, currency: order.currency,
beneficiary: order.receiving_account.name,
account: order.receiving_account.number, // full, unmasked
routing: order.receiving_account.routing, // full
reference: order.reference
});
// 2. Verify the human authorisation covers exactly this statement
const ok = await verifyReceipt(receipt, stmt, publishedKeys);
if (!ok) throw new AuthorisationError('no valid human authorisation');
// 3. Only then call the payment platform
return platform.paymentOrders.create(order);
}
Three properties make this work. The statement covers the order's material terms, so an altered order fails. Verification is offline against published keys, so the gate does not add a network dependency in the payment path. And it fails closed — absence of a valid receipt is a refusal, not a warning.
Handling legitimate automation
Most treasury automation is not a single human clicking approve. It is a scheduled run paying two hundred vendors.
That case is handled by a pre-signed authorisation with explicit bounds rather than by exempting automation:
- A named human signs a delegation covering the run: maximum total, maximum per payment, permitted payee set or payee-file hash, and a validity window.
- The scheduled run presents that delegation. Payments inside the bounds proceed without further human involvement.
- A payment to a payee outside the set, or above a ceiling, produces no valid authorisation and is held for a fresh signature.
- The delegation expires, so the automation's authority is renewed deliberately rather than persisting indefinitely.
Where the gate belongs architecturally
In the service that calls the payment platform, not in the platform and not at the edge. Two reasons.
First, the material terms of the payment are known there — the gate needs to canonicalise the actual order, not a proxy for it. Second, placing it at the edge means an internal service compromised behind the edge bypasses it, which is the scenario that matters most.
What this does not solve
It does not prevent a compromised approver from signing a fraudulent payment, and it does not validate that the invoice behind the payment is genuine. It closes one specific gap: a payment order created without any human authorisation at all.
For a platform where a leaked environment variable is sufficient to move money, that is the gap worth closing first.
The one endpoint that matters most
Not the payment order. The counterparty or external account whose details the order resolves to. A payment order gated tightly against an account record that can be updated freely is a control on the wrong object — the diversion happens upstream and every subsequent payment is correctly authorised to the wrong place.
| Endpoint | Gate |
|---|---|
| External account create / update | Always — this is where diversion starts |
| Payment order above threshold | Signature over the rendered order |
| First payment to a new counterparty | Signature, regardless of amount |
| Routine recurring payment within bounds | Delegation scope is sufficient |
| Ledger reads and reconciliation | No gate |
Objections and honest limits
“We rotate keys frequently.” Rotation reduces the window and does not change what the key proves. An attacker with a current key is indistinguishable from your scheduler for as long as they hold it.
“Our payments are fully automated by design.” Then use a signed delegation: a named human authorises this agent to pay these counterparties up to this ceiling until this date, revocable. Automation keeps working and the chain terminates at a person.
Instrumenting a programmatic treasury integration
- Gate external account changes first. Highest value per unit of work, and the lowest volume.
- Render the delta, not the record. What was on file, what it is changing to, and when it last changed.
- Set a value threshold for payment orders. Above it, a fresh signature; below it, delegation scope.
- Bind the signature to the canonical order. So a payload rebuilt after approval fails verification.
- Keep the receipt with the ledger entry. So reconciliation and evidence live in the same place.
Terms used here
- Payment order
- The instruction to move funds, typically created through an API and resolved against a stored counterparty record.
- External account
- The stored bank details a payment order resolves to. Changing it redirects every future payment.
- Scoped delegation
- A signed grant letting automation act within stated bounds, so the chain from a payment to a human remains intact.
Frequently asked questions
Does this add latency to payments? Verification is a local signature check, sub-millisecond. The gate adds no network round trip because it verifies against published keys held locally.
What about high-frequency automated payments? They run under a pre-signed delegation with explicit bounds. The human signs the envelope, not each payment.
Does it work with any payment platform? The gate sits in your service ahead of the platform call, so it is platform-independent. What varies is where the material terms live in that platform's object model.
What happens if the receipt service is unavailable? Verification is offline, so there is no receipt service to be unavailable. The gate needs the published keys, which are distributed in advance.
Why isn't an API key enough? It authenticates a caller. A scheduled batch and an attacker holding the same key produce identical, equally valid requests.
Which endpoint should be gated first? External account create and update. The payment order can be perfectly controlled and still pay the wrong place.
Does this break automated payments? No. A signed delegation with a ceiling, a counterparty scope and an expiry keeps automation running with a human at the root.
Where this fits in Manav
Manav adds one verification call in front of the endpoints that move money or change where it goes. Routine traffic is untouched; the gated set requires a receipt the platform itself cannot mint.