Agent Contract Page
Agent-facing OpenAPI and SDK contract details for Plaidly
Agent OpenAPI / SDK Contracts
This page is the canonical contract surface for agent automation.
All examples use network: "testnet" to match the sandbox-first onboarding flow.
Shared conventions
- Base URL:
https://api.plaidly.io - Auth:
- Public/read-only endpoints: no auth
- Merchant-scoped endpoints:
X-API-Key: plaidly_sk_...
- Payload naming: camelCase for nested objects (
paymentMethod,methodID) and snake_case for response and event body fields - Mainnet gates: sandbox merchants are testnet-only; mainnet payment sessions and payout requests require merchant KYC verification
Contract guardrails
plaidly-api/api/openapi.yamlis the source of truth for the public REST contract — read it from GitHub.- The spec at
https://api.plaidly.io/openapi.jsonmust stay aligned with the committed spec. It currently requires the same auth as merchant endpoints (verified 2026-07-11, see API reference) rather than being publicly fetchable — that gap is tracked as a docs/API drift item, not treated as intended behavior. - Every contract shown below must stay aligned with the committed spec.
- The Node, Python, and Go SDKs are validated against the same payment-session, merchant, webhook, and receipt behaviors in their unit tests.
- Any breaking API change should fail the docs build or SDK test surface before release; see Versioning & deprecation policy for what counts as breaking.
- All error responses use the envelope in Error model —
code/messagetoday, additivefield/remediation/request_id/retryableonce BDT-541's planned envelope ships.
Catalog contract (store / product / plan / price)
BDT-223 shipped and is deployed: store, product, plan, and price resources, plus public buyer-agent discovery and checkout-intent resolution, are live under /v1/stores, /v1/products, /v1/plans, /v1/prices, /v1/catalog/products, and /v1/catalog/prices/{price_id}/checkout-intent. There is no distinct entitlement resource — entitlement is a freeform object field on plan. Full contract: Create a store, product, and price and Create or select a checkout. Per-merchant rail-policy intersection (as opposed to the per-price allowed_payment_methods restriction, which is live) is still planned — see Configure rails. The Plaidly CLI (BDT-544) is also in parallel development and not referenced by SDK parity notes below until it ships.
Operational surfaces
When an agent needs to inspect money movement or control boundaries, use the operational docs alongside the API contract.
- Wallet and tenant limits
- Audit log events
- Ledger and settlement view
- Webhook delivery status
- RPC provider health metrics
- Reconciliation export
1) register_merchant
Public/sandbox registration is a four-step flow as of BDT-542 (email verification, confirm, proof-of-work, then this call) with a default-deny email trust gate. Full step-by-step contract, the proof-of-work solve algorithm, and the trust-gate caveat: Register a merchant. The schema below is only the final POST /v1/merchants call.
REST contract
POST /v1/merchants/email-verification(step 1)POST /v1/merchants/email-verification/confirm(step 2)POST /v1/merchants/registration-proof-of-work(step 3)POST /v1/merchants(step 4)
Request schema (RegisterMerchantRequest, step 4)
{
"type": "object",
"required": ["name"],
"properties": {
"name": {
"type": "string",
"description": "Merchant display name"
},
"sandbox": {
"type": "boolean",
"description": "Create this merchant in sandbox mode. Defaults to false."
},
"webhook_url": {
"type": "string",
"description": "Optional webhook URL for payment events"
},
"idempotency_key": {
"type": "string",
"description": "Idempotency key for replay-safe onboarding retries"
},
"registration_intent_id": {
"type": "string",
"description": "Required for public/sandbox registration: the id returned by POST /v1/merchants/email-verification, after email verification and (unless invitation-exempt) proof-of-work are complete"
},
"proof_of_work_challenge_id": {
"type": "string",
"description": "Required for public/sandbox registration unless the registration intent carries a valid invitation token"
},
"proof_of_work_nonce": {
"type": "string",
"description": "The nonce solving the proof-of-work challenge"
}
}
}Response schema (Merchant)
{
"type": "object",
"required": ["id", "name", "api_key", "created_at"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"email": { "type": "string" },
"api_key": { "type": "string" },
"webhook_url": { "type": "string" },
"webhook_secret": { "type": "string", "description": "Response-only signing secret for webhook verification" },
"rate_limit_per_minute": { "type": "integer", "format": "int64" },
"created_at": { "type": "string" }
}
}SDK parity
- Go SDK:
client.RegisterMerchant(ctx, plaidly.RegisterMerchantRequest) - Node SDK:
client.merchants.register(RegisterMerchantRequest) - Python SDK:
client.merchants.register(name, webhook_url=None)
Example
This is only the final call — registration_intent_id and, unless invitation-exempt, proof_of_work_challenge_id/proof_of_work_nonce come from steps 1-3, and the email itself must pass the trust gate. See Register a merchant for the full sequence.
curl -X POST https://api.plaidly.io/v1/merchants \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Test Co",
"sandbox": true,
"webhook_url": "https://example.org/webhooks/plaidly",
"registration_intent_id": "ri_01HX9K...",
"proof_of_work_challenge_id": "pow_01HX9K...",
"proof_of_work_nonce": "482913"
}'2) create_payment_session
REST contract
POST /v1/payment_sessions
Request schema (CreatePaymentSessionRequest)
{
"type": "object",
"required": ["amount", "expires_in", "paymentMethod"],
"properties": {
"amount": { "type": "number", "description": "Expected amount to receive" },
"expires_in": { "type": "string", "description": "Duration string, e.g. \"15m\", \"1h\"" },
"metadata": { "type": "object", "description": "Optional extra fields (max 4KB)" },
"paymentMethod": {
"type": "object",
"required": ["methodID", "chain", "token", "network"],
"properties": {
"methodID": {
"type": "integer",
"enum": [0, 1],
"description": "0 = on-chain crypto, 1 = fiat"
},
"chain": { "type": "string" },
"token": { "type": "string" },
"network": { "type": "string", "enum": ["testnet", "mainnet"] }
}
}
},
"additionalProperties": false
}Response schema (PaymentSession)
{
"type": "object",
"required": [
"session_id",
"merchant_id",
"expected_amount",
"received_amount",
"address",
"status",
"metadata",
"expires_at",
"created_at",
"updated_at",
"paymentMethod",
"demo"
],
"properties": {
"session_id": { "type": "string" },
"merchant_id": { "type": "string" },
"expected_amount": { "type": "number" },
"received_amount": { "type": "number" },
"address": { "type": "string" },
"status": { "type": "string" },
"metadata": { "type": "object" },
"expires_at": { "type": "string" },
"completed_at": { "type": "string" },
"created_at": { "type": "string" },
"updated_at": { "type": "string" },
"demo": { "type": "boolean" },
"currency": { "type": "string" },
"payment_url": { "type": "string" },
"qr_data": { "type": "string" },
"explorer_url": { "type": "string" },
"paymentMethod": {
"type": "object",
"required": ["methodID", "chain", "token", "network"]
}
}
}SDK parity
- Go SDK:
client.CreatePaymentSession(ctx, plaidly.CreatePaymentSessionRequest) - Node SDK:
client.paymentSessions.create(CreatePaymentSessionRequest) - Python SDK:
client.payment_sessions.create(amount, chain, token, network="mainnet", expires_in="15m", method_id=0, metadata=None)
Example
curl -X POST https://api.plaidly.io/v1/payment_sessions \
-H "X-API-Key: plaidly_sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"amount": 25.00,
"expires_in": "45m",
"paymentMethod": {
"methodID": 0,
"chain": "solana",
"token": "USDC",
"network": "testnet"
},
"metadata": { "order_id": "order_01HX9K" }
}'3) simulate_payment
REST contract
POST /v1/payment_sessions/{session_id}/simulate- Auth: optional, but canonical flow is sandbox/demo only
Request schema
No request body.
Response schema
PaymentSession (same object schema as above).
SDK parity
- Go SDK:
client.SimulatePayment(ctx, sessionID) - Node SDK:
client.paymentSessions.simulate(sessionId) - Python SDK:
client.payment_sessions.simulate(session_id)
Example
curl -X POST https://api.plaidly.io/v1/payment_sessions/sess_test_abc123/simulate4) attempt_agent_wallet_spend
This is the current agent-wallet outbound spend-attempt surface. It evaluates
merchant ownership, sandbox/mainnet eligibility, KYC, and spend-policy rules
before any chain broadcast exists. Allowed attempts return pending and create
a pending spent ledger row; approval-gated attempts return
approval_required; denied attempts never create a ledger row.
REST contract
POST /v1/agent-wallets/{agent_wallet_id}/spend-attempts
Request schema (AgentSpendAttemptRequest)
{
"type": "object",
"required": ["target", "amount", "amount_subunits", "token_symbol", "chain", "network"],
"properties": {
"target": {
"type": "string",
"description": "Destination address, API payee, or resource target"
},
"amount": {
"type": "number",
"description": "Human-readable amount"
},
"amount_subunits": {
"type": "string",
"description": "Integer token subunits"
},
"token_symbol": {
"type": "string"
},
"chain": {
"type": "string"
},
"network": {
"type": "string",
"enum": ["testnet", "mainnet"]
},
"api_domain": { "type": "string" },
"reference": { "type": "string" },
"metadata": { "type": "object" }
},
"additionalProperties": false
}Response schema (AgentSpendAttempt)
{
"type": "object",
"required": [
"id",
"merchant_id",
"agent_wallet_id",
"target",
"amount",
"amount_subunits",
"token_symbol",
"chain",
"network",
"status",
"created_at"
],
"properties": {
"id": { "type": "string" },
"merchant_id": { "type": "string" },
"agent_wallet_id": { "type": "string" },
"target": { "type": "string" },
"amount": { "type": "number" },
"amount_subunits": { "type": "string" },
"token_symbol": { "type": "string" },
"chain": { "type": "string" },
"network": { "type": "string" },
"status": { "type": "string", "enum": ["pending", "approval_required", "denied"] },
"reason": { "type": "string" },
"policy_id": { "type": "string" },
"ledger_entry_id": { "type": "string" },
"spend_reference": { "type": "string" },
"created_at": { "type": "string" }
}
}SDK parity
The REST surface is live first; SDK convenience wrappers can follow the same shape as generated contracts update.
Example
curl -X POST https://api.plaidly.io/v1/agent-wallets/aw_test_123/spend-attempts \
-H "X-API-Key: plaidly_sk_..." \
-H "Content-Type: application/json" \
-d '{
"target": "0xabc123...",
"amount": 25,
"amount_subunits": "25000000",
"token_symbol": "USDC",
"chain": "ethereum",
"network": "testnet",
"api_domain": "api.example.com",
"reference": "spend_demo_01"
}'5) verify_webhook
Incoming payload contract (WebhookEvent)
{
"type": "object",
"required": ["event_type", "session_id", "status", "amount", "currency", "chain", "network", "timestamp"],
"properties": {
"event_type": {
"type": "string",
"enum": [
"payment_session.completed",
"payment_session.expired",
"payment_session.partial_paid"
]
},
"session_id": { "type": "string" },
"status": { "type": "string" },
"amount": { "type": "number" },
"currency": { "type": "string" },
"chain": { "type": "string" },
"network": { "type": "string", "enum": ["testnet", "mainnet"] },
"timestamp": { "type": "string" }
}
}Required headers
X-Plaidly-Event(must matchevent_type)X-Plaidly-Signature(t=<unix>,v1=<hex>)
SDK parity
- Go SDK:
webhook.VerifyWebhook(payload, signatureHeader, merchantSecret, tolerance) - Node SDK:
webhook.verifyWebhookSignature(payload, signatureHeader, secret) - Python SDK:
verify_webhook_signature(payload, signature, secret)
Example
curl -X POST https://merchant.example.com/webhooks/plaidly \
-H "Content-Type: application/json" \
-H "X-Plaidly-Event: payment_session.completed" \
-H "X-Plaidly-Signature: t=1718310000,v1=<hex>" \
-d '{
"event_type": "payment_session.completed",
"session_id": "sess_test_01HX9K...",
"status": "completed",
"amount": 25,
"currency": "USDC",
"chain": "solana",
"network": "testnet",
"timestamp": "1718310000"
}'6) pay_x402_resource
x402 challenge schema (X402Challenge)
{
"type": "object",
"properties": {
"version": { "type": "string" },
"scheme": { "type": "string", "description": "Payment scheme, e.g. exact" },
"network": { "type": "string", "enum": ["testnet", "mainnet"] },
"maxAmountRequired": { "type": "string" },
"amount": { "type": "string" },
"asset": { "type": "string" },
"payTo": { "type": "string" },
"recipient": { "type": "string" },
"memo": { "type": "string" },
"resource": { "type": "string" },
"description": { "type": "string" }
},
"additionalProperties": false
}Transport headers
Resource servers return 402 Payment Required with one of:
PAYMENT-REQUIRED(base64 JSON challenge payload)X-PAYMENT-REQUIRED(same)
Clients then retry with:
X-Plaidly-Payment: <proof>
Optional verifier header from providers:
PAYMENT-SIGNATURE
SDK parity
- Go SDK:
plaidly.RetryRequestWithX402Proof(ctx, doRequest, proofProvider, facilitator, out) - Node SDK:
retryRequestWithX402Proof(doRequest, options) - PHP SDK:
X402::parseChallenge(...),X402::resolveProof(...),X402::proofHeaders($proof) - Python SDK:
retry_request_with_x402_proof(do_request, proof_provider=..., facilitator=...)
Example challenge
{
"version": "1",
"scheme": "exact",
"network": "testnet",
"maxAmountRequired": "1000000",
"amount": "1000000",
"asset": "USDC",
"payTo": "0xTestAddr...",
"recipient": "Plaidly-Agent",
"memo": "checkout-order-01",
"resource": "/agent-invoice/ord_01HX9K",
"description": "Testnet payment for Plaidly checkout invoice"
}