Plaidly Docs

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.yaml is the source of truth for the public REST contract — read it from GitHub.
  • The spec at https://api.plaidly.io/openapi.json must 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 modelcode/message today, additive field/remediation/request_id/retryable once 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.

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/simulate

4) 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 match event_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"
}

On this page