ZeniPayZeniPay

Reference

API documentation

Everything you need to integrate ZeniPay — accounts, agents, cards, treasury, approvals, webhooks.

Authentication

Every ZeniPay API call is authenticated by a merchant API key. Create one in /app/settings → API Keys.

Every API call must include a Bearer token in the Authorization header. Keys live in your merchant settings at /app/settings → API Keys.

curl https://api.zenipay.ca/v1/agents \
  -H "Authorization: Bearer zpk_live_<your_key>"

Test vs live keys

zpk_test_ keys route to sandbox resources. zpk_live_ keys touch real money. Build against test, promote to live when the flow is stable.

Agents

Create, list, and manage AI agents. Each agent has a wallet with a balance, a policy, and a keypair signed by your org.

POST/api/v1/agentsCreate a new agent.
curl -X POST https://api.zenipay.ca/v1/agents \
  -H "Authorization: Bearer zpk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Marco",
    "agent_type": "sales",
    "description": "Qualifies inbound leads 24/7"
  }'
GET/api/v1/agentsList all agents in your org.
GET/api/v1/agents/:idFetch one agent + its wallet + policy.
PATCH/api/v1/agents/:idUpdate name / description / policy.

Treasury

Fund an org treasury and distribute to agents. Merchant → treasury transfers always land in the org wallet first; distribution to a specific agent is a separate action.

POST/api/v1/agents/treasury/distribute-from-merchantDebit a ZeniPay account, credit the org treasury.
POST/api/v1/agents/treasury/request-distributionSmart wrapper around distribute-to-agent. Creates an approval request when a rule matches, otherwise executes immediately.
POST/api/v1/agents/treasury/distribute-to-agentDebit the org treasury, credit an agent wallet.
POST/api/v1/agents/treasury/reclaim-from-agentReverse of distribute-to-agent. Debit agent wallet, credit treasury.
POST/api/v1/agents/treasury/return-to-merchantDebit the org treasury, credit a merchant ZeniPay account.
GET/api/v1/agents/treasury/eventsList funding events (card top-ups, ACH, wire, USDC).
curl -X POST https://api.zenipay.ca/v1/agents/treasury/request-distribution \
  -H "Authorization: Bearer zpk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to_agent_id": "agt_xxx",
    "amount_units": 250,
    "currency": "CAD",
    "idempotency_key": "ops-payroll-2026-05-01"
  }'

ZeniCards

Issue virtual or physical cards for agents. Every authorization runs through the tamper-evident ZeniCore ledger.

POST/api/v1/agents/zenicards/issueIssue a new card for an agent.
GET/api/v1/agents/zenicardsList cards in your org.
PATCH/api/v1/agents/zenicards/:id/statusFreeze, unfreeze, or cancel a card.

Webhooks

Subscribe to events to keep your own systems in sync. Every payload is signed with your webhook secret.

Events emitted:

  • payment.completed — customer payment settled.
  • payout.sent — withdrawal fired to an external destination.
  • approval.requested — merchant-rule approval pending.
  • card.charged — agent card authorized or settled.
  • agent.funded — agent wallet received funds from treasury.

Signature verification

import { createHmac, timingSafeEqual } from "node:crypto";

async function verify(req: Request, secret: string): Promise<boolean> {
  const sig = req.headers.get("x-zp-signature") ?? "";
  const body = await req.text();
  const expected = createHmac("sha256", secret).update(body).digest("hex");
  return timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
}

Errors

Every error response carries a stable { error: { code, message } } shape. Here are the codes you'll see most often.

HTTPCodeMeaning
400invalid_requestBody is malformed or missing required fields.
401unauthorizedAPI key missing, invalid, or revoked.
403forbiddenKey lacks permission for this resource.
404not_foundResource doesn’t exist or you can’t see it.
409conflictState conflict — e.g. already approved.
422validation_errorBusiness-rule violation (insufficient funds, currency mismatch).
429rate_limitedToo many requests. Back off and retry.
500internal_errorWe broke something. Retry and tell us if it recurs.