{
 "slug": "modern-treasury-api-approval-gate-passkeys",
 "topic_id": "TOPIC-006",
 "cluster": "B2B Wire, AP & Treasury Payment Release",
 "tier": "Tier B",
 "title": "Gating payment orders at the API boundary: an API key is not an intent",
 "summary": "Programmatic treasury platforms treat possession of an API key as proof of intent. There is no cryptographic distinction between a scheduled batch run and an attacker with a leaked environment variable.",
 "lede": "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.",
 "date": "2025-01-07",
 "category": "Compliance",
 "author_id": "tobias-lindqvist-rao",
 "tags": [
  "payment API",
  "treasury automation",
  "API keys",
  "Modern Treasury",
  "payment orders",
  "middleware"
 ],
 "image_title": "API Approval Gate",
 "schema": "Article",
 "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."
 ],
 "body": [
  {
   "type": "h2",
   "text": "What the API key proves"
  },
  {
   "type": "diagram",
   "kind": "compare",
   "alt": "What the API key proves versus what a payment order needs",
   "caption": "Two different questions, and only one of them is answered at the boundary today.",
   "nodes": [],
   "left": {
    "title": "API key proves",
    "items": [
     "A caller holds this secret",
     "It was not revoked",
     "It is within rate limits",
     "Nothing about intent"
    ]
   },
   "right": {
    "title": "A payment order needs",
    "items": [
     "A named human chose this",
     "Over this beneficiary and amount",
     "Recently, and once",
     "Checkable by a third party"
    ]
   }
  },
  {
   "type": "p",
   "html": "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."
  },
  {
   "type": "p",
   "html": "What the key does not carry is any assertion about intent. Consider three calls that are byte-identical at the API boundary:"
  },
  {
   "type": "code",
   "text": "# 1. Scheduled vendor payment run, approved in the ERP\nPOST /api/payment_orders  Authorization: Bearer $KEY\n\n# 2. Developer testing against production by mistake\nPOST /api/payment_orders  Authorization: Bearer $KEY\n\n# 3. Attacker with a key from a leaked .env in a repository\nPOST /api/payment_orders  Authorization: Bearer $KEY"
  },
  {
   "type": "p",
   "html": "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."
  },
  {
   "type": "h2",
   "text": "Why the usual compensating controls fall short"
  },
  {
   "type": "table",
   "head": [
    "Control",
    "Addresses",
    "Does not address"
   ],
   "rows": [
    [
     "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",
     "<strong style=\"font-weight:600\">Not conveyed to the API</strong>"
    ]
   ]
  },
  {
   "type": "p",
   "html": "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."
  },
  {
   "type": "h2",
   "text": "The gate"
  },
  {
   "type": "p",
   "html": "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."
  },
  {
   "type": "code",
   "text": "// Pseudocode — the shape matters more than the language\nasync function createPaymentOrder(order, receipt) {\n  // 1. Canonicalise the order's material terms\n  const stmt = canonicalise({\n    action: 'authorise_payment_order',\n    amount: order.amount, currency: order.currency,\n    beneficiary: order.receiving_account.name,\n    account: order.receiving_account.number,      // full, unmasked\n    routing:  order.receiving_account.routing,    // full\n    reference: order.reference\n  });\n\n  // 2. Verify the human authorisation covers exactly this statement\n  const ok = await verifyReceipt(receipt, stmt, publishedKeys);\n  if (!ok) throw new AuthorisationError('no valid human authorisation');\n\n  // 3. Only then call the payment platform\n  return platform.paymentOrders.create(order);\n}"
  },
  {
   "type": "p",
   "html": "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."
  },
  {
   "type": "h2",
   "text": "Handling legitimate automation"
  },
  {
   "type": "p",
   "html": "Most treasury automation is not a single human clicking approve. It is a scheduled run paying two hundred vendors."
  },
  {
   "type": "p",
   "html": "That case is handled by a pre-signed authorisation with explicit bounds rather than by exempting automation:"
  },
  {
   "type": "ul",
   "items": [
    "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."
   ]
  },
  {
   "type": "h2",
   "text": "Where the gate belongs architecturally"
  },
  {
   "type": "p",
   "html": "In the service that calls the payment platform, not in the platform and not at the edge. Two reasons."
  },
  {
   "type": "p",
   "html": "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."
  },
  {
   "type": "h2",
   "text": "What this does not solve"
  },
  {
   "type": "p",
   "html": "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."
  },
  {
   "type": "p",
   "html": "For a platform where a leaked environment variable is sufficient to move money, that is the gap worth closing first."
  },
  {
   "type": "h2",
   "text": "The one endpoint that matters most"
  },
  {
   "type": "p",
   "html": "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."
  },
  {
   "type": "table",
   "caption": "Gating by endpoint, in order of value",
   "head": [
    "Endpoint",
    "Gate"
   ],
   "rows": [
    [
     "External account create / update",
     "<strong style=\"font-weight:600\">Always</strong> — 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"
    ]
   ]
  },
  {
   "type": "h2",
   "text": "Objections and honest limits"
  },
  {
   "type": "p",
   "html": "<strong style=\"font-weight:600\">“We rotate keys frequently.”</strong> 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."
  },
  {
   "type": "p",
   "html": "<strong style=\"font-weight:600\">“Our payments are fully automated by design.”</strong> 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."
  }
 ],
 "faq": [
  {
   "q": "Does this add latency to payments?",
   "a": "Verification is a local signature check, sub-millisecond. The gate adds no network round trip because it verifies against published keys held locally."
  },
  {
   "q": "What about high-frequency automated payments?",
   "a": "They run under a pre-signed delegation with explicit bounds. The human signs the envelope, not each payment."
  },
  {
   "q": "Does it work with any payment platform?",
   "a": "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."
  },
  {
   "q": "What happens if the receipt service is unavailable?",
   "a": "Verification is offline, so there is no receipt service to be unavailable. The gate needs the published keys, which are distributed in advance."
  },
  {
   "q": "Why isn't an API key enough?",
   "a": "It authenticates a caller. A scheduled batch and an attacker holding the same key produce identical, equally valid requests."
  },
  {
   "q": "Which endpoint should be gated first?",
   "a": "External account create and update. The payment order can be perfectly controlled and still pay the wrong place."
  },
  {
   "q": "Does this break automated payments?",
   "a": "No. A signed delegation with a ceiling, a counterparty scope and an expiry keeps automation running with a human at the root."
  }
 ],
 "sources": [
  {
   "t": "FTC — business guidance on marketplaces and consumer protection",
   "u": "https://www.ftc.gov/business-guidance"
  },
  {
   "t": "CISA — known exploited vulnerabilities and incident reporting",
   "u": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog"
  },
  {
   "t": "FCC — protecting consumers from SIM swap and port-out fraud",
   "u": "https://www.fcc.gov/sim-swap-port-out-fraud"
  },
  {
   "t": "RFC 8785 — JSON Canonicalization Scheme",
   "u": "https://www.rfc-editor.org/rfc/rfc8785"
  },
  {
   "t": "Federal Reserve — Fedwire Funds Service",
   "u": "https://www.frbservices.org/financial-services/wires"
  },
  {
   "t": "Nacha Operating Rules",
   "u": "https://www.nacha.org/rules"
  }
 ],
 "related": [
  {
   "slug": "erp-dual-authorization-flaw-sox-controls",
   "title": "The four-eyes illusion",
   "category": "Developer"
  },
  {
   "slug": "revocation-latency-framework",
   "title": "Ninety-one percent of leaked secrets still work",
   "category": "Developer"
  },
  {
   "slug": "fednow-rtp-instant-payment-fraud-risks",
   "title": "Instant rails, instant irreversibility",
   "category": "Comparison"
  },
  {
   "slug": "bill-com-vs-ramp-vs-netsuite-security-engineers",
   "title": "Payment release in AP and spend platforms, compared",
   "category": "Comparison"
  }
 ],
 "image": "https://cdn.twc.sh/images/igcache/API%20Approval%20Gate/1200_630/blog.jpg",
 "wordcount": 1047,
 "url": "/blog/modern-treasury-api-approval-gate-passkeys.html",
 "reading_time": "5 min read",
 "seo_title": "Gating payment orders at the API boundary",
 "meta_description": "Programmatic treasury platforms treat possession of an API key as proof of intent. A scheduled batch and an attacker look identical.",
 "hub": {
  "slug": "topics/payment-release-authorization",
  "title": "Payment release authorization"
 },
 "answer": "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.",
 "answer_q": "Why is an API key not proof of payment intent?",
 "entities": [
  {
   "name": "Modern Treasury",
   "type": "Organization",
   "url": "https://www.moderntreasury.com/",
   "primary": true
  }
 ],
 "glossary": [
  {
   "term": "Payment order",
   "def": "The instruction to move funds, typically created through an API and resolved against a stored counterparty record."
  },
  {
   "term": "External account",
   "def": "The stored bank details a payment order resolves to. Changing it redirects every future payment."
  },
  {
   "term": "Scoped delegation",
   "def": "A signed grant letting automation act within stated bounds, so the chain from a payment to a human remains intact."
  }
 ],
 "checklist": {
  "title": "Instrumenting a programmatic treasury integration",
  "id": "instrument",
  "desc": "Five steps that do not slow the routine path.",
  "steps": [
   {
    "name": "Gate external account changes first.",
    "text": "Highest value per unit of work, and the lowest volume."
   },
   {
    "name": "Render the delta, not the record.",
    "text": "What was on file, what it is changing to, and when it last changed."
   },
   {
    "name": "Set a value threshold for payment orders.",
    "text": "Above it, a fresh signature; below it, delegation scope."
   },
   {
    "name": "Bind the signature to the canonical order.",
    "text": "So a payload rebuilt after approval fails verification."
   },
   {
    "name": "Keep the receipt with the ledger entry.",
    "text": "So reconciliation and evidence live in the same place."
   }
  ]
 },
 "cta": {
  "title": "Where this fits in Manav",
  "html": "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.",
  "href": "../docs.html",
  "label": "See the API gate"
 }
}