Skip to main content

Consumers

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

7 min read
View MarkdownEdit on GitHub

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

Every operation below requires a Bearer token. See Authentication.

GET /v1/consumers

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

List titulars

verified=true|false filters on the stored status, which is a GENERATED column derived from whether a document is linked — so the filter cannot disagree with the row it returns.

Query parameters

NameTypeRequiredDescription
limitintegerno—
verified"true" | "false"no—

Responses

StatusBodyDescription
200objectOK
400objectBad Request — the body or query did not match the schema.

Response 200

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

res = requests.get(
    "https://api.codespar.dev/v1/consumers",
    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/consumers", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/consumers");
Example response 200
application/json
{
  "consumers": [
    {
      "id": "obj_0000000000000000",
      "display_name": "Example",
      "metadata": {},
      "status": "unverified",
      "document": "string",
      "document_verified_at": "string",
      "document_source": "string",
      "person_type": "string",
      "tax_type": "string",
      "created_at": "string"
    }
  ]
}

POST /v1/consumers

POSThttps://api.codespar.dev/v1/consumers

Create the titular, without a document

Creates the holder of the money as UNVERIFIED. There is deliberately no field for a document: the document→consumer link is born exactly once, already verified, from an approved account application. A body carrying document is REFUSED with 400 rather than accepted with the field ignored — being told is the point, because a caller who believed the document was stored would carry that belief into every later call.

The id is server-minted (cons_ + nanoid). The free caller-supplied string is the shape this resource replaces.

Request body

FieldTypeRequiredDescription
display_namestringno—
metadataobjectno—

Responses

StatusBodyDescription
201objectOK
400objectBad Request — the body or query did not match the schema.

Response 201

FieldTypeRequiredDescription
created_atstringyes—
display_namestring,nullyes—
documentstring,nullyes—
document_sourcestring,nullyes—
document_verified_atstring,nullyes—
idstringyes—
metadataobjectyes—
person_typestring,nullyes—
status"unverified" | "verified"yes—
tax_typestring,nullyes—
Example request
curl -X POST https://api.codespar.dev/v1/consumers \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "display_name": "Example",
       "metadata": {}
     }'
POST /v1/consumers HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "display_name": "Example",
  "metadata": {}
}
import os
import requests

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

const data = await res.json();
const result = await cs.api.post("/v1/consumers", {
  body: {
    display_name: "Example",
    metadata: {}
  }
});
Example response 201
application/json
{
  "id": "obj_0000000000000000",
  "display_name": "Example",
  "metadata": {},
  "status": "unverified",
  "document": "string",
  "document_verified_at": "string",
  "document_source": "string",
  "person_type": "string",
  "tax_type": "string",
  "created_at": "string"
}

GET /v1/consumers/consents

GEThttps://api.codespar.dev/v1/consumers/consents
Deprecated

List consent evidence (deprecated path)

DEPRECATED alias of GET /v1/consent-records (ent#979), kept for two releases. Same handler, same scope; switch the path and nothing else changes.

Query parameters

NameTypeRequiredDescription
consumer_idstringno—
limitintegerno—

Responses

StatusBodyDescription
200objectOK
400objectThe query did not match the schema.

Response 200

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

res = requests.get(
    "https://api.codespar.dev/v1/consumers/consents",
    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/consumers/consents", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/consumers/consents");
Example response 200
application/json
{
  "consents": [
    {
      "id": "obj_0000000000000000",
      "consumer_id": "csm_0000000000000000",
      "funding_source_id": "fundingsource_0000000000000000",
      "agent_id": "agt_0000000000000000",
      "kind": "string",
      "hmac_secret_version": 0,
      "cap_minor": 1,
      "per_tx_cap_minor": 1,
      "currency": "BRL",
      "purpose": "string",
      "ip_address": "string",
      "signed_at": "2026-01-15T12:00:00.000Z",
      "expires_at": "2026-01-15T12:00:00.000Z",
      "metadata": {}
    }
  ]
}

GET /v1/consumers/funding-sources

GEThttps://api.codespar.dev/v1/consumers/funding-sources
Deprecated

List consumer funding sources, optionally with live balance (deprecated path)

DEPRECATED alias of GET /v1/funding-sources (ent#979), kept for two releases, but with a behavioral difference the canonical path does not have, which is why it is not a path swap and nothing more.

?with_balance=1 works HERE and not on the canonical path. For each pix-celcoin row, the live balance is fetched from the provider, one call per row. The canonical path suppresses that because a listing of every consumer would become a fan-out of N calls per request. When you migrate to the canonical path, ask for the balance per consumer instead of for the whole list.

Query parameters

NameTypeRequiredDescription
consumer_idstringno—
limitintegerno—
with_balancestringno1 or true turns the live balance lookup on. Any other value does not.

Responses

StatusBodyDescription
200objectOK
400objectThe query did not match the schema.

Response 200

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

res = requests.get(
    "https://api.codespar.dev/v1/consumers/funding-sources",
    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/consumers/funding-sources", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/consumers/funding-sources");
Example response 200
application/json
{
  "funding_sources": [
    {
      "id": "obj_0000000000000000",
      "project_id": "prj_0000000000000000",
      "consumer_id": "csm_0000000000000000",
      "rail": "string",
      "currency": "BRL",
      "provider": "string",
      "display_label": "Example",
      "status": "string",
      "created_at": "2026-01-15T12:00:00.000Z",
      "expires_at": "2026-01-15T12:00:00.000Z",
      "revoked_at": "2026-01-15T12:00:00.000Z",
      "account_masked": "string",
      "branch": "string",
      "metadata": {},
      "balance_minor": 1000
    }
  ],
  "unattributed_count": 1
}

GET /v1/consumers/mandates

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

List consumer allowances (legacy path)

DEPRECATED alias of GET /v1/mandates (ent#979), kept for two releases. Same handler, same mandates:read scope; switch the path and nothing else changes. The truncation and the 400-on-unknown-status behaviour described on the canonical path apply here unchanged.

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/consumers/mandates \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/consumers/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/consumers/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/consumers/mandates", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/consumers/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/consumers/{id}

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

Read one titular, with the ids of what hangs off it

The hub: the row plus wallet_id, funding_source_ids, mandate_ids and dda_subscription_documents, so a caller navigates from the holder instead of filtering every sub-resource collection.

document is masked here and everywhere else on this surface: last four characters, the rest replaced. Pix keys have no section because they are not a table — a registered key is cached on its funding source, so it is reached through funding_source_ids.

Responses

StatusBodyDescription
200objectOK
404objectNot Found

Response 200

FieldTypeRequiredDescription
created_atstringyes—
display_namestring,nullyes—
documentstring,nullyes—
document_sourcestring,nullyes—
document_verified_atstring,nullyes—
idstringyes—
metadataobjectyes—
person_typestring,nullyes—
status"unverified" | "verified"yes—
tax_typestring,nullyes—
Example request
curl -X GET https://api.codespar.dev/v1/consumers/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/consumers/{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/consumers/{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/consumers/{id}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/consumers/{id}");
Example response 200
application/json
{
  "id": "obj_0000000000000000",
  "display_name": "Example",
  "metadata": {},
  "status": "unverified",
  "document": "string",
  "document_verified_at": "string",
  "document_source": "string",
  "person_type": "string",
  "tax_type": "string",
  "created_at": "string"
}

PATCH /v1/consumers/{id}

PATCHhttps://api.codespar.dev/v1/consumers/{id}

Update the mutable fields of a titular

display_name, metadata and tax_type. document, person_type, the id and the org are immutable and are refused — by this schema and, independently, by a database trigger, so reaching past one still meets the other.

A tax_type transition is recorded with its previous value, its new value and the key that asked, in the same transaction as the update. A patch that leaves tax_type alone, or resends its current value, records nothing.

Request body

FieldTypeRequiredDescription
display_namestring,nullno—
metadataobjectno—
tax_type"PJ" | "MEI" | "ME" | "PF"no—

Responses

StatusBodyDescription
200objectOK
400objectBad Request — the body or query did not match the schema.
404objectNot Found

Response 200

FieldTypeRequiredDescription
created_atstringyes—
display_namestring,nullyes—
documentstring,nullyes—
document_sourcestring,nullyes—
document_verified_atstring,nullyes—
idstringyes—
metadataobjectyes—
person_typestring,nullyes—
status"unverified" | "verified"yes—
tax_typestring,nullyes—
Example request
curl -X PATCH https://api.codespar.dev/v1/consumers/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "display_name": "Example",
       "metadata": {},
       "tax_type": "PJ"
     }'
PATCH /v1/consumers/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "display_name": "Example",
  "metadata": {},
  "tax_type": "PJ"
}
import os
import requests

res = requests.patch(
    "https://api.codespar.dev/v1/consumers/{id}",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "display_name": "Example",
      "metadata": {},
      "tax_type": "PJ"
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/consumers/{id}", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "display_name": "Example",
    "metadata": {},
    "tax_type": "PJ"
  }),
});

const data = await res.json();
const result = await cs.api.patch("/v1/consumers/{id}", {
  body: {
    display_name: "Example",
    metadata: {},
    tax_type: "PJ"
  }
});
Example response 200
application/json
{
  "id": "obj_0000000000000000",
  "display_name": "Example",
  "metadata": {},
  "status": "unverified",
  "document": "string",
  "document_verified_at": "string",
  "document_source": "string",
  "person_type": "string",
  "tax_type": "string",
  "created_at": "string"
}

GET /v1/consumers/{id}/wallet

GEThttps://api.codespar.dev/v1/consumers/{id}/wallet

The consumer's unified wallet, rolled up per currency

One line per currency the consumer has spend authority in, each carrying what the agent is authorized to spend, what it has spent, and what is left.

Caps are PER CURRENCY and no FX is ever applied. A BRL authorization and a USDC authorization are separate slots with separate ceilings, and nothing here converts one into the other. available_minor is authorized minus spent and is NOT floored at zero: an overrun is reported as a negative value with overspent: true, never erased.

authorized_minor is a CEILING, not a balance and not a charge: it is the sum of the slot caps across the consumer's active mandates, so it says what may be spent, not what exists. spent_minor counts settled debits only.

Legacy single-currency mandates fold in as a one-currency wallet, so the shape is uniform. A consumer with no mandates answers 200 with an empty currencies array, not 404: this route asserts nothing about whether the consumer exists.

Path parameters

NameTypeRequiredDescription
idstringyes—

Responses

StatusBodyDescription
200objectOK

Response 200

FieldTypeRequiredDescription
consumer_idstringyes—
currenciesarray of objectyes—
Example request
curl -X GET https://api.codespar.dev/v1/consumers/{id}/wallet \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/consumers/{id}/wallet HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/consumers/{id}/wallet",
    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/consumers/{id}/wallet", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/consumers/{id}/wallet", {
  path: {
    id: "csm_0000000000000000"
  }
});
Example response 200
application/json
{
  "consumer_id": "csm_0000000000000000",
  "currencies": [
    {
      "currency": "BRL",
      "rail": "string",
      "authorized_minor": 1,
      "spent_minor": 1,
      "available_minor": 1,
      "overspent": true,
      "funding_source_ids": [
        "string"
      ],
      "mandate_ids": [
        "string"
      ]
    }
  ]
}
Consumers | CodeSpar