Connect via REST
Sandbox live. Create a free sandbox workspace at agentic-sandbox.pinkwallet.com (test credentials only; no money moves). Production is not yet available.
Pink Agentic AI Payments (by PinkWallet, early access) is the approval layer between AI agents and company money: plain-language rules, per-agent budgets and human approvals decide each payment before a single-use card or bank transfer is issued.
The REST API is a mirror of the MCP tools (Connect via MCP) for agents that don't speak MCP, plus admin routes with no MCP equivalent. All examples use $PINK_BASE (export PINK_BASE=https://agentic-sandbox.pinkwallet.com — public sandbox: live).
Authentication
Authorization: Bearer <key>
An agent key (pk_sandbox_agent_...) can call the agent routes below. An admin key (pk_sandbox_admin_...) can call /v1/admin/*. Using an agent key on an admin route (or vice versa) returns 403. No key at all returns 401:
curl -s -i "$PINK_BASE/v1/budget"
# HTTP/1.1 401 Unauthorized
# {"error":"Unauthorized: send Authorization: Bearer <key>"}
X-Api-Key: <key> and ?key=<key> are also accepted, but Authorization: Bearer is the documented default.
Agent routes (agent key)
| Method & path | Does |
|---|---|
GET /v1/me |
Workspace + this agent's id/name/department/vault. |
GET /v1/budget |
Same object as pink.get_budget. |
GET /v1/payees |
{ payees: [...] } — same as pink.list_payees. |
GET /v1/vaults |
{ vaults: [...] } — just the vault(s) this agent is attached to. |
GET /v1/rules |
{ rules: [...] } — same as pink.list_rules. |
POST /v1/payments/check |
Dry run — same as pink.check_policy. |
POST /v1/payments |
The real call — same as pink.request_payment. Returns 201 (allowed), 202 (pending_human), or 403 (blocked). |
GET /v1/payments/{id} |
Same as pink.get_credential(hold_id). |
POST /v1/payments/{id}/receipt |
Same as pink.report_receipt. Body: {"receipt": {...}} (or the receipt object directly — the code accepts both body.receipt and body itself). |
GET /v1/budget
curl -s "$PINK_BASE/v1/budget" -H "authorization: Bearer $PURCH"
{
"agent": "Purchasing AI", "status": "active",
"monthly_budget": 4000, "spent_this_month": 3570, "left_this_month": 430,
"spent_today": 1310, "single_payment_cap": 500,
"vault": { "id": "v_ops", "name": "Operating", "balances": { "USD": 17110.55 } },
"company_daily_ceiling": 6000, "company_spent_today": 1310, "policy_version": 1
}
POST /v1/payments/check
curl -s -X POST "$PINK_BASE/v1/payments/check" -H "authorization: Bearer $PURCH" -H 'content-type: application/json' \
-d '{"payee_id":"p_uline","amount":186.4,"purpose":"cups"}'
Returns 200 with the same {decision: would_allow|would_ask|would_block, reason, rule, trace} shape as pink.check_policy — see Connect via MCP for the full trace format.
POST /v1/payments — 201 allowed
curl -s -i -X POST "$PINK_BASE/v1/payments" -H "authorization: Bearer $PURCH" -H 'content-type: application/json' \
-H 'idempotency-key: rest-demo-1' \
-d '{"payee_id":"p_uline","amount":186.4,"purpose":"cups"}'
HTTP/1.1 201 Created
{
"payment_id": "pay_5e7da67d6879", "decision": "allowed",
"agent": "Purchasing AI", "payee": "Uline", "payee_id": "p_uline",
"amount": 186.4, "currency": "USD", "purpose": "cups",
"rule": "Small supply orders go through", "policy_version": 1,
"created_at": "2026-09-30T21:07:03.192Z",
"credential": { "type": "virtual_card", "sandbox": true, "max_amount": 186.4, "currency": "USD", "locked_to": "Uline", "single_use": true, "expires_at": "2026-09-30T21:22:03.192Z", "card": { "pan": "4111 1111 1086 9610", "exp": "12/27", "cvv": "201", "name": "PINK SANDBOX" } }
}
pan is always a 4111 1111 test-range BIN — a sandbox test value, never a real card. The idempotency key can go in the body (idempotency_key) or as an Idempotency-Key / idempotency-key header — the server checks the header only as a fallback when the body field is absent. Confirmed via the MCP route (which only takes the body field) that a repeated key returns the original payment with "replayed": true.
POST /v1/payments — 202 pending_human
curl -s -i -X POST "$PINK_BASE/v1/payments" -H "authorization: Bearer $INV" -H 'content-type: application/json' \
-d '{"payee_id":"p_uline","amount":600,"purpose":"bulk cup order"}'
HTTP/1.1 202 Accepted
{
"payment_id": "pay_3adfda5d15c7", "decision": "pending_human",
"agent": "Inventory AI", "payee": "Uline", "amount": 600,
"rule": "Bigger supply orders: store manager checks",
"hold_id": "pay_3adfda5d15c7", "approvers_needed": 1, "approvals_so_far": 0,
"who": "Luis Ortega (Store manager)", "expires_at": "2026-09-30T21:37:19.751Z",
"poll": "pink.get_credential(hold_id) · or GET /v1/payments/{id}"
}
POST /v1/payments — 403 blocked
Two real ways to get a 403: a rule match, or the agent's own monthly budget being used up (the code returns decision: "blocked" with 403 for either):
// rule match (gift cards)
{ "payment_id": "pay_40009af4f54c", "decision": "blocked", "payee": "giftcards.com", "payee_id": null, "rule": "Never: gift cards, cash-like, crypto", "credential": null, "retry_after": null, "why": "matched · block" }
// budget exhausted (not a named rule — the engine's own budget check)
{ "payment_id": "pay_d1426ff46ad9", "decision": "blocked", "rule": "Purchasing AI's monthly budget is used up", "credential": null, "retry_after": null, "why": "$3,756.40 used of $4,000 · this would exceed it" }
GET /v1/payments/{id} and POST /v1/payments/{id}/receipt
curl -s "$PINK_BASE/v1/payments/pay_5e7da67d6879" -H "authorization: Bearer $PURCH"
# → same 201 body as above, fetched back (200 OK)
curl -s -X POST "$PINK_BASE/v1/payments/pay_5e7da67d6879/receipt" -H "authorization: Bearer $PURCH" -H 'content-type: application/json' \
-d '{"receipt":{"merchant":"Uline","total":186.4,"reference":"INV-2"}}'
# → {"ok":true,"payment_id":"pay_5e7da67d6879","trust_score":97}
GET /v1/payments/{id} only returns a payment that belongs to the calling agent — a different agent's key gets 404 {"error":"not found"}.
Admin routes (admin key)
| Method & path | Does |
|---|---|
GET /v1/admin/state |
Full dump: workspace, people, groups, vaults, agents, payees, rules, breakers, pending holds, last 100 transactions. |
GET /v1/admin/workspace |
Admin-view summary (agents with keys, payee ids, rule count). |
POST /v1/admin/requests/{id}/approve |
Approve a pending hold → issues the credential. |
POST /v1/admin/requests/{id}/decline |
Decline a pending hold. |
PUT /v1/admin/rules |
Replace the whole rule set. Body {"rules": [...]}. Bumps policy_version. |
PATCH /v1/admin/agents/{id} |
Patch status (active/paused), monthly, or perPay on one agent. |
POST /v1/admin/reset-day |
Zero every agent's spentToday. |
POST /v1/admin/reset |
Reset the whole workspace back to its template, keeping the same keys. |
Approve / decline
curl -s -X POST "$PINK_BASE/v1/admin/requests/pay_c4821e4c4f85/approve" -H "authorization: Bearer $ADMIN" -H 'content-type: application/json' -d '{}'
{
"payment_id": "pay_c4821e4c4f85", "decision": "approved",
"credential": { "type": "virtual_card", "sandbox": true, "max_amount": 890, "currency": "USD", "locked_to": "Sysco", "single_use": true, "expires_at": "2026-09-30T21:21:49.073Z", "card": { "pan": "4111 1111 7481 6092", "exp": "12/27", "cvv": "922", "name": "PINK SANDBOX" } },
"approved_by": ["Sandbox admin"]
}
curl -s -X POST "$PINK_BASE/v1/admin/requests/pay_3adfda5d15c7/decline" -H "authorization: Bearer $ADMIN" -H 'content-type: application/json' -d '{}'
# → {"payment_id":"pay_3adfda5d15c7","decision":"declined","credential":null,"declined_by":["Sandbox admin"]}
by in the approve/decline body (default "Sandbox admin") names the approver; if it's not "Sandbox admin" the code still requires approvedBy.length >= approvers_needed before issuing the credential — a single non-default approver on a 2-of-3 rule leaves the hold pending_human.
PATCH an agent (pause it)
curl -s -i -X PATCH "$PINK_BASE/v1/admin/agents/a_mkt" -H "authorization: Bearer $ADMIN" -H 'content-type: application/json' -d '{"status":"paused"}'
HTTP/1.1 200 OK
{ "id": "a_mkt", "name": "Marketing AI", "dept": "Marketing", "vault": "v_mkt", "owner": "owner", "monthly": 3000, "perPay": 300, "spentMonth": 1840, "spentToday": 0, "status": "paused", "trust": 95, "key": "pk_sandbox_agent_...", "desc": "Runs Instagram and Meta ads on slow days." }
Note this returns the internal agent shape (dept, perPay, spentMonth, raw key) — different field names than the agent-facing pink.get_budget/me views. This endpoint is for the admin console, not for agents.
GET /v1/admin/state (trimmed)
{
"workspace": { "id": "o8fvsqi24z", "name": "Bloom & Bean", "template": "coffee", "policy_version": { "n": 1, "by": "Sandbox", "at": "..." }, "history": [...] },
"pending": [ /* holds awaiting a person, with agent_reason, evidence, trace */ ],
"transactions": [ /* last 100 payments, same shape as pending + agent_reason/evidence */ ],
"breakers": { "dailyCeiling": 6000, "velocity": 5, "unregistered": true },
"people": [...], "groups": [...], "vaults": [...], "agents": [...], "payees": [...], "rules": [...]
}
Workspaces and templates (no key required)
curl -s "$PINK_BASE/v1/sandbox/templates"
{
"templates": [
{ "id": "coffee", "label": "Coffee shop", "blurb": "Four agents, 11 rules. Purchasing, inventory, ads and payroll for a two-location café.", "agents": 4, "rules": 11 },
{ "id": "startup", "label": "Software startup · 50 people", "blurb": "Six agents, 17 rules. API-credit circuit breaker, CFO tier, 2-of-3 leadership above $5,000.", "agents": 6, "rules": 17 },
{ "id": "ecommerce", "label": "E-commerce · 120 people", "blurb": "Seven agents, 30 rules. PO matching, duplicate invoices, payee bank-detail changes, HKD/JPY, refund tiers.", "agents": 7, "rules": 30 }
]
}
curl -s -X POST "$PINK_BASE/v1/sandbox/workspaces" -H 'content-type: application/json' \
-d '{"company":"Bloom & Bean","email":"[email protected]","template":"coffee"}'
Returns 201 with admin_key, all 4 agent keys, and ready-to-use urls.mcp_with_key / urls.rest / urls.console — the exact object is shown in full in Quickstart.






