Repository navigation
PayPal Sandbox Runbook
Procedure only. Source: docs/sandbox-runbook.md and settlement/sandbox_client.py at 9900817. Nothing here was executed for this wiki. Running anything against the sandbox is a maintainer decision.
- Environment variables (never commit, never print):
-
PERMIT_PAYPAL_CLIENT_ID,PERMIT_PAYPAL_CLIENT_SECRET: sandbox REST app credentials -
PERMIT_PAYPAL_MERCHANT_ID: the sandbox merchant the allowlist is bound to. Without it, merchant binding is disabled and the server prints a warning
-
- A browser and a sandbox buyer account for payer approval.
-
.env.sandboxis gitignored. - Start the service:
python3 server.py --sandbox --port 8741.
-
POST /api/permits: grant. -
POST /api/permits/<id>/spend→ 202approval_requiredwithoperation_id,order_idandapproval_url. The reservation stays held. Do not spend again for the same purchase. - The payer approves at
approval_url. Confirm through the order's API state, never the checkout page (the page can stall while the order is already APPROVED). -
POST /api/operations/<operation_id>/resumeauthorizes the same order and registers the escrow. -
POST /api/escrows/<id>/releasewith evidence → capture (PayPal-Request-Ididempotent). - On a timeout, the escrow is UNKNOWN →
POST /api/escrows/<id>/reconcile. -
POST /api/permits/<id>/estoprevokes and voids in-flight holds. Completed captures need a refund (out of scope). - Record
GET /api/ledgerat the end. The receipt chain is the evidence.
Setup $50 → real $30 purchase captured → $60 blocked with zero new authorizations → e-stop mid-hold voids the authorization → tampered evidence refused → dropped capture response → UNKNOWN → reconcile with exactly one capture. The README says this integrated run is "not yet recorded".
- Orders API,
intent: AUTHORIZE, currency CAD. Amounts are sent ascents/100formatted to 2 decimals. - Trust binding on authorize: the payee
merchant_id(if configured), the currency CAD, and the exact cents must match, orMerchantMismatchis raised. Binding runs against the POST authorize response. - A 422
ORDER_NOT_APPROVEDraisesNeedsPayerApproval. Any other non-200/201 response fails an assertion. ⚠️ ORDER_ALREADY_AUTHORIZEDrecovery is absent. The recovery (read the existing authorization via GET order) and GET-based trust binding were added inea2dcd3after the 2026-10-05 sandbox run. They were removed in980da3e, along with their 2 regression tests. On9900817, a 422ORDER_ALREADY_AUTHORIZEDwould raise anAssertionErrorinauthorize_order. Whether PayPal still returns that code on this flow: Unknown (not exercised).- Every HTTP call has a 30 s timeout.
wait_for_approval()defaults to a 120 s timeout and 5 s polling.
The sandbox smoke script (scripts/sandbox_smoke.py) and the launch helper (launch_sandbox.py) are no longer in the tree (removed in 980da3e). The only sandbox entry point at 9900817 is server.py --sandbox.
See Evidence Map for what sandbox evidence exists.
Permit · payment authority for AI agents · MIT · Wiki facts verified against commit 9900817 on 2026-10-07 (America/Edmonton). Where this wiki and the code disagree, the code wins.
Concepts
- Overview and Problem
- The Four-Clause Gate
- Spend Pipeline
- Delegation and Sub-Permits
- Principal Approvals
- Tighten-Only Narrowing
- E-Stop and Revocation
- Claim Ledger
- UNKNOWN and Reconcile
Using it
Design and status
Reference