Skip to main content

Cards

5 operations (GET POST): 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/cards

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

List issued cards

The cards issued in this project, most recent first.

This listing carries CARDS, not mint flows: a POST that stopped at 202 waiting for KYC does not appear here until it becomes a card. To follow that flow, read the flow_id the issuance returned.

Query parameters

NameTypeRequiredDescription
consumer_idstringnoFilters to one consumer's cards.
limitintegerno1 to 200; the default is 50.

Responses

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

Response 200

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

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

const data = await res.json();
const result = await cs.api.get("/v1/cards");
Example response 200
application/json
{
  "cards": [
    {
      "id": "obj_0000000000000000",
      "issuer": "pomelo",
      "card_id": "card_0000000000000000",
      "last4": "string",
      "network": "visa",
      "status": "active",
      "token_id": "token_0000000000000000",
      "cde": "ingested",
      "created_at": "2026-01-15T12:00:00.000Z"
    }
  ]
}

POST /v1/cards

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

Issue a card under a signed mandate

Issues a card for an agent, under a signed mandate.

Three success outcomes, and the status separates them. 201 is the issuance that finished now. 202 is the issuer asking for KYC on the holder before issuing: the flow stays pending, and kyc_url is where to send the person. 200 is a REPLAY: the same Idempotency-Key arriving again on a flow that already finished, returning what it produced instead of issuing a second card.

Repeating is safe and it is the recovery path. A 502 issuer_ingest_failed means the card EXISTS and only the ingest into the data environment failed: repeating with the SAME key reuses the token, and sending a new key would issue a second card. The body of that refusal already carries the issued card.

A repeated key with a different body is 409, not a new issuance: the request fingerprint is derived from the holder and compared, and it survives the redaction of the holder's data, so the divergence is caught much later.

Request body

FieldTypeRequiredDescription
countrystringyesISO 3166-1 alpha-2, uppercase. Decides the issuing corridor.
currencystringyesISO 4217, uppercase.
holderobjectyes—
mandate_idstringyesREQUIRED on every rail, with no exception by issuer. The card is an agent's spending instrument and the mandate is the consumer's signed authorization for it. Issuing with no mandate produced, on Bridge, a card with no limit and no category on a rail that never declines; and on Pomelo, a card the authorizer has no link to at all at spend time.
metadataobjectno—

Responses

StatusBodyDescription
200object | objectOK
201objectOK
202objectOK
400objectThe body did not match the schema, the country/currency pair has no corridor, the holder was rejected by the issuer, or the controls requested do not exist on this rail.
403objectThe key and the issuer's environment do not match. A test key does not issue a real card, and a live key does not issue on the staging issuer.
404objectThe named mandate does not exist in this organization.
409objectThe Idempotency-Key was already used with a different body, or the mandate is not active. details carries the mandate's state in the second case.
500objectInvariant break: the flow is recorded as issued and there is no readable card row behind it.
502objectThe issuer refused or failed the issuance; or the card was issued and the ingest into the data environment failed. In the second case the card EXISTS and comes in details.card; repeat with the SAME key.
503objectThe issuer's credentials could not be read from the vault.
Example request
curl -X POST https://api.codespar.dev/v1/cards \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "country": "string",
       "currency": "BRL",
       "holder": {
         "full_name": "Example",
         "email": "person@example.com",
         "birthdate": "string",
         "document": {
           "type": "string",
           "value": "string",
           "issuing_country": "string"
         },
         "address": {
           "line1": "string",
           "line2": "string",
           "street_number": "string",
           "neighborhood": "string",
           "city": "string",
           "state": "string",
           "postal_code": "string",
           "municipality": "string",
           "country": "string"
         },
         "phone": "string"
       },
       "mandate_id": "mandate_0000000000000000",
       "metadata": {}
     }'
POST /v1/cards HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "country": "string",
  "currency": "BRL",
  "holder": {
    "full_name": "Example",
    "email": "person@example.com",
    "birthdate": "string",
    "document": {
      "type": "string",
      "value": "string",
      "issuing_country": "string"
    },
    "address": {
      "line1": "string",
      "line2": "string",
      "street_number": "string",
      "neighborhood": "string",
      "city": "string",
      "state": "string",
      "postal_code": "string",
      "municipality": "string",
      "country": "string"
    },
    "phone": "string"
  },
  "mandate_id": "mandate_0000000000000000",
  "metadata": {}
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/cards",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "country": "string",
      "currency": "BRL",
      "holder": {
        "full_name": "Example",
        "email": "person@example.com",
        "birthdate": "string",
        "document": {
          "type": "string",
          "value": "string",
          "issuing_country": "string"
        },
        "address": {
          "line1": "string",
          "line2": "string",
          "street_number": "string",
          "neighborhood": "string",
          "city": "string",
          "state": "string",
          "postal_code": "string",
          "municipality": "string",
          "country": "string"
        },
        "phone": "string"
      },
      "mandate_id": "mandate_0000000000000000",
      "metadata": {}
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/cards", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "country": "string",
    "currency": "BRL",
    "holder": {
      "full_name": "Example",
      "email": "person@example.com",
      "birthdate": "string",
      "document": {
        "type": "string",
        "value": "string",
        "issuing_country": "string"
      },
      "address": {
        "line1": "string",
        "line2": "string",
        "street_number": "string",
        "neighborhood": "string",
        "city": "string",
        "state": "string",
        "postal_code": "string",
        "municipality": "string",
        "country": "string"
      },
      "phone": "string"
    },
    "mandate_id": "mandate_0000000000000000",
    "metadata": {}
  }),
});

const data = await res.json();
const r = await cs.api.response("post", "/v1/cards", {
  body: {
    country: "string",
    currency: "BRL",
    holder: {
      full_name: "Example",
      email: "person@example.com",
      birthdate: "string",
      document: {
        type: "string",
        value: "string",
        issuing_country: "string"
      },
      address: {
        line1: "string",
        line2: "string",
        street_number: "string",
        neighborhood: "string",
        city: "string",
        state: "string",
        postal_code: "string",
        municipality: "string",
        country: "string"
      },
      phone: "string"
    },
    mandate_id: "mandate_0000000000000000",
    metadata: {}
  }
});
// 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
{
  "state": "issued",
  "flow_id": "flow_0000000000000000",
  "card": {
    "id": "obj_0000000000000000",
    "issuer": "pomelo",
    "card_id": "card_0000000000000000",
    "last4": "string",
    "network": "visa",
    "status": "active",
    "token_id": "token_0000000000000000",
    "cde": "ingested",
    "created_at": "2026-01-15T12:00:00.000Z"
  }
}

GET /v1/cards/{id}

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

Read an issued card, or the mint flow that is producing one

ONE PATH, TWO RESOURCES, chosen by the id's prefix. An id beginning imf_ is a MINT FLOW and the response is the flow. Anything else is looked up as a card row and the response is the card. There is no query parameter and no header that changes this: the prefix is the whole dispatch, so a client has to be ready for both bodies on one operation.

A flow that reached issued and already points at a card row answers with the flow PLUS a card key. No other flow state carries that key at all. So the polling loop after a mint is: read the flow id, and when state is issued, read card.

SCOPING. Both lookups carry the caller's org and project in the WHERE clause. A card or flow belonging to a sibling project of the same org is indistinguishable from one that was never created: both are 404, with the same code.

NO CARD-NUMBER MATERIAL crosses this surface. last4 and, once ingest lands, token_id are what a card exposes; the number itself lives only in the cardholder-data environment and is never part of this response.

The canonical path. GET /v1/issuer/cards/{id} is the same handler at the path that shipped first, kept for two releases and not described in this document.

Path parameters

NameTypeRequiredDescription
idstringyesA card row id, or an imf_ prefixed mint flow id. The prefix selects which.

Responses

StatusBodyDescription
200object | objectOK
404objectissuer_card_unknown for both misses, with different messages: mint flow not found for an imf_ id and issuer card not found otherwise. One code, so a client cannot tell a wrong id from another project's id by reading the code.
Example request
curl -X GET https://api.codespar.dev/v1/cards/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/cards/{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/cards/{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/cards/{id}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/cards/{id}", {
  path: {
    id: "card_0000000000000000"
  }
});
Example response 200
application/json
{
  "id": "obj_0000000000000000",
  "issuer": "pomelo",
  "card_id": "card_0000000000000000",
  "last4": "string",
  "network": "visa",
  "status": "active",
  "token_id": "token_0000000000000000",
  "cde": "ingested",
  "created_at": "2026-01-15T12:00:00.000Z"
}

POST /v1/issuer/cards

POSThttps://api.codespar.dev/v1/issuer/cards
Deprecated

Issue a card under a signed mandate (deprecated path)

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

Three success outcomes: 201 issued the card now, 202 the issuer asked for KYC of the holder (kyc_url is where to send the person), 200 is a repeat of the same Idempotency-Key on a flow that already finished. Repeating with the SAME key is the recovery path: a 502 issuer_ingest_failed means the card EXISTS and only the ingest failed, and a new key would issue a second card.

Request body

FieldTypeRequiredDescription
countrystringyesISO 3166-1 alpha-2, uppercase.
currencystringyesISO 4217, uppercase.
holderobjectyes—
mandate_idstringyesRequired: there is no card without a signed mandate.
metadataobjectno—

Responses

StatusBodyDescription
200object | objectOK
201objectOK
202objectOK
400objectBody outside the schema, a country/currency pair with no corridor, a rejected holder, or controls requested that the rail does not have.
403objectThe key and the issuer environment do not match.
404objectThe named mandate does not exist in this organization.
409objectKey repeated with a different body, or the mandate is not active.
500objectFlow issued with no readable card row.
502objectThe issuer failed, or the card was issued and the ingest failed; in the second case the card comes back in details.card and the recovery is to repeat the same key.
503objectIssuer credentials not readable in the vault.
Example request
curl -X POST https://api.codespar.dev/v1/issuer/cards \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "country": "string",
       "currency": "BRL",
       "holder": {
         "full_name": "Example",
         "email": "person@example.com",
         "birthdate": "string",
         "document": {
           "type": "string",
           "value": "string",
           "issuing_country": "string"
         },
         "address": {
           "line1": "string",
           "line2": "string",
           "street_number": "string",
           "neighborhood": "string",
           "city": "string",
           "state": "string",
           "postal_code": "string",
           "municipality": "string",
           "country": "string"
         },
         "phone": "string"
       },
       "mandate_id": "mandate_0000000000000000",
       "metadata": {}
     }'
POST /v1/issuer/cards HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "country": "string",
  "currency": "BRL",
  "holder": {
    "full_name": "Example",
    "email": "person@example.com",
    "birthdate": "string",
    "document": {
      "type": "string",
      "value": "string",
      "issuing_country": "string"
    },
    "address": {
      "line1": "string",
      "line2": "string",
      "street_number": "string",
      "neighborhood": "string",
      "city": "string",
      "state": "string",
      "postal_code": "string",
      "municipality": "string",
      "country": "string"
    },
    "phone": "string"
  },
  "mandate_id": "mandate_0000000000000000",
  "metadata": {}
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/issuer/cards",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "country": "string",
      "currency": "BRL",
      "holder": {
        "full_name": "Example",
        "email": "person@example.com",
        "birthdate": "string",
        "document": {
          "type": "string",
          "value": "string",
          "issuing_country": "string"
        },
        "address": {
          "line1": "string",
          "line2": "string",
          "street_number": "string",
          "neighborhood": "string",
          "city": "string",
          "state": "string",
          "postal_code": "string",
          "municipality": "string",
          "country": "string"
        },
        "phone": "string"
      },
      "mandate_id": "mandate_0000000000000000",
      "metadata": {}
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/issuer/cards", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "country": "string",
    "currency": "BRL",
    "holder": {
      "full_name": "Example",
      "email": "person@example.com",
      "birthdate": "string",
      "document": {
        "type": "string",
        "value": "string",
        "issuing_country": "string"
      },
      "address": {
        "line1": "string",
        "line2": "string",
        "street_number": "string",
        "neighborhood": "string",
        "city": "string",
        "state": "string",
        "postal_code": "string",
        "municipality": "string",
        "country": "string"
      },
      "phone": "string"
    },
    "mandate_id": "mandate_0000000000000000",
    "metadata": {}
  }),
});

const data = await res.json();
const r = await cs.api.response("post", "/v1/issuer/cards", {
  body: {
    country: "string",
    currency: "BRL",
    holder: {
      full_name: "Example",
      email: "person@example.com",
      birthdate: "string",
      document: {
        type: "string",
        value: "string",
        issuing_country: "string"
      },
      address: {
        line1: "string",
        line2: "string",
        street_number: "string",
        neighborhood: "string",
        city: "string",
        state: "string",
        postal_code: "string",
        municipality: "string",
        country: "string"
      },
      phone: "string"
    },
    mandate_id: "mandate_0000000000000000",
    metadata: {}
  }
});
// 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
{
  "state": "issued",
  "flow_id": "flow_0000000000000000",
  "card": {
    "id": "obj_0000000000000000",
    "issuer": "pomelo",
    "card_id": "card_0000000000000000",
    "last4": "string",
    "network": "visa",
    "status": "active",
    "token_id": "token_0000000000000000",
    "cde": "ingested",
    "created_at": "2026-01-15T12:00:00.000Z"
  }
}

GET /v1/issuer/cards/{id}

GEThttps://api.codespar.dev/v1/issuer/cards/{id}
Deprecated

Read one issued card, or the mint flow behind it

DEPRECATED alias of GET /v1/cards/{id} (ent#979), kept for two releases. Same handler, same required scope; switch the path and nothing else changes.

ONE path, TWO bodies, chosen by the id's prefix. An imf_ id is a mint flow and answers with the flow; anything else is read as a card row and answers with the card. Branch on the prefix you sent, or on flow_id being present, never on field order.

Resolution is scoped to the organization AND the project the credential authenticates for: both lookups filter on all three of id, org and project, so a card held by a sibling project in the same organization answers 404 here, indistinguishable from one that never existed.

No cardholder data crosses this boundary. The number is represented by last4 and by the token id; PAN, CVV and expiry never enter the process that serves this route.

Requires the cards:read scope.

Path parameters

NameTypeRequiredDescription
idstringyesicrd_ for a card, imf_ for a mint flow. The prefix selects the body.

Responses

StatusBodyDescription
200object | objectOK. The card, or the mint flow when the id was an imf_ one — see the description for which id yields which.
404objectNot Found. No card and no mint flow with this id in the caller's organization and project. Both branches answer with the same code and differ only in the message.
Example request
curl -X GET https://api.codespar.dev/v1/issuer/cards/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/issuer/cards/{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/issuer/cards/{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/issuer/cards/{id}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/issuer/cards/{id}", {
  path: {
    id: "card_0000000000000000"
  }
});
Example response 200
application/json
{
  "id": "obj_0000000000000000",
  "issuer": "pomelo",
  "card_id": "card_0000000000000000",
  "last4": "string",
  "network": "visa",
  "status": "active",
  "token_id": "token_0000000000000000",
  "cde": "ingested",
  "created_at": "2026-01-15T12:00:00.000Z"
}
Cards | CodeSpar