Commerce Memory
12 operations under /v1/commerce-memory (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/commerce-memory/counterparties
https://api.codespar.dev/v1/commerce-memory/counterpartiesList this project's counterparties (deprecated path)
DEPRECATED alias of GET /v1/counterparties (ent#979), described in this document under that path and kept here 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.
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.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
identifier | string | no | — |
identifier_kind | "cnpj" | "cpf" | "email" | "phone" | "wallet_address" | "other" | no | — |
kind | "supplier" | "customer" | "agent_peer" | no | — |
limit | integer | no | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | invalid_query, with the validator's issue list under details.issues. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
counterparties | array of object | yes | — |
curl -X GET https://api.codespar.dev/v1/commerce-memory/counterparties \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/commerce-memory/counterparties HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/commerce-memory/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/commerce-memory/counterparties", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.get("/v1/commerce-memory/counterparties");{
"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/commerce-memory/counterparties
https://api.codespar.dev/v1/commerce-memory/counterpartiesRegister a counterparty, or merge into the one already there (deprecated path)
DEPRECATED alias of POST /v1/counterparties (ent#979), described in this document under that path and kept here 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.
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.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
identifier | string | no | Set 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 | — |
metadata | object | no | — |
name | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
201 | object | Created, or merged into the existing row. |
400 | object | invalid_body. details.issues carries the validator's own issue list, including the case where only one of identifier / identifier_kind was sent. |
Response 201
| Field | Type | Required | Description |
|---|---|---|---|
created_at | string (date-time) | yes | — |
id | string | yes | cp_ prefixed. |
identifier | string,null | yes | Null 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_at | string (date-time) | yes | Bumped by a re-registration of the same identifier and by every interaction written against this counterparty. |
metadata | object | yes | — |
name | string | yes | — |
curl -X POST https://api.codespar.dev/v1/commerce-memory/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/commerce-memory/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/commerce-memory/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/commerce-memory/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/commerce-memory/counterparties", {
body: {
kind: "supplier",
name: "Example",
identifier: "string",
identifier_kind: "cnpj",
metadata: {}
}
});{
"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/commerce-memory/counterparties/{id}
https://api.codespar.dev/v1/commerce-memory/counterparties/{id}Read one counterparty (deprecated path)
DEPRECATED alias of GET /v1/counterparties/{id} (ent#979), 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. The full response is described here, so nothing about this operation is withheld until you switch paths.
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.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | cp_ prefixed. |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
404 | object | not_found, including for a counterparty outside the caller's project. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
created_at | string (date-time) | yes | — |
id | string | yes | cp_ prefixed. |
identifier | string,null | yes | Null 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_at | string (date-time) | yes | Bumped by a re-registration of the same identifier and by every interaction written against this counterparty. |
metadata | object | yes | — |
name | string | yes | — |
curl -X GET https://api.codespar.dev/v1/commerce-memory/counterparties/{id} \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/commerce-memory/counterparties/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/commerce-memory/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/commerce-memory/counterparties/{id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.get("/v1/commerce-memory/counterparties/{id}", {
path: {
id: "counterpartie_0000000000000000"
}
});{
"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/commerce-memory/insights/counterparty-summary
https://api.codespar.dev/v1/commerce-memory/insights/counterparty-summaryPer counterparty rollup over a window
One row per counterparty in the project: how many interactions it had in the window, how many of those were payments, what they summed to, and which currencies appeared.
total_payment_minor ADDS ACROSS CURRENCIES. It sums every PAYMENT interaction in the window whatever its currency, so a counterparty billed in both BRL and USD gets a single number that is an amount in neither.
currencies DOES NOT TELL YOU WHETHER THAT HAPPENED, and it is the field most likely to be read as if it did. It aggregates the distinct currencies of the window's interactions of EVERY kind, while the total is built from payments alone: an invoice priced in USD next to a payment in BRL yields two entries over a total that is pure BRL. The set is an upper bound on the currencies inside the total, so at most one entry does bound the total to a single currency, and two or more prove nothing either way. To attribute a total to a currency, list the window's interactions with kind=payment and add them up per currency.
The window filters the interactions, not the counterparties. A counterparty with nothing in the window still appears, with zero counts and a zero total, which is what makes this usable for spotting dormant relationships. Omitting both bounds sums the whole history.
Ordered by total_payment_minor descending, then by last_seen_at descending, capped by limit (max 500, default 100). No cursor.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
kind | "supplier" | "customer" | "agent_peer" | no | — |
limit | integer | no | — |
since | string (date-time) | no | Inclusive lower bound on occurred_at. |
until | string (date-time) | no | Exclusive upper bound on occurred_at. |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | invalid_query, with the validator's issue list under details.issues. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
rows | array of object | yes | — |
window | object | yes | The since and until of the request, echoed back, null when not supplied. |
curl -X GET https://api.codespar.dev/v1/commerce-memory/insights/counterparty-summary \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/commerce-memory/insights/counterparty-summary HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/commerce-memory/insights/counterparty-summary",
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/commerce-memory/insights/counterparty-summary", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.get("/v1/commerce-memory/insights/counterparty-summary");{
"window": {
"since": "2026-01-15T12:00:00.000Z",
"until": "2026-01-15T12:00:00.000Z"
},
"rows": [
{
"counterparty_id": "counterparty_0000000000000000",
"name": "Example",
"kind": "supplier",
"identifier": "string",
"identifier_kind": "cnpj",
"last_seen_at": "2026-01-15T12:00:00.000Z",
"interaction_count": 1,
"payment_count": 1,
"total_payment_minor": 1,
"currencies": [
"string"
]
}
]
}GET /v1/commerce-memory/insights/negotiation-stats
https://api.codespar.dev/v1/commerce-memory/insights/negotiation-statsNegotiation outcomes and average discount, grouped by item
One row per (item_ref, outcome) pair among the negotiations whose started_at falls in the window. Pass item_ref to narrow to a single sku; leave it out and every group in the project comes back.
avg_discount_pct IS A FRACTION, NOT A PERCENTAGE. It is the mean of (initial_price_minor - agreed_price_minor) / initial_price_minor, so 0.075 means the agreed price landed 7.5 percent below the opening ask. A negative value means the close was ABOVE the opening ask. It is null on every group whose outcome is not agreed, and null on an agreed group where no negotiation carries both a positive initial_price_minor and an agreed_price_minor; discount_sample_n says how many rows actually fed it, and it is routinely smaller than n.
THIS ENDPOINT HAS NO limit. The response carries every group in the window, so a wide window on a project with many distinct item_ref values returns a large body. Ordered by item_ref with the null group last, then by outcome.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
item_ref | string | no | — |
since | string (date-time) | no | Inclusive lower bound on started_at. |
until | string (date-time) | no | Exclusive upper bound on started_at. |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | invalid_query, with the validator's issue list under details.issues. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
item_ref | string,null | yes | The item_ref filter of the request, echoed back. |
rows | array of object | yes | — |
window | object | yes | The since and until of the request, echoed back, null when not supplied. |
curl -X GET https://api.codespar.dev/v1/commerce-memory/insights/negotiation-stats \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/commerce-memory/insights/negotiation-stats HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/commerce-memory/insights/negotiation-stats",
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/commerce-memory/insights/negotiation-stats", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.get("/v1/commerce-memory/insights/negotiation-stats");{
"window": {
"since": "2026-01-15T12:00:00.000Z",
"until": "2026-01-15T12:00:00.000Z"
},
"item_ref": "string",
"rows": [
{
"item_ref": "string",
"outcome": "agreed",
"n": 0,
"avg_rounds": 0,
"currencies": [
"string"
],
"avg_discount_pct": 1,
"discount_sample_n": 1
}
]
}GET /v1/commerce-memory/insights/top-counterparties
https://api.codespar.dev/v1/commerce-memory/insights/top-counterpartiesCounterparties ranked by payment volume in a window
The leaderboard cut of the summary above, for a dashboard widget that does not want the long tail: ordered by total_payment_minor descending and capped at limit (max 50, default 10).
IT DROPS ROWS THE SUMMARY KEEPS. A counterparty with no payment interaction inside the window is absent here, where counterparty-summary lists it with zeros. Absence from this response is not absence from the project.
total_payment_minor adds across currencies, exactly as in the summary, and nothing in this response tells you whether a given total did. dominant_currency is not that check and neither is the summary's currencies: this one is the most frequent currency across ALL of the counterparty's payment interactions and ignores since and until, so it can name a currency the window never saw and it stays populated for a counterparty that used two; that one spans every interaction kind and is only an upper bound. A ranking you need to trust as money is one you build from the window's kind=payment interactions, per currency.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
kind | "supplier" | "customer" | "agent_peer" | no | — |
limit | integer | no | — |
since | string (date-time) | no | Inclusive lower bound on occurred_at. |
until | string (date-time) | no | Exclusive upper bound on occurred_at. |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | invalid_query, with the validator's issue list under details.issues. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
kind | "supplier" | "customer" | "agent_peer" | yes | The kind filter of the request, echoed back. |
rows | array of object | yes | — |
window | object | yes | The since and until of the request, echoed back, null when not supplied. |
curl -X GET https://api.codespar.dev/v1/commerce-memory/insights/top-counterparties \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/commerce-memory/insights/top-counterparties HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/commerce-memory/insights/top-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/commerce-memory/insights/top-counterparties", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.get("/v1/commerce-memory/insights/top-counterparties");{
"window": {
"since": "2026-01-15T12:00:00.000Z",
"until": "2026-01-15T12:00:00.000Z"
},
"kind": "supplier",
"rows": [
{
"counterparty_id": "counterparty_0000000000000000",
"name": "Example",
"kind": "supplier",
"payment_count": 1,
"total_payment_minor": 1,
"dominant_currency": "BRL"
}
]
}GET /v1/commerce-memory/interactions
https://api.codespar.dev/v1/commerce-memory/interactionsList recorded interactions
Newest occurred_at first, capped by limit (max 500). There is NO CURSOR, so a busy counterparty cannot be walked past the first page; narrow with since / until instead. since is inclusive and until is exclusive, which makes adjacent windows tile without double counting a row on the boundary.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
counterparty_id | string | no | — |
kind | "quote" | "negotiation" | "order" | "invoice" | "payment" | "shipment" | "message" | "other" | no | — |
limit | integer | no | — |
since | string (date-time) | no | Inclusive lower bound on occurred_at. |
until | string (date-time) | no | Exclusive upper bound on occurred_at. |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | invalid_query, with the validator's issue list under details.issues. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
interactions | array of object | yes | — |
curl -X GET https://api.codespar.dev/v1/commerce-memory/interactions \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/commerce-memory/interactions HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/commerce-memory/interactions",
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/commerce-memory/interactions", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.get("/v1/commerce-memory/interactions");{
"interactions": [
{
"id": "obj_0000000000000000",
"counterparty_id": "counterparty_0000000000000000",
"kind": "quote",
"amount_minor": 1000,
"amount_native": "1000",
"amount": "1000",
"currency": "BRL",
"occurred_at": "2026-01-15T12:00:00.000Z",
"source": "agent_execute",
"source_ref": "string",
"agent_id": "agt_0000000000000000",
"metadata": {}
}
]
}POST /v1/commerce-memory/interactions
https://api.codespar.dev/v1/commerce-memory/interactionsRecord a commercial event against a counterparty
This RECORDS an event; it moves no money and settles nothing. amount_minor is a figure being written into the commercial record, not an instruction to pay it.
IDEMPOTENT ON (source, source_ref) WITHIN THE PROJECT, and only when source_ref is supplied. Re-posting a pair that already exists writes nothing and returns 200 with { idempotent: true, interaction } carrying the row that was already there, so a retrying ingest pipeline is safe. Omit source_ref and there is no key: every call inserts, and a double post double counts.
On a real insert the counterparty's last_seen_at is bumped to now. On the idempotent path it is not.
amount_minor and currency travel together: one without the other is 400. occurred_at defaults to the moment of the call, so backfilling history means sending it explicitly.
404 counterparty_not_found when counterparty_id is not in the caller's org and project. A counterparty in a sibling project reads the same as one that does not exist.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
agent_id | string | no | — |
amount_minor | integer | no | Minor units (cents). Stored in the currency's native unit (USDC x10^4, BRL as is), so this field cannot carry a sub-cent USDC figure. Set it together with currency or omit both. |
counterparty_id | string | yes | — |
currency | string | no | — |
kind | "quote" | "negotiation" | "order" | "invoice" | "payment" | "shipment" | "message" | "other" | yes | — |
metadata | object | no | — |
occurred_at | string (date-time) | no | Defaults to now. |
source | "agent_execute" | "webhook" | "manual" | "backfill" | no | — |
source_ref | string | no | The idempotency key, paired with source. Omit it and every post writes a new row. |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | Idempotent replay. (source, source_ref) already existed in this project; nothing was written and the stored row is returned. |
201 | object | Recorded. |
400 | object | invalid_body, with the validator's issue list under details.issues. |
404 | object | counterparty_not_found: the id does not belong to this org and project. |
500 | object | idempotent_lookup_failed. The insert hit the idempotency key but the conflicting row could not be read back afterwards. Nothing was written; the call is safe to retry. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
idempotent | true | yes | — |
interaction | object | yes | — |
curl -X POST https://api.codespar.dev/v1/commerce-memory/interactions \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"counterparty_id": "counterparty_0000000000000000",
"kind": "quote",
"amount_minor": 1000,
"currency": "BRL",
"occurred_at": "2026-01-15T12:00:00.000Z",
"source": "agent_execute",
"source_ref": "string",
"agent_id": "agt_0000000000000000",
"metadata": {}
}'POST /v1/commerce-memory/interactions HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json
{
"counterparty_id": "counterparty_0000000000000000",
"kind": "quote",
"amount_minor": 1000,
"currency": "BRL",
"occurred_at": "2026-01-15T12:00:00.000Z",
"source": "agent_execute",
"source_ref": "string",
"agent_id": "agt_0000000000000000",
"metadata": {}
}import os
import requests
res = requests.post(
"https://api.codespar.dev/v1/commerce-memory/interactions",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
json={
"counterparty_id": "counterparty_0000000000000000",
"kind": "quote",
"amount_minor": 1000,
"currency": "BRL",
"occurred_at": "2026-01-15T12:00:00.000Z",
"source": "agent_execute",
"source_ref": "string",
"agent_id": "agt_0000000000000000",
"metadata": {}
},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/commerce-memory/interactions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"counterparty_id": "counterparty_0000000000000000",
"kind": "quote",
"amount_minor": 1000,
"currency": "BRL",
"occurred_at": "2026-01-15T12:00:00.000Z",
"source": "agent_execute",
"source_ref": "string",
"agent_id": "agt_0000000000000000",
"metadata": {}
}),
});
const data = await res.json();const result = await cs.api.post("/v1/commerce-memory/interactions", {
body: {
counterparty_id: "counterparty_0000000000000000",
kind: "quote",
amount_minor: 1000,
currency: "BRL",
occurred_at: "2026-01-15T12:00:00.000Z",
source: "agent_execute",
source_ref: "string",
agent_id: "agt_0000000000000000",
metadata: {}
}
});{
"idempotent": true,
"interaction": {
"id": "obj_0000000000000000",
"counterparty_id": "counterparty_0000000000000000",
"kind": "quote",
"amount_minor": 1000,
"amount_native": "1000",
"amount": "1000",
"currency": "BRL",
"occurred_at": "2026-01-15T12:00:00.000Z",
"source": "agent_execute",
"source_ref": "string",
"agent_id": "agt_0000000000000000",
"metadata": {}
}
}GET /v1/commerce-memory/negotiations
https://api.codespar.dev/v1/commerce-memory/negotiationsList negotiations
Newest started_at first, capped by limit (max 500). No cursor. item_ref matches exactly, so it reads one sku's negotiation history across every counterparty in the project.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
counterparty_id | string | no | — |
item_ref | string | no | — |
limit | integer | no | — |
outcome | "agreed" | "declined" | "pending" | "expired" | no | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | invalid_query, with the validator's issue list under details.issues. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
negotiations | array of object | yes | — |
curl -X GET https://api.codespar.dev/v1/commerce-memory/negotiations \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/commerce-memory/negotiations HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/commerce-memory/negotiations",
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/commerce-memory/negotiations", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.get("/v1/commerce-memory/negotiations");{
"negotiations": [
{
"id": "obj_0000000000000000",
"counterparty_id": "counterparty_0000000000000000",
"item": "string",
"item_ref": "string",
"initial_price_minor": 1,
"agreed_price_minor": 1,
"currency": "BRL",
"rounds": 0,
"outcome": "agreed",
"agent_id": "agt_0000000000000000",
"started_at": "2026-01-15T12:00:00.000Z",
"closed_at": "2026-01-15T12:00:00.000Z",
"metadata": {}
}
]
}POST /v1/commerce-memory/negotiations
https://api.codespar.dev/v1/commerce-memory/negotiationsRecord a price negotiation and its outcome
One row per negotiation thread. Unlike the other two writes on this resource this one has NO IDEMPOTENCY KEY: posting the same negotiation twice produces two rows, and both then feed the negotiation-stats rollup.
outcome defaults to pending. closed_at is stored only when the outcome is terminal (agreed, declined or expired) and is stamped to now when the outcome is terminal and closed_at was omitted; while the outcome is pending a supplied closed_at is dropped without an error. The two fields cannot disagree in a stored row.
outcome: agreed requires agreed_price_minor, and a body missing it is 400 rather than a row with no agreed price. currency is required on every negotiation; both price fields are minor units of it.
item_ref is optional and is the key the negotiation-stats rollup groups on, so a negotiation posted without one is aggregated into that endpoint's null group instead of against the sku.
404 counterparty_not_found when counterparty_id is not in the caller's org and project.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
agent_id | string | no | — |
agreed_price_minor | integer | no | Minor units of currency. Required when outcome is agreed. |
closed_at | string (date-time) | no | Read only when outcome is terminal. Ignored while the outcome is pending. |
counterparty_id | string | yes | — |
currency | string | yes | — |
initial_price_minor | integer | no | Minor units of currency. |
item | string | yes | Free text description of what was negotiated. |
item_ref | string | no | Structured pointer (sku, contract id, supplier product code). It is the grouping key of the negotiation-stats rollup. |
metadata | object | no | — |
outcome | "agreed" | "declined" | "pending" | "expired" | no | Defaults to pending. |
rounds | integer | no | Defaults to 1. |
started_at | string (date-time) | no | Defaults to now. |
Responses
| Status | Body | Description |
|---|---|---|
201 | object | Recorded. |
400 | object | invalid_body, with the validator's issue list under details.issues. Includes the outcome: agreed without agreed_price_minor case. |
404 | object | counterparty_not_found: the id does not belong to this org and project. |
Response 201
| Field | Type | Required | Description |
|---|---|---|---|
agent_id | string,null | yes | — |
agreed_price_minor | integer,null | yes | Minor units of currency. |
closed_at | string,null (date-time) | yes | Null exactly when outcome is pending. |
counterparty_id | string | yes | — |
currency | string | yes | — |
id | string | yes | neg_ prefixed. |
initial_price_minor | integer,null | yes | Minor units of currency. |
item | string | yes | — |
item_ref | string,null | yes | — |
metadata | object | yes | — |
outcome | "agreed" | "declined" | "pending" | "expired" | yes | — |
rounds | integer | yes | — |
started_at | string (date-time) | yes | — |
curl -X POST https://api.codespar.dev/v1/commerce-memory/negotiations \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"counterparty_id": "counterparty_0000000000000000",
"item": "string",
"item_ref": "string",
"initial_price_minor": 1,
"agreed_price_minor": 1,
"currency": "BRL",
"rounds": 0,
"outcome": "agreed",
"agent_id": "agt_0000000000000000",
"started_at": "2026-01-15T12:00:00.000Z",
"closed_at": "2026-01-15T12:00:00.000Z",
"metadata": {}
}'POST /v1/commerce-memory/negotiations HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json
{
"counterparty_id": "counterparty_0000000000000000",
"item": "string",
"item_ref": "string",
"initial_price_minor": 1,
"agreed_price_minor": 1,
"currency": "BRL",
"rounds": 0,
"outcome": "agreed",
"agent_id": "agt_0000000000000000",
"started_at": "2026-01-15T12:00:00.000Z",
"closed_at": "2026-01-15T12:00:00.000Z",
"metadata": {}
}import os
import requests
res = requests.post(
"https://api.codespar.dev/v1/commerce-memory/negotiations",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
json={
"counterparty_id": "counterparty_0000000000000000",
"item": "string",
"item_ref": "string",
"initial_price_minor": 1,
"agreed_price_minor": 1,
"currency": "BRL",
"rounds": 0,
"outcome": "agreed",
"agent_id": "agt_0000000000000000",
"started_at": "2026-01-15T12:00:00.000Z",
"closed_at": "2026-01-15T12:00:00.000Z",
"metadata": {}
},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/commerce-memory/negotiations", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"counterparty_id": "counterparty_0000000000000000",
"item": "string",
"item_ref": "string",
"initial_price_minor": 1,
"agreed_price_minor": 1,
"currency": "BRL",
"rounds": 0,
"outcome": "agreed",
"agent_id": "agt_0000000000000000",
"started_at": "2026-01-15T12:00:00.000Z",
"closed_at": "2026-01-15T12:00:00.000Z",
"metadata": {}
}),
});
const data = await res.json();const result = await cs.api.post("/v1/commerce-memory/negotiations", {
body: {
counterparty_id: "counterparty_0000000000000000",
item: "string",
item_ref: "string",
initial_price_minor: 1,
agreed_price_minor: 1,
currency: "BRL",
rounds: 0,
outcome: "agreed",
agent_id: "agt_0000000000000000",
started_at: "2026-01-15T12:00:00.000Z",
closed_at: "2026-01-15T12:00:00.000Z",
metadata: {}
}
});{
"id": "obj_0000000000000000",
"counterparty_id": "counterparty_0000000000000000",
"item": "string",
"item_ref": "string",
"initial_price_minor": 1,
"agreed_price_minor": 1,
"currency": "BRL",
"rounds": 0,
"outcome": "agreed",
"agent_id": "agt_0000000000000000",
"started_at": "2026-01-15T12:00:00.000Z",
"closed_at": "2026-01-15T12:00:00.000Z",
"metadata": {}
}GET /v1/commerce-memory/preferences
https://api.codespar.dev/v1/commerce-memory/preferencesList preferences
Most recently updated first, capped by limit (max 500, default 100). No cursor. Filter by counterparty_id for a single counterparty's full preference set, or by source to separate what an operator asserted from what a job derived.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
counterparty_id | string | no | — |
limit | integer | no | — |
source | "explicit" | "derived" | "inherited" | no | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | invalid_query, with the validator's issue list under details.issues. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
preferences | array of object | yes | — |
curl -X GET https://api.codespar.dev/v1/commerce-memory/preferences \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/commerce-memory/preferences HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/commerce-memory/preferences",
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/commerce-memory/preferences", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.get("/v1/commerce-memory/preferences");{
"preferences": [
{
"id": "obj_0000000000000000",
"counterparty_id": "counterparty_0000000000000000",
"preference_key": "string",
"value": "string",
"confidence": 0,
"source": "explicit",
"created_at": "2026-01-15T12:00:00.000Z",
"updated_at": "2026-01-15T12:00:00.000Z",
"metadata": {}
}
]
}POST /v1/commerce-memory/preferences
https://api.codespar.dev/v1/commerce-memory/preferencesSet or update one preference on a counterparty
An UPSERT keyed on (counterparty_id, preference_key) within the caller's org and project. Re-sending a key REPLACES value, confidence and source and bumps updated_at; metadata is merged over the stored object at the top level only. There is no way to append to a value from here, and no delete.
The key vocabulary is open on purpose (payment_term_days, preferred_payment_method, and whatever a later signal needs), which is why the only constraint on it is the shape: lowercase ASCII, digits and underscore.
ALWAYS SEND value, even though the schema below does not mark it required. The validator types it as unknown, which in Zod accepts a body that leaves the key out, so a body without it gets no 400 and reaches the insert. What that insert then stores is not specified by this document, and a preference whose value is a stored JSON null is not distinguishable afterwards from one an operator meant to set that way.
confidence defaults to 1, which is the value meaning an operator asserted this rather than a job deriving it; source defaults to explicit for the same reason. A derived signal should send both.
The status is 201 on both paths, including the update.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
confidence | number | no | Defaults to 1. |
counterparty_id | string | yes | — |
metadata | object | no | — |
preference_key | string | yes | Lowercase ASCII, digits and underscore. The vocabulary is open: a new key needs no migration. |
source | "explicit" | "derived" | "inherited" | no | Defaults to explicit. |
value | — | no | Any JSON value. Send it. The validator is unknown, which in Zod accepts a body that omits the key, but the column behind it is NOT NULL, so a body without value is not a supported call. |
Responses
| Status | Body | Description |
|---|---|---|
201 | object | Created, or updated in place. |
400 | object | invalid_body, with the validator's issue list under details.issues. |
404 | object | counterparty_not_found: the id does not belong to this org and project. |
Response 201
| Field | Type | Required | Description |
|---|---|---|---|
confidence | number | yes | 0 to 1. 1 means an operator asserted it; below that it was derived. |
counterparty_id | string | yes | — |
created_at | string (date-time) | yes | — |
id | string | yes | pref_ prefixed. |
metadata | object | yes | — |
preference_key | string | yes | — |
source | "explicit" | "derived" | "inherited" | yes | — |
updated_at | string (date-time) | yes | — |
value | string | number | boolean | array of — | object | null | yes | Any JSON value. Always present. |
curl -X POST https://api.codespar.dev/v1/commerce-memory/preferences \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"counterparty_id": "counterparty_0000000000000000",
"preference_key": "string",
"confidence": 0,
"source": "explicit",
"metadata": {}
}'POST /v1/commerce-memory/preferences HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json
{
"counterparty_id": "counterparty_0000000000000000",
"preference_key": "string",
"confidence": 0,
"source": "explicit",
"metadata": {}
}import os
import requests
res = requests.post(
"https://api.codespar.dev/v1/commerce-memory/preferences",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
json={
"counterparty_id": "counterparty_0000000000000000",
"preference_key": "string",
"confidence": 0,
"source": "explicit",
"metadata": {}
},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/commerce-memory/preferences", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"counterparty_id": "counterparty_0000000000000000",
"preference_key": "string",
"confidence": 0,
"source": "explicit",
"metadata": {}
}),
});
const data = await res.json();const result = await cs.api.post("/v1/commerce-memory/preferences", {
body: {
counterparty_id: "counterparty_0000000000000000",
preference_key: "string",
confidence: 0,
source: "explicit",
metadata: {}
}
});{
"id": "obj_0000000000000000",
"counterparty_id": "counterparty_0000000000000000",
"preference_key": "string",
"value": "string",
"confidence": 0,
"source": "explicit",
"created_at": "2026-01-15T12:00:00.000Z",
"updated_at": "2026-01-15T12:00:00.000Z",
"metadata": {}
}Consent Records
2 operations under /v1/consent-records (GET): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
Orgs
20 operations under /v1/orgs/{orgId} (POST GET PATCH): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.