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 appThe trust domains stay separate:
| Domain | Owner | Keel role |
|---|---|---|
| Link credential ownership | Your Stripe Link account | None. Keel never stores or retrieves your Link credentials. |
| Agent spend decision | Keel permit policy | Decide whether this agent may use this SpendRequest for this action. |
| Settlement rail | Stripe and the merchant | Record rail outcome and bind the receipt evidence. |
| Audit trail | Keel plus the Stripe receipt | Preserve 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-receiptresponse header.
Set your Keel environment:
export KEEL_BASE_URL="https://api.keelapi.com"
export KEEL_API_KEY="keel_sk_your_project_key"Quick start
- Create a SpendRequest with your Stripe Link account.
- Retrieve the approved SpendRequest payload.
- Send it to Keel through
POST /v1/executewithaction_verb: "mpp.payment". - Branch on
execution.primary_outcome, not only HTTP status.
Python
pip install keel-sdkfrom 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-sdkimport { 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"
}
}
}| Field | Required? | Notes |
|---|---|---|
model | Yes | Use stripe.mpp.v1. |
action_verb | Yes | Use mpp.payment. |
input.spend_request | Yes | The approved Stripe Link SpendRequest payload. If your Link tooling returns { "ok": true, "data": [...] }, pass the first item from data. |
input.requested_amount | Yes | Amount Keel will authorize for the merchant request, in the smallest currency unit. |
input.requested_currency | Yes | Lowercase rail currency, such as usd. |
input.mpp_target_url | Yes | Merchant MPP endpoint Keel should call with the SPT. |
input.authority | Yes | Keel-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 field | Keel handling |
|---|---|
id | Stored as the Stripe SpendRequest reference. Use synthetic examples such as lsrq_example_... in tests and docs. |
amount | Compared against requested_amount and authority.amount_max. |
currency | Compared against requested_currency and mapped to authority.currency_class. |
status | Must be approved immediately before brokering. Terminal statuses cannot be retried. |
credential_type | Must be shared_payment_token for this integration. |
shared_payment_token.id | Used only to present the payment credential to the merchant endpoint. |
shared_payment_token.valid_until | Must outlive authority.ttl_seconds from the time Keel issues the permit. |
merchant_name, merchant_url, context | Customer-controlled values. Keel binds digests into the signed artifact rather than storing these fields as authority text. |
payment_method, payment_details | Stripe-controlled identifiers. Keel may retain them as receipt context. |
line_items, totals | Optional 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.
| Field | Required? | Notes |
|---|---|---|
amount_max | Yes | Maximum authorized spend in the smallest currency unit. Send as a decimal string when possible. |
currency_class | Yes | Currency class Keel should enforce against requested_currency. |
cadence | Yes | For Stripe MPP v1 use one_shot. recurring and streaming are reserved for separate rail support. |
ttl_seconds | Yes | Authority lifetime from permit issuance. Must be less than the SPT remaining lifetime. |
purpose_binding | Yes | Functional 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_CRYPTOSupported 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, otherUse 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:
| Field | Meaning |
|---|---|
execution.primary_outcome | Product-level outcome for the transaction. Common success value: allowed_and_paid. |
execution.rail_outcome | Payment rail result, such as paid, pending, failed, or timeout. |
execution.settlement_status | Settlement interpretation for the merchant response. |
verifier.status | Evidence adjudication status. A payment can be operationally complete while verifier findings still carry advisory context. |
provider_attestation | Receipt-bound evidence envelope. This is the field auditors usually need. |
output.resource | The 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:
| Code | Typical cause | User-facing handling |
|---|---|---|
MPP_REQUIRES_PRODUCTION_TIER | Project is not on Production or Enterprise. | Show an admin setup error. Do not ask the end user to retry. |
MPP_AUTHORITY_REQUIRED | input.authority is missing or malformed. | Fix server-side integration config. |
MPP_CADENCE_NOT_SUPPORTED | Authority cadence is not one_shot. | Create a one-time SpendRequest or use a future rail-specific guide. |
MPP_AMOUNT_EXCEEDS_AUTHORITY | requested_amount is greater than authority.amount_max. | Ask the user to approve a higher limit or lower the purchase amount. |
MPP_CURRENCY_CLASS_MISMATCH | requested_currency does not match authority.currency_class. | Recreate authority with the correct currency class. |
MPP_SPEND_REQUEST_NOT_APPROVED | SpendRequest is still created or pending_approval. | Wait for Link approval before calling Keel. |
MPP_SPEND_REQUEST_ALREADY_CONSUMED | SpendRequest is already succeeded. | Create a fresh SpendRequest. Do not retry the consumed one. |
MPP_SPEND_REQUEST_DENIED | User rejected the Link approval. | Tell the agent the user declined the payment. |
MPP_SPEND_REQUEST_EXPIRED | SpendRequest expired before use. | Create a fresh SpendRequest. |
MPP_SPEND_REQUEST_CANCELLED | User or system cancelled the request. | Create a fresh SpendRequest if the user still wants the purchase. |
MPP_SPEND_REQUEST_AMOUNT_INSUFFICIENT | Link-approved amount is below requested_amount. | Ask the user to approve the actual amount. |
MPP_SPEND_REQUEST_CURRENCY_MISMATCH | Link currency and requested currency differ. | Recreate the SpendRequest in the intended currency. |
MPP_CREDENTIAL_TYPE_UNSUPPORTED | SpendRequest is not shared_payment_token. | Use the Stripe MPP agent flow, not a card credential flow. |
MPP_SHARED_PAYMENT_TOKEN_MISSING | Approved SpendRequest has no SPT. | Recreate the SpendRequest and surface a setup error if it repeats. |
MPP_SPT_TTL_EXCEEDS_CREDENTIAL | ttl_seconds extends beyond the SPT lifetime. | Lower ttl_seconds or refresh the SpendRequest. |
MPP_TARGET_URL_REQUIRED | Merchant MPP target URL missing. | Fix server-side request construction. |
MPP_RECEIPT_MISSING | Merchant 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_idTo 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.
Related pages
- Execute -
POST /v1/executeroute semantics - Permits - decision-first permit contract
- Execution Modes - choosing the right runtime surface
- Reconciliation - receipt and usage reconciliation
- Signed Exports - signed compliance exports
- Scope and Limits - trust boundaries and evidence limits