Skip to main content

Mandates

5 operations under /v1/mandates (GET POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.

12 min read
View MarkdownEdit on GitHub

Base URL: https://api.codespar.dev

Every operation below requires a Bearer token. See Authentication.

These five operations read and transition a consumer mandate: the allowance a consumer signed for one agent, with a purpose, a payee allowlist, per-currency caps and an expiry. None of them creates one. A mandate is born in the consent flow (POST /v1/consents, then the consumer signs at the hosted page) and is signed server-side, so there is no signing step on your side. The concept page, Mandates, receipts and approvals, has the matrix of what exists per channel (REST, SDK, CLI, dashboard, meta-tool), the lifecycle, and what is still not in this document.

The same four operations are also registered under /v1/consumers/mandates/..., the older spelling. Both spellings are described below and both work; the ones without /consumers are canonical, the others carry a deprecation mark. One handler serves each pair, so switching the path changes nothing else.

Every operation, from the spec

Generated from the published OpenAPI document, so it never drifts from what the API actually serves. The section above is written by hand and carries what a schema cannot: the object model, field rules, and the order to call things in.

GET /v1/mandates

GEThttps://api.codespar.dev/v1/mandates

List consumer allowances

The allowances a consumer has signed, newest first by creation time. Scoped to the credential's organization by a predicate in the SELECT, so nothing in the query can widen it.

limit caps the page at 200 and defaults to 50, and there is NO cursor: the response carries the rows and nothing to page with, so a consumer with more allowances than the limit is truncated in silence. Narrow with consumer_id and status rather than paging.

A status this filter does not know is a 400, not an empty list. That matters because an unrecognised value answering { "mandates": [] } reads as "this consumer has none" when it means "you asked for a state that does not exist".

The response object has one key, mandates, and no count.

GET /v1/consumers/mandates is the legacy spelling of this same operation, kept for two releases (ent#979): same handler, same mandates:read scope.

Query parameters

NameTypeRequiredDescription
consumer_idstringno—
limitintegerno—
status"active" | "paused" | "revoked" | "expired"no—

Responses

StatusBodyDescription
200objectOK
400objectThe query did not match the schema: an unknown status, a limit outside 1..200, or a limit that is not a number. details.issues carries the Zod issues.

Response 200

FieldTypeRequiredDescription
mandatesarray of objectyes—
Example request
curl -X GET https://api.codespar.dev/v1/mandates \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/mandates HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/mandates",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/mandates", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/mandates");
Example response 200
application/json
{
  "mandates": [
    {
      "id": "obj_0000000000000000",
      "consumer_id": "csm_0000000000000000",
      "agent_id": "agt_0000000000000000",
      "display_name": "Example",
      "purpose": "string",
      "merchant_allowlist": [
        "string"
      ],
      "merchant_pin_kind": "pix-key",
      "intent_note": "string",
      "cap_minor": "1000",
      "per_tx_cap_minor": "1000",
      "currency": "BRL",
      "status": "active",
      "expires_at": "2026-01-15T12:00:00.000Z",
      "created_at": "2026-01-15T12:00:00.000Z"
    }
  ]
}

GET /v1/mandates/{id}

GEThttps://api.codespar.dev/v1/mandates/{id}

Read one consumer allowance

The same 14-field projection the listing returns, by id, from the same table and under the same organization predicate, plus org_paused / org_paused_at: whether the organization's kill switch is pressed (ent#1648).

This path has NO alias. Its four siblings are registered twice each, under /v1/mandates/... and /v1/consumers/mandates/...; this one is registered once, because point reads of an allowance did not exist before it and there was no older spelling to keep alive.

Path parameters

NameTypeRequiredDescription
idstringyes—

Responses

StatusBodyDescription
200objectOK
404objectNo such allowance for this credential's organization. An allowance belonging to another organization answers exactly the same way: the org predicate sits in the SELECT, so the handler cannot tell absent from someone else's, and must not.

Response 200

FieldTypeRequiredDescription
agent_idstringyesThe agent this allowance authorizes to spend.
cap_minorstringyesTotal spend authorized over the life of the allowance, in minor units. A STRING: the column is bigint and the driver hands it back as text, so parse it as an integer and never as a float.
consumer_idstringyes—
created_atstring (date-time)yes—
currencystringyesLeft open here rather than closed. The column admits BRL, USD, MXN, COP, ARS, USDC and BRLA today, and that list is a CHECK constraint on our side rather than a promise on the wire.
dda_allowlistarray,nullyesDocuments this allowance authorizes to be registered for DDA (0238). Null: none.
display_namestring,nullyes—
expires_atstring (date-time)yesNever null: the column is NOT NULL.
fundingobject,nullyesThe rail and currency of a single-source allowance's funding source, never the source itself. Null for a multi-slot allowance (each slot names its rail) or when the source is not found.
idstringyes—
intent_notestring,nullyes—
merchant_allowlistarray of stringyesThe payees this allowance may pay, read as merchant_pin_kind says to read them. Never empty: consumer_mandates_allowlist_nonempty refuses a row whose allowlist is not an array of at least one entry, so an allowance that authorizes nothing cannot exist.
merchant_pin_kind"pix-key" | "merchant-id" | "mcc"yesHow each allowlist entry pins a payee. The v1 rail pins by Pix key.
org_pausedbooleanyesTrue while the organization has paused ALL agent spend (the kill switch, ent#1648). status is still this allowance's own stored state: a pause changes no mandate. While this is true every spend under this allowance is refused org_paused, so a caller that checks before executing should treat it as "do not execute" whatever status says.
org_paused_atstring,null (date-time)yesWhen the organization paused. Null when org_paused is false.
per_tx_cap_minorstringyesPer-transaction ceiling, in minor units. Same string shape as cap_minor.
periodic_capobject,nullyesThe renewing cap (0263): at most cap_minor per São Paulo calendar window. Null when the consent declared none, which means no period limit, not a zero one.
purposestringyes—
slotsarray,nullyesPer-currency slots of a multi-slot allowance (0092); null for a single-currency one. Each slot's funding source is not returned, as the top-level one is not.
status"active" | "paused" | "revoked" | "expired"yes—
usageobjectyesThe sums the spend path's cap gates run, in the allowance's own unit (the unit cap_minor is in: cents for a legacy v1 allowance even in USDC, the currency's native unit for v2).
withdrawal_allowlistarray,nullyesDestinations money may LEAVE to (0148). Null: this allowance authorizes no withdrawal.
Example request
curl -X GET https://api.codespar.dev/v1/mandates/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/mandates/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/mandates/{id}",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/mandates/{id}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/mandates/{id}", {
  path: {
    id: "mandate_0000000000000000"
  }
});
Example response 200
application/json
{
  "id": "obj_0000000000000000",
  "consumer_id": "csm_0000000000000000",
  "agent_id": "agt_0000000000000000",
  "display_name": "Example",
  "purpose": "string",
  "merchant_allowlist": [
    "string"
  ],
  "merchant_pin_kind": "pix-key",
  "intent_note": "string",
  "cap_minor": "1000",
  "per_tx_cap_minor": "1000",
  "currency": "BRL",
  "status": "active",
  "expires_at": "2026-01-15T12:00:00.000Z",
  "created_at": "2026-01-15T12:00:00.000Z",
  "org_paused": true,
  "org_paused_at": "2026-01-15T12:00:00.000Z",
  "periodic_cap": {
    "window": "day",
    "cap_minor": "1000"
  },
  "withdrawal_allowlist": [
    "string"
  ],
  "dda_allowlist": [
    "string"
  ],
  "slots": [
    {
      "currency": "BRL",
      "rail": "string",
      "cap_minor": "1000",
      "per_tx_cap_minor": "1000"
    }
  ],
  "funding": {
    "rail": "string",
    "currency": "BRL"
  },
  "usage": {
    "spent_minor": "1000",
    "period_spent_minor": "1000"
  }
}

POST /v1/mandates/{id}/pause

POSThttps://api.codespar.dev/v1/mandates/{id}/pause

Pause an allowance

Moves an active allowance to paused. active is the only legal source: a paused, revoked or expired allowance answers 409 rather than being quietly re-paused, and an allowance that is already paused answers 200 with changed: false and writes nothing.

Pausing stops the spend. The three places that spend against an allowance affirm the state they permit rather than listing the ones they refuse, so a status they do not recognise is refused rather than admitted.

The status flip and the evidence row are one transaction, and the UPDATE is guarded on the status this request read.

Who may call it: a project API key or an OAuth token holding mandates:write, or the dashboard acting for a verified admin or owner (x-codespar-user-token). The evidence row and the commerce.mandate.* event name the actor as actor_kind and actor_id.

POST /v1/consumers/mandates/{id}/pause is the legacy spelling, kept for two releases (ent#979): same handler, same mandates:write scope.

Path parameters

NameTypeRequiredDescription
idstringyes—

Request body

FieldTypeRequiredDescription
reasonstringnoRecorded in the evidence row's metadata, alongside the from/to pair. Not echoed in the response.

Responses

StatusBodyDescription
200objectOK
400objectA body was sent and did not match the schema. details.issues carries the Zod issues.
403objectService auth (the dashboard) did not establish an admin. user_token_required: no x-codespar-user-token was forwarded, and a service key alone never moves a mandate. user_token_invalid: the token did not verify; mint a fresh one. user_token_identity_mismatch: x-codespar-user names somebody else. insufficient_role: that person is not an admin or owner of the organization. A project API key without mandates:write gets the scope refusal instead. Nothing was written. Bare { error, remediation } body.
404objectNo such allowance for this credential's organization. An allowance belonging to another organization answers exactly the same way: the org predicate sits in the SELECT, so the handler cannot tell absent from someone else's, and must not.
409objectinvalid_transition when the allowance's current status is not a legal source for this verb. transition_conflict when another request moved the allowance between this one's read and its write: the UPDATE is guarded on the status that was read, so a concurrent transition loses rather than double-applying, and the evidence row is written in the same transaction as the status change or not at all.
503objectuser_token_verification_unconfigured: a deployment fault on the dashboard path, not a caller fault; retrying the same request will not help. Bare body.

Response 200

FieldTypeRequiredDescription
changedbooleanyesFalse when the allowance was already in this state. Nothing was written and no evidence row was appended.
mandateobjectyes—
Example request
curl -X POST https://api.codespar.dev/v1/mandates/{id}/pause \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "reason": "string"
     }'
POST /v1/mandates/{id}/pause HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "reason": "string"
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/mandates/{id}/pause",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "reason": "string"
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/mandates/{id}/pause", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "reason": "string"
  }),
});

const data = await res.json();
const r = await cs.api.response("post", "/v1/mandates/{id}/pause", {
  path: {
    id: "mandate_0000000000000000"
  },
  body: {
    reason: "string"
  }
});
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
  console.log(r.data);
}
Example response 200
application/json
{
  "mandate": {
    "id": "obj_0000000000000000",
    "status": "paused"
  },
  "changed": true
}

POST /v1/mandates/{id}/resume

POSThttps://api.codespar.dev/v1/mandates/{id}/resume

Resume a paused allowance

Moves a paused allowance back to active. paused is the only legal source, so a revoked allowance answers 409 and does not come back to life.

Caps and expiry are untouched: the UPDATE sets status and updated_at and nothing else, so the allowance resumes with the spend it had already used still counted against it. Resuming an allowance whose expires_at has passed is legal here and still buys nothing, because the spend path refuses an expired mandate on its own.

Who may call it: a project API key or an OAuth token holding mandates:write, or the dashboard acting for a verified admin or owner (x-codespar-user-token). The evidence row and the commerce.mandate.* event name the actor as actor_kind and actor_id.

POST /v1/consumers/mandates/{id}/resume is the legacy spelling, kept for two releases (ent#979): same handler, same mandates:write scope.

Path parameters

NameTypeRequiredDescription
idstringyes—

Request body

FieldTypeRequiredDescription
reasonstringnoRecorded in the evidence row's metadata, alongside the from/to pair. Not echoed in the response.

Responses

StatusBodyDescription
200objectOK
400objectA body was sent and did not match the schema. details.issues carries the Zod issues.
403objectService auth (the dashboard) did not establish an admin. user_token_required: no x-codespar-user-token was forwarded, and a service key alone never moves a mandate. user_token_invalid: the token did not verify; mint a fresh one. user_token_identity_mismatch: x-codespar-user names somebody else. insufficient_role: that person is not an admin or owner of the organization. A project API key without mandates:write gets the scope refusal instead. Nothing was written. Bare { error, remediation } body.
404objectNo such allowance for this credential's organization. An allowance belonging to another organization answers exactly the same way: the org predicate sits in the SELECT, so the handler cannot tell absent from someone else's, and must not.
409objectinvalid_transition when the allowance's current status is not a legal source for this verb. transition_conflict when another request moved the allowance between this one's read and its write: the UPDATE is guarded on the status that was read, so a concurrent transition loses rather than double-applying, and the evidence row is written in the same transaction as the status change or not at all.
503objectuser_token_verification_unconfigured: a deployment fault on the dashboard path, not a caller fault; retrying the same request will not help. Bare body.

Response 200

FieldTypeRequiredDescription
changedbooleanyesFalse when the allowance was already in this state. Nothing was written and no evidence row was appended.
mandateobjectyes—
Example request
curl -X POST https://api.codespar.dev/v1/mandates/{id}/resume \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "reason": "string"
     }'
POST /v1/mandates/{id}/resume HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "reason": "string"
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/mandates/{id}/resume",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "reason": "string"
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/mandates/{id}/resume", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "reason": "string"
  }),
});

const data = await res.json();
const r = await cs.api.response("post", "/v1/mandates/{id}/resume", {
  path: {
    id: "mandate_0000000000000000"
  },
  body: {
    reason: "string"
  }
});
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
  console.log(r.data);
}
Example response 200
application/json
{
  "mandate": {
    "id": "obj_0000000000000000",
    "status": "active"
  },
  "changed": true
}

POST /v1/mandates/{id}/revoke

POSThttps://api.codespar.dev/v1/mandates/{id}/revoke

Revoke an allowance

Moves an active or paused allowance to revoked, and there is no verb here that moves it back: resume accepts paused only. An expired allowance answers 409, because expiry is already terminal for spending and revoking it would only rewrite which terminal state the evidence log records.

Revoking stops the spend on the same three gates that pausing does.

Who may call it: a project API key or an OAuth token holding mandates:write, or the dashboard acting for a verified admin or owner (x-codespar-user-token). The evidence row and the commerce.mandate.* event name the actor as actor_kind and actor_id.

POST /v1/consumers/mandates/{id}/revoke is the legacy spelling, kept for two releases (ent#979): same handler, same mandates:write scope.

Path parameters

NameTypeRequiredDescription
idstringyes—

Request body

FieldTypeRequiredDescription
reasonstringnoRecorded in the evidence row's metadata, alongside the from/to pair. Not echoed in the response.

Responses

StatusBodyDescription
200objectOK
400objectA body was sent and did not match the schema. details.issues carries the Zod issues.
403objectService auth (the dashboard) did not establish an admin. user_token_required: no x-codespar-user-token was forwarded, and a service key alone never moves a mandate. user_token_invalid: the token did not verify; mint a fresh one. user_token_identity_mismatch: x-codespar-user names somebody else. insufficient_role: that person is not an admin or owner of the organization. A project API key without mandates:write gets the scope refusal instead. Nothing was written. Bare { error, remediation } body.
404objectNo such allowance for this credential's organization. An allowance belonging to another organization answers exactly the same way: the org predicate sits in the SELECT, so the handler cannot tell absent from someone else's, and must not.
409objectinvalid_transition when the allowance's current status is not a legal source for this verb. transition_conflict when another request moved the allowance between this one's read and its write: the UPDATE is guarded on the status that was read, so a concurrent transition loses rather than double-applying, and the evidence row is written in the same transaction as the status change or not at all.
503objectuser_token_verification_unconfigured: a deployment fault on the dashboard path, not a caller fault; retrying the same request will not help. Bare body.

Response 200

FieldTypeRequiredDescription
changedbooleanyesFalse when the allowance was already in this state. Nothing was written and no evidence row was appended.
mandateobjectyes—
Example request
curl -X POST https://api.codespar.dev/v1/mandates/{id}/revoke \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "reason": "string"
     }'
POST /v1/mandates/{id}/revoke HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "reason": "string"
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/mandates/{id}/revoke",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "reason": "string"
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/mandates/{id}/revoke", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "reason": "string"
  }),
});

const data = await res.json();
const r = await cs.api.response("post", "/v1/mandates/{id}/revoke", {
  path: {
    id: "mandate_0000000000000000"
  },
  body: {
    reason: "string"
  }
});
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
  console.log(r.data);
}
Example response 200
application/json
{
  "mandate": {
    "id": "obj_0000000000000000",
    "status": "revoked"
  },
  "changed": true
}
Mandates | CodeSpar