Skip to Content
Payment RailsStripe MPP

Stripe MPP

Stripe Machine Payments Protocol lets agents make purchases on behalf of users with cryptographically-bound spend authority. Keel adds the decision and evidence layer: your app creates the Stripe Link SpendRequest, sends the approved SpendRequest to Keel, and Keel decides whether the agent may use that authority for this purchase.

Use this guide when your application already participates in Stripe Link MPP and you want Keel to decide, execute, and prove the MPP transaction through the same permit-centered audit model used by Execute, Permits, and Signed Exports.

Keel does not create or retrieve Stripe Link SpendRequests for this integration. Your application owns Link credentials and passes the approved SpendRequest payload to Keel.

Architecture

Customer app | | 1. Create SpendRequest with link-cli or the Link API v Stripe Link account | | 2. Return approved SpendRequest with shared payment token (SPT) v Customer app | | 3. POST /v1/execute with action_verb=mpp.payment, | SpendRequest payload, requested amount, target URL, | and Keel spend authority v Keel | | 4. Decide against policy and spend authority | 5. Present SPT to the merchant MPP endpoint v Merchant | | 6. Return paid resource plus payment-receipt header v Keel | | 7. Bind receipt into provider_attestation v Customer app

The trust domains stay separate:

DomainOwnerKeel role
Link credential ownershipYour Stripe Link accountNone. Keel never stores or retrieves your Link credentials.
Agent spend decisionKeel permit policyDecide whether this agent may use this SpendRequest for this action.
Settlement railStripe and the merchantRecord rail outcome and bind the receipt evidence.
Audit trailKeel plus the Stripe receiptPreserve tamper-evident audit evidence for review.

Prerequisites

  • A Stripe Link merchant account with MPP enabled.
  • A Keel project on Production or Enterprise. See Plans & Entitlements.
  • A client-scoped Keel API key for POST /v1/execute.
  • An agent workflow that can create a Stripe MPP SpendRequest and wait for approval before calling Keel.
  • A merchant MPP endpoint that accepts the shared payment token and returns a payment-receipt response header.

Set your Keel environment:

export KEEL_BASE_URL="https://api.keelapi.com" export KEEL_API_KEY="keel_sk_your_project_key"

Quick start

  1. Create a SpendRequest with your Stripe Link account.
  2. Retrieve the approved SpendRequest payload.
  3. Send it to Keel through POST /v1/execute with action_verb: "mpp.payment".
  4. Branch on execution.primary_outcome, not only HTTP status.

Python

pip install keel-sdk
from keel_sdk import KeelClient client = KeelClient() spend_request = { "id": "lsrq_example_approved_001", "amount": 1499, "currency": "usd", "status": "approved", "credential_type": "shared_payment_token", "shared_payment_token": { "id": "spt_example_001", "valid_until": "2026-06-03T05:08:27Z", }, "merchant_name": "Example Merchant", "merchant_url": "https://merchant.example/checkout", "context": ( "Agent purchase for user usr_123. The user approved buying the " "monthly research report from Example Merchant for no more than " "$14.99 USD during this checkout session." ), "created_at": "2026-06-02T17:08:27Z", "updated_at": "2026-06-02T17:08:56Z", } result = client.execute.run( { "model": "stripe.mpp.v1", "action_verb": "mpp.payment", "input": { "spend_request": spend_request, "requested_amount": 1499, "requested_currency": "usd", "mpp_target_url": "https://merchant.example/mpp/pay", "authority": { "amount_max": "1499", "currency_class": "USD_FIAT", "cadence": "one_shot", "ttl_seconds": 3600, "purpose_binding": "purchase.once", }, }, } ) if result["execution"]["primary_outcome"] != "allowed_and_paid": error = result.get("error") or {} raise RuntimeError(error.get("code") or result["execution"]["primary_outcome"]) print("permit:", result["permit"]["id"]) print("receipt digest:", result["provider_attestation"]["provider_attestation_digest"])

TypeScript

npm install keel-sdk
import { KeelClient } from "keel-sdk"; const client = new KeelClient(); const spendRequest = { id: "lsrq_example_approved_001", amount: 1499, currency: "usd", status: "approved", credential_type: "shared_payment_token", shared_payment_token: { id: "spt_example_001", valid_until: "2026-06-03T05:08:27Z", }, merchant_name: "Example Merchant", merchant_url: "https://merchant.example/checkout", context: "Agent purchase for user usr_123. The user approved buying the monthly research report from Example Merchant for no more than $14.99 USD during this checkout session.", created_at: "2026-06-02T17:08:27Z", updated_at: "2026-06-02T17:08:56Z", }; const request = { model: "stripe.mpp.v1", action_verb: "mpp.payment", input: { spend_request: spendRequest, requested_amount: 1499, requested_currency: "usd", mpp_target_url: "https://merchant.example/mpp/pay", authority: { amount_max: "1499", currency_class: "USD_FIAT", cadence: "one_shot", ttl_seconds: 3600, purpose_binding: "purchase.once", }, }, }; const result = await client.execute.run(request as any); if (result.execution?.primary_outcome !== "allowed_and_paid") { const code = result.error?.code ?? result.execution?.primary_outcome; throw new Error(`MPP payment did not complete: ${code}`); } console.log("permit:", result.permit.id); console.log( "receipt digest:", result.provider_attestation.provider_attestation_digest, );

Some TypeScript SDK versions may not yet include the action_verb union in the generated ExecuteRequest type. The request body above is the API shape; upgrade the SDK when the typed MPP surface is available.

Request shape

MPP uses the same route as provider-shaped execution:

POST /v1/execute Authorization: Bearer <client_project_api_key> Content-Type: application/json Idempotency-Key: mpp-checkout-001
{ "model": "stripe.mpp.v1", "action_verb": "mpp.payment", "input": { "spend_request": { "id": "lsrq_example_approved_001", "amount": 1499, "currency": "usd", "status": "approved", "credential_type": "shared_payment_token", "shared_payment_token": { "id": "spt_example_001", "billing_address": {}, "valid_until": "2026-06-03T05:08:27Z" }, "merchant_name": "Example Merchant", "merchant_url": "https://merchant.example/checkout", "context": "At least 100 characters describing what the user approved and why the agent is spending.", "line_items": [], "totals": [], "payment_method": "pm_example_001", "payment_details": "csmrpd_example_001", "created_at": "2026-06-02T17:08:27Z", "updated_at": "2026-06-02T17:08:56Z" }, "requested_amount": 1499, "requested_currency": "usd", "mpp_target_url": "https://merchant.example/mpp/pay", "authority": { "amount_max": "1499", "currency_class": "USD_FIAT", "cadence": "one_shot", "ttl_seconds": 3600, "purpose_binding": "purchase.once" } } }
FieldRequired?Notes
modelYesUse stripe.mpp.v1.
action_verbYesUse mpp.payment.
input.spend_requestYesThe approved Stripe Link SpendRequest payload. If your Link tooling returns { "ok": true, "data": [...] }, pass the first item from data.
input.requested_amountYesAmount Keel will authorize for the merchant request, in the smallest currency unit.
input.requested_currencyYesLowercase rail currency, such as usd.
input.mpp_target_urlYesMerchant MPP endpoint Keel should call with the SPT.
input.authorityYesKeel-side spend authority. See Spend authority configuration.

Idempotency-Key is an HTTP header on /v1/execute. See Idempotency for the route-level replay model.

SpendRequest payload reference

The verified Stripe Link CLI full-output shape is:

{ "ok": true, "data": [ { "id": "lsrq_example_approved_001", "merchant_name": "Example Merchant", "merchant_url": "https://merchant.example/checkout", "context": "At least 100 characters describing the approved purchase.", "amount": 1499, "currency": "usd", "line_items": [], "totals": [], "payment_method": "pm_example_001", "payment_details": "csmrpd_example_001", "status": "approved", "created_at": "2026-06-02T17:08:27Z", "updated_at": "2026-06-02T17:08:56Z", "shared_payment_token": { "id": "spt_example_001", "billing_address": {}, "valid_until": "2026-06-03T05:08:27Z" }, "credential_type": "shared_payment_token" } ], "meta": { "command": "spend-request retrieve", "duration": "379ms" } }

Pass the SpendRequest object itself to Keel, not the ok / data / meta wrapper.

SpendRequest fieldKeel handling
idStored as the Stripe SpendRequest reference. Use synthetic examples such as lsrq_example_... in tests and docs.
amountCompared against requested_amount and authority.amount_max.
currencyCompared against requested_currency and mapped to authority.currency_class.
statusMust be approved immediately before brokering. Terminal statuses cannot be retried.
credential_typeMust be shared_payment_token for this integration.
shared_payment_token.idUsed only to present the payment credential to the merchant endpoint.
shared_payment_token.valid_untilMust outlive authority.ttl_seconds from the time Keel issues the permit.
merchant_name, merchant_url, contextCustomer-controlled values. Keel binds digests into the signed artifact rather than storing these fields as authority text.
payment_method, payment_detailsStripe-controlled identifiers. Keel may retain them as receipt context.
line_items, totalsOptional purchase detail. Treat as customer-controlled context.

Stripe MPP SpendRequests are single-use. After a successful payment the SpendRequest becomes terminal, usually succeeded; do not retry the same SpendRequest ID. Create a fresh SpendRequest for a fresh payment attempt.

Spend authority configuration

The authority object is the Keel-side spend boundary. It is separate from the Link approval and is what Keel evaluates before presenting the SPT to the merchant.

FieldRequired?Notes
amount_maxYesMaximum authorized spend in the smallest currency unit. Send as a decimal string when possible.
currency_classYesCurrency class Keel should enforce against requested_currency.
cadenceYesFor Stripe MPP v1 use one_shot. recurring and streaming are reserved for separate rail support.
ttl_secondsYesAuthority lifetime from permit issuance. Must be less than the SPT remaining lifetime.
purpose_bindingYesFunctional purpose of the spend authority. For one-time purchases use purchase.once.

Supported currency_class values:

USD_FIAT, EUR_FIAT, GBP_FIAT, USDC_STABLE, USDT_STABLE, ETH_NATIVE, BTC_NATIVE, OTHER_FIAT, OTHER_STABLE, OTHER_CRYPTO

Supported purpose_binding values:

purchase.once, charge.recurring, charge.usage_based, compute.metered, access.data, access.content, tool.execution, credential.purchase, funds.release, funds.reversal, account.funding, other

Use the narrowest authority that matches the user approval. For example, a user-approved one-time USD purchase should use currency_class: "USD_FIAT", cadence: "one_shot", and purpose_binding: "purchase.once".

Response shape

MPP execution returns an execution envelope with MPP-specific nested namespaces. Keep verifier findings under verifier.* and rail/payment outcome under execution.*; do not flatten these into one status.

{ "id": "exec_mpp_example_001", "object": "execution", "created_at": "2026-06-02T17:09:01Z", "status": "completed", "status_code": 200, "action_verb": "mpp.payment", "permit": { "id": "permit_01kexamplempp", "decision": "allow" }, "execution": { "status": "completed", "permit_outcome": "allowed", "rail_outcome": "paid", "settlement_status": "settled", "primary_outcome": "allowed_and_paid" }, "verifier": { "primary_outcome": "PASS", "status": "PASS", "findings": [], "highest_severity": null, "parse_valid": true, "signature_valid": true, "binding_valid": true, "policy_satisfied": true }, "provider_attestation": { "payload_type": "permit.provider_attestation.v1", "signer_id": "stripe_mpp", "key_id": "b8f6c4e2d0a9b7c5f3e1d2c4b6a8979081726354aabbccddeeff001122334455", "signed_at": "2026-06-02T17:09:02Z", "signed_payload_hash": "7e6d5c4b3a291807162534aabbccddeeff00112233445566778899aabbccdde0", "signature": "opaque_receipt_example_token", "provider_protocol": "stripe_mpp", "provider_protocol_version": "1.0", "normalizer_version": "stripe_mpp.v1", "raw_schema_fingerprint": "7e6d5c4b3a291807162534aabbccddeeff00112233445566778899aabbccdde0", "rail_class": "stripe.mpp.v1", "provider_transaction_id": "stripe_mpp_receipt:example", "provider_event_time": "2026-06-02T17:09:02Z", "provider_event_time_trust_source": "provider_signed", "severity": "ADVISORY", "purpose_binding": "purchase.once", "trust_domain": "provider_principal", "spend_scope_hash": "4c3b2a19080706050403020100abcdefabcdefabcdefabcd9f2d7c3e8b6a5d", "provider_attestation_digest": "9f2d7c3e8b6a5d4c3b2a19080706050403020100abcdefabcdefabcdefabcd", "provider_receipt_fingerprint": "6a5d4c3b2a19080706050403020100abcdefabcdefabcdefabcd9f2d7c3e8b", "bound_execution_record_hash": "5d4c3b2a19080706050403020100abcdefabcdefabcdefabcd9f2d7c3e8b6a", "accepted_at": "2026-06-02T17:09:02Z" }, "output": { "rail_outcome": "paid", "settlement_status": "settled", "http_status": 200, "resource": { "order_id": "order_example_001" } }, "timing": { "started_at": "2026-06-02T17:09:01Z", "completed_at": "2026-06-02T17:09:02Z", "duration_ms": 811 }, "error": null }

Important fields:

FieldMeaning
execution.primary_outcomeProduct-level outcome for the transaction. Common success value: allowed_and_paid.
execution.rail_outcomePayment rail result, such as paid, pending, failed, or timeout.
execution.settlement_statusSettlement interpretation for the merchant response.
verifier.statusEvidence adjudication status. A payment can be operationally complete while verifier findings still carry advisory context.
provider_attestationReceipt-bound evidence envelope. This is the field auditors usually need.
output.resourceThe merchant’s original paid resource body.

Error handling

Denied MPP executions return an execution envelope. Branch on status, execution.primary_outcome, and error.code.

if (result.status !== "completed") { const findings = result.error?.details?.mpp_validation_findings ?? []; showUserFacingPaymentError(result.error?.code, findings); }

Common validation and runtime codes:

CodeTypical causeUser-facing handling
MPP_REQUIRES_PRODUCTION_TIERProject is not on Production or Enterprise.Show an admin setup error. Do not ask the end user to retry.
MPP_AUTHORITY_REQUIREDinput.authority is missing or malformed.Fix server-side integration config.
MPP_CADENCE_NOT_SUPPORTEDAuthority cadence is not one_shot.Create a one-time SpendRequest or use a future rail-specific guide.
MPP_AMOUNT_EXCEEDS_AUTHORITYrequested_amount is greater than authority.amount_max.Ask the user to approve a higher limit or lower the purchase amount.
MPP_CURRENCY_CLASS_MISMATCHrequested_currency does not match authority.currency_class.Recreate authority with the correct currency class.
MPP_SPEND_REQUEST_NOT_APPROVEDSpendRequest is still created or pending_approval.Wait for Link approval before calling Keel.
MPP_SPEND_REQUEST_ALREADY_CONSUMEDSpendRequest is already succeeded.Create a fresh SpendRequest. Do not retry the consumed one.
MPP_SPEND_REQUEST_DENIEDUser rejected the Link approval.Tell the agent the user declined the payment.
MPP_SPEND_REQUEST_EXPIREDSpendRequest expired before use.Create a fresh SpendRequest.
MPP_SPEND_REQUEST_CANCELLEDUser or system cancelled the request.Create a fresh SpendRequest if the user still wants the purchase.
MPP_SPEND_REQUEST_AMOUNT_INSUFFICIENTLink-approved amount is below requested_amount.Ask the user to approve the actual amount.
MPP_SPEND_REQUEST_CURRENCY_MISMATCHLink currency and requested currency differ.Recreate the SpendRequest in the intended currency.
MPP_CREDENTIAL_TYPE_UNSUPPORTEDSpendRequest is not shared_payment_token.Use the Stripe MPP agent flow, not a card credential flow.
MPP_SHARED_PAYMENT_TOKEN_MISSINGApproved SpendRequest has no SPT.Recreate the SpendRequest and surface a setup error if it repeats.
MPP_SPT_TTL_EXCEEDS_CREDENTIALttl_seconds extends beyond the SPT lifetime.Lower ttl_seconds or refresh the SpendRequest.
MPP_TARGET_URL_REQUIREDMerchant MPP target URL missing.Fix server-side request construction.
MPP_RECEIPT_MISSINGMerchant returned success without payment-receipt.Treat as a merchant integration failure; do not mark the payment evidence complete.

For non-MPP permit and execution errors, see Errors.

Audit trail

Capture these fields from the execution response:

id permit.id execution.primary_outcome execution.rail_outcome verifier.status provider_attestation.provider_attestation_digest provider_attestation.provider_transaction_id

To retrieve the permit-scoped audit bundle later:

curl -sS https://api.keelapi.com/v1/permits/$KEEL_PERMIT_ID/bundle \ -H "Authorization: Bearer $KEEL_API_KEY" \ | jq '.. | objects | select(.payload_type? == "permit.provider_attestation.v1")'

For compliance review, request a signed export that includes the relevant permit window:

curl -sS -X POST https://api.keelapi.com/v1/compliance/exports \ -H "Authorization: Bearer $KEEL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "export_type": "full_audit", "format": "json", "filters": { "permit_id": "permit_01kexamplempp" } }'

Then verify the export before review. See Signed Exports, Independent Verification Overview, and Running Keel Verify.

FAQ

Why does my app pass the SpendRequest instead of having Keel retrieve it?

Trust domain separation. Your application owns Link credentials and the user’s payment authority. Keel decides whether the agent may use that authority for a specific action, but Keel does not custody Link credentials and does not become the system that can independently fetch payment authority from Stripe.

That boundary keeps the integration clean:

  • your Link account remains the source of payment authority
  • Keel remains the decision and evidence layer
  • merchant settlement remains on the Stripe MPP rail

Can I use this pattern with x402, AP2, or MCP payment flows?

Yes. The same pattern applies: your app or payment rail creates the payment authority, Keel receives the authority payload, Keel decides whether the agent may use it, and Keel binds the rail receipt into audit evidence.

Use separate guides for x402, AP2, and MCP payment integrations because each rail has a different authority payload, receipt format, and replay model.

Last updated on Edit this page on GitHubÂ