Manav.id
Compliance · 5 min read

Gating payment orders at the API boundary: an API key is not an intent

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.

Key takeaways
  • 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.

What the API key proves

API key provesA caller holds this secretIt was not revokedIt is within rate limitsNothing about intentA payment order needsA named human chose thisOver this beneficiary and amountRecently, and onceCheckable by a third partyvs
Two different questions, and only one of them is answered at the boundary today.

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

ControlAddressesDoes not address
IP allowlistingCalls from unexpected networksA compromised server inside the allowlist
Velocity and amount limitsHigh-volume drainA single large payment inside the limit
Separate keys per environmentTest/production confusionProduction key compromise
Key rotationLong-lived exposureThe window between leak and rotation
Approval in the ERPHuman intent, upstreamNot 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:

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.

Gating by endpoint, in order of value
EndpointGate
External account create / updateAlways — this is where diversion starts
Payment order above thresholdSignature over the rendered order
First payment to a new counterpartySignature, regardless of amount
Routine recurring payment within boundsDelegation scope is sufficient
Ledger reads and reconciliationNo 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

  1. Gate external account changes first. Highest value per unit of work, and the lowest volume.
  2. Render the delta, not the record. What was on file, what it is changing to, and when it last changed.
  3. Set a value threshold for payment orders. Above it, a fresh signature; below it, delegation scope.
  4. Bind the signature to the canonical order. So a payload rebuilt after approval fails verification.
  5. 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.

See the API gate →

Sources and further reading