Skip to main content

Counterparties

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

5 min read
View MarkdownEdit on GitHub

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

Every operation below requires a Bearer token. See Authentication.

GET /v1/counterparties

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

List this project's counterparties

Most recently seen first, capped by limit. There is NO CURSOR: a project with more counterparties than limit (max 200) cannot be walked past the first page from here.

identifier and identifier_kind filter only when BOTH are present. Either one on its own is accepted by the validator and then silently ignored, so a lookup by bare identifier returns the unfiltered page rather than an error or an empty list.

The canonical path. GET /v1/commerce-memory/counterparties is the deprecated alias of this operation and is kept for two releases. The two paths are the same handler with the same required scope, so switching one for the other changes nothing about the request, the response or the credential.

Query parameters

NameTypeRequiredDescription
identifierstringno—
identifier_kind"cnpj" | "cpf" | "email" | "phone" | "wallet_address" | "other"no—
kind"supplier" | "customer" | "agent_peer"no—
limitintegerno—

Responses

StatusBodyDescription
200objectOK
400objectinvalid_query, with the validator's issue list under details.issues.

Response 200

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

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

const data = await res.json();
const result = await cs.api.get("/v1/counterparties");
Example response 200
application/json
{
  "counterparties": [
    {
      "id": "obj_0000000000000000",
      "kind": "supplier",
      "name": "Example",
      "identifier": "string",
      "identifier_kind": "cnpj",
      "metadata": {},
      "created_at": "2026-01-15T12:00:00.000Z",
      "last_seen_at": "2026-01-15T12:00:00.000Z"
    }
  ]
}

POST /v1/counterparties

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

Register a counterparty, or merge into the one already there

An UPSERT when identifier and identifier_kind are both supplied, keyed on that pair within the caller's org AND project. Registering the same CNPJ a second time from a different agent run in the same project does not create a second row: name is replaced, last_seen_at is bumped to now, and metadata is merged over the stored object at the top level only, so a nested object is replaced wholesale rather than deep merged. Two different projects that both register that CNPJ keep two rows and two separate histories.

Omit both identifier fields and there is no dedup key, so every call creates a new row. The two fields travel together: one without the other is 400.

The status is 201 on both paths, including the merge. A 201 here is not a promise that a row was created.

The canonical path. POST /v1/commerce-memory/counterparties is the deprecated alias of this operation and is kept for two releases. The two paths are the same handler with the same required scope, so switching one for the other changes nothing about the request, the response or the credential.

Request body

FieldTypeRequiredDescription
identifierstringnoSet it together with identifier_kind or omit both. One without the other is refused.
identifier_kind"cnpj" | "cpf" | "email" | "phone" | "wallet_address" | "other"no—
kind"supplier" | "customer" | "agent_peer"yes—
metadataobjectno—
namestringyes—

Responses

StatusBodyDescription
201objectCreated, or merged into the existing row.
400objectinvalid_body. details.issues carries the validator's own issue list, including the case where only one of identifier / identifier_kind was sent.

Response 201

FieldTypeRequiredDescription
created_atstring (date-time)yes—
idstringyescp_ prefixed.
identifierstring,nullyesNull when the counterparty was registered without one.
identifier_kind"cnpj" | "cpf" | "email" | "phone" | "wallet_address" | "other"yes—
kind"supplier" | "customer" | "agent_peer"yes—
last_seen_atstring (date-time)yesBumped by a re-registration of the same identifier and by every interaction written against this counterparty.
metadataobjectyes—
namestringyes—
Example request
curl -X POST https://api.codespar.dev/v1/counterparties \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "kind": "supplier",
       "name": "Example",
       "identifier": "string",
       "identifier_kind": "cnpj",
       "metadata": {}
     }'
POST /v1/counterparties HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "kind": "supplier",
  "name": "Example",
  "identifier": "string",
  "identifier_kind": "cnpj",
  "metadata": {}
}
import os
import requests

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

const data = await res.json();
const result = await cs.api.post("/v1/counterparties", {
  body: {
    kind: "supplier",
    name: "Example",
    identifier: "string",
    identifier_kind: "cnpj",
    metadata: {}
  }
});
Example response 201
application/json
{
  "id": "obj_0000000000000000",
  "kind": "supplier",
  "name": "Example",
  "identifier": "string",
  "identifier_kind": "cnpj",
  "metadata": {},
  "created_at": "2026-01-15T12:00:00.000Z",
  "last_seen_at": "2026-01-15T12:00:00.000Z"
}

GET /v1/counterparties/{id}

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

Read one counterparty

The same row the list returns, by id.

A counterparty that belongs to another org, or to a sibling project of the same org, is indistinguishable from one that does not exist: both are 404. Counterparty ids are unique per project and not globally, so the same id can name a different company in another project.

The canonical path. GET /v1/commerce-memory/counterparties/{id} is the deprecated alias of this operation, described in this document under that path and kept for two releases. The two paths are the same handler with the same required scope, so switching one for the other changes nothing about the request, the response or the credential.

Path parameters

NameTypeRequiredDescription
idstringyescp_ prefixed.

Responses

StatusBodyDescription
200objectOK
404objectnot_found, including for a counterparty outside the caller's project.

Response 200

FieldTypeRequiredDescription
created_atstring (date-time)yes—
idstringyescp_ prefixed.
identifierstring,nullyesNull when the counterparty was registered without one.
identifier_kind"cnpj" | "cpf" | "email" | "phone" | "wallet_address" | "other"yesCHECK (identifier_kind IN ('cnpj', 'cpf', 'email', 'phone', 'wallet_address', 'other')), migration 0050. Null exactly when identifier is null: the pair is held together by commerce_counterparties_identifier_pair, a table constraint, so one without the other cannot be stored.
kind"supplier" | "customer" | "agent_peer"yesCHECK (kind IN ('supplier', 'customer', 'agent_peer')), migration 0050.
last_seen_atstring (date-time)yesBumped by a re-registration of the same identifier and by every interaction recorded against this row.
metadataobjectyes—
namestringyes—
Example request
curl -X GET https://api.codespar.dev/v1/counterparties/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/counterparties/{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/counterparties/{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/counterparties/{id}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/counterparties/{id}", {
  path: {
    id: "counterpartie_0000000000000000"
  }
});
Example response 200
application/json
{
  "id": "obj_0000000000000000",
  "kind": "supplier",
  "name": "Example",
  "identifier": "string",
  "identifier_kind": "cnpj",
  "metadata": {},
  "created_at": "2026-01-15T12:00:00.000Z",
  "last_seen_at": "2026-01-15T12:00:00.000Z"
}
Counterparties | CodeSpar