Agent Integration Patterns
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.
These patterns are tied directly to the tool signatures in Connect via MCP. Nothing below adds new API surface — it's how to use what's already defined in the sandbox.
Check before you work
Call pink.check_policy (or POST /v1/payments/check) before the agent commits effort to a plan that might not clear. It runs the exact same rule engine as request_payment but never spends, holds, or creates a payment record — so calling it as many times as you like costs nothing and changes nothing.
The returned decision is would_allow / would_ask / would_block — note these are different strings from request_payment's allowed / pending_human / blocked. They come from different code paths that both run the same underlying rule evaluation; don't write code that compares a check_policy result directly to a request_payment result as if they were the same enum.
Handling pending_human
When request_payment returns "decision": "pending_human", the response has a hold_id (identical string to payment_id) and expires_at (an ISO timestamp — verified value 30 minutes after created_at).
- Don't treat
pending_humanas a failure — it means a person has to look at it. The response also includeswho(a human-readable string of the approver(s), e.g."Luis Ortega (Store manager)") andapprovers_needed/approvals_so_far, useful for telling the user who they're waiting on. - Poll
pink.get_credential(hold_id)(orGET /v1/payments/{id}) untildecisionchanges. - After
expires_atpasses with no decision, the next poll flips the record to"decision": "expired", not"approved"— the sandbox checks the current time againstexpires_aton every poll and flips a still-pending hold toexpiredonce it's passed. This matches the product's own line, "No answer = no." Plan forexpiredas a distinct terminal state, separate fromdeclined(a person said no) andapproved(a person said yes). - If the final decision is
declined, the response hascredential: nullanddeclined_by: [...](the approver names). Ifapproved, it hascredentialandapproved_by: [...].
The credential is single-use, short-lived, and shaped by payment method
Every credential carries sandbox: true, max_amount, currency, locked_to (the payee name), single_use: true, and expires_at (verified: 15 minutes after issuance). Three concrete shapes, chosen by the payee's own method field (never by the agent):
type: "virtual_card"→card: { pan, exp, cvv, name: "PINK SANDBOX" }.panis always in the4111 1111 ****test range (Luhn-shaped, a sandbox test value, not a real card).type: "bank_transfer"→transfer: { rail, reference, status: "submitted" }.railis picked from the payment currency:ACH(USD),SEPA(EUR),FPS(GBP),FPS (HK)(HKD),FAST(SGD),Zengin(JPY),wireotherwise.type: "platform_refund"→platform: { name, reference }(for payees markedmethod: "platform", e.g. Stripe/Shopify refund payees in the e-commerce template).
An agent never chooses which of these it gets — the payee's own record decides. Don't branch on payment amount or currency to guess the credential shape; read credential.type from the response.
idempotency_key — verified behavior, not assumed
Unlike early API sketches (which flagged this as an open question), the real sandbox does implement idempotency: request_payment checks for an existing payment from the same agent with the same idempotency_key before evaluating anything, and if found, returns the original payment with "replayed": true added.
- Pass a unique
idempotency_keyper payment intent (not per HTTP attempt) so that retrying a timed-out network call is safe. - The dedup key is scoped to the calling agent — two different agents can reuse the same string safely.
- REST: the key can go in the request body (
idempotency_key) or theIdempotency-Keyheader; the header is only read as a fallback when the body field is absent. - This closes the exact gap the product's own "bug case" describes (a 03:14 retry loop trying to buy $49,600 of API credits) — but the $200/day rule in that scenario is still the real backstop;
idempotency_keyonly prevents duplicate submissions of the same intent, not an agent that keeps generating new intents.
Filing receipts
pink.report_receipt(payment_id, receipt) — verified: it raises the agent's trust score by exactly 1 per call (capped at 100; seed value in every template is 95), and returns { ok: true, payment_id, trust_score }. Use the payment_id from the request_payment/get_credential response, not hold_id from an earlier step in your own variable naming — in this API they're the same string, but keep the naming honest in your code since other payment systems don't guarantee that.
receipt is a loosely-typed object (merchant, total, currency, reference, url, note are all optional, and extra fields pass through) — there's no format validation in this sandbox build, so don't rely on the API to catch a malformed receipt.
What blocked means for the agent's plan
A "decision": "blocked" response has credential: null, retry_after: null (always, in this build — there is no automatic retry timer), and a why field naming the specific reason — either a named rule ("Never: gift cards, cash-like, crypto") or a budget/ceiling message the engine generates itself ("$3,756.40 used of $4,000 · this would exceed it"). Treat blocked as final for that specific request:
- Surface
whyto the user/log — it's the same trace a human approver would see withcheck_policy'stracearray. - Don't mechanically resubmit the identical
request_paymentcall on a block; if the cause is a per-transaction rule (e.g., a $500 cap), replan with a different amount/payee, not a retry loop. - If the cause is a budget/ceiling, check
pink.get_budgetbefore trying anything else in that vault — re-submitting will just produce anotherblockedresponse with the samewhy.
Rules are evaluated top-to-bottom, first match wins
pink.list_rules returns them in evaluation order, and check_policy's trace array shows every rule the engine walked, marked PASS (it didn't apply, kept going), SKIP (structurally didn't apply — wrong agent, wrong hours, missing evidence flag), or the final matching rule's action. If nothing matches, the default is block (verified: the gift-card rule and every template's rule list end with catch-alls, and giftcards.com — a payee not on any list — was blocked by the first rule, not fell through unblocked).
Two flag-gated rule types worth designing around explicitly if your agent works across templates:
requires: <flag>rules only match if you pass that flag inevidence_flags(e.g.,"statement"for a carrier-invoice rule in the e-commerce template) — the rule is silentlySKIPped without it, which usually routes the payment into a stricter, human-approval rule instead.absent: <flag>rules match only if you do not pass that flag (e.g., a purchase-order match:absent: "po"blocks a supplier payment with no PO reference).
An agent that never populates evidence_flags will get asked/blocked far more often than one that passes the flags its workflow actually has evidence for — this is by design ("Anything uncovered is blocked" — the product's own policy philosophy), not a bug to work around.






