Cards
5 operations (GET POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
Base URL: https://api.codespar.dev
Every operation below requires a Bearer token. See Authentication.
GET /v1/cards
https://api.codespar.dev/v1/cardsList 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
| Name | Type | Required | Description |
|---|---|---|---|
consumer_id | string | no | Filters to one consumer's cards. |
limit | integer | no | 1 to 200; the default is 50. |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | The query did not match the schema. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
cards | array of object | yes | — |
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_KEYimport 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");{
"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
https://api.codespar.dev/v1/cardsIssue 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
| Field | Type | Required | Description |
|---|---|---|---|
country | string | yes | ISO 3166-1 alpha-2, uppercase. Decides the issuing corridor. |
currency | string | yes | ISO 4217, uppercase. |
holder | object | yes | — |
mandate_id | string | yes | REQUIRED 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. |
metadata | object | no | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | object | OK |
201 | object | OK |
202 | object | OK |
400 | object | The 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. |
403 | object | The 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. |
404 | object | The named mandate does not exist in this organization. |
409 | object | The 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. |
500 | object | Invariant break: the flow is recorded as issued and there is no readable card row behind it. |
502 | object | The 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. |
503 | object | The issuer's credentials could not be read from the vault. |
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);
}{
"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}
https://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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | A card row id, or an imf_ prefixed mint flow id. The prefix selects which. |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | object | OK |
404 | object | issuer_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. |
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_KEYimport 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"
}
});{
"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
https://api.codespar.dev/v1/issuer/cardsIssue 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
| Field | Type | Required | Description |
|---|---|---|---|
country | string | yes | ISO 3166-1 alpha-2, uppercase. |
currency | string | yes | ISO 4217, uppercase. |
holder | object | yes | — |
mandate_id | string | yes | Required: there is no card without a signed mandate. |
metadata | object | no | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | object | OK |
201 | object | OK |
202 | object | OK |
400 | object | Body outside the schema, a country/currency pair with no corridor, a rejected holder, or controls requested that the rail does not have. |
403 | object | The key and the issuer environment do not match. |
404 | object | The named mandate does not exist in this organization. |
409 | object | Key repeated with a different body, or the mandate is not active. |
500 | object | Flow issued with no readable card row. |
502 | object | The 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. |
503 | object | Issuer credentials not readable in the vault. |
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);
}{
"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}
https://api.codespar.dev/v1/issuer/cards/{id}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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | icrd_ for a card, imf_ for a mint flow. The prefix selects the body. |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | object | OK. The card, or the mint flow when the id was an imf_ one — see the description for which id yields which. |
404 | object | Not 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. |
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_KEYimport 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"
}
});{
"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"
}Consumer Payments
2 operations under /v1/consumer-payments (POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
Funding Sources
2 operations under /v1/funding-sources (GET): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.