Skip to main content

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.

18 min read
View MarkdownEdit on GitHub

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

Every operation below requires a Bearer token. See Authentication.

GET /v1/commerce-memory/counterparties

GEThttps://api.codespar.dev/v1/commerce-memory/counterparties
Deprecated

List 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

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/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_KEY
import 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");
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/commerce-memory/counterparties

POSThttps://api.codespar.dev/v1/commerce-memory/counterparties
Deprecated

Register 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

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/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: {}
  }
});
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/commerce-memory/counterparties/{id}

GEThttps://api.codespar.dev/v1/commerce-memory/counterparties/{id}
Deprecated

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

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"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 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_KEY
import 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"
  }
});
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"
}

GET /v1/commerce-memory/insights/counterparty-summary

GEThttps://api.codespar.dev/v1/commerce-memory/insights/counterparty-summary

Per 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

NameTypeRequiredDescription
kind"supplier" | "customer" | "agent_peer"no—
limitintegerno—
sincestring (date-time)noInclusive lower bound on occurred_at.
untilstring (date-time)noExclusive upper bound on occurred_at.

Responses

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

Response 200

FieldTypeRequiredDescription
rowsarray of objectyes—
windowobjectyesThe since and until of the request, echoed back, null when not supplied.
Example request
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_KEY
import 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");
Example response 200
application/json
{
  "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

GEThttps://api.codespar.dev/v1/commerce-memory/insights/negotiation-stats

Negotiation 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

NameTypeRequiredDescription
item_refstringno—
sincestring (date-time)noInclusive lower bound on started_at.
untilstring (date-time)noExclusive upper bound on started_at.

Responses

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

Response 200

FieldTypeRequiredDescription
item_refstring,nullyesThe item_ref filter of the request, echoed back.
rowsarray of objectyes—
windowobjectyesThe since and until of the request, echoed back, null when not supplied.
Example request
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_KEY
import 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");
Example response 200
application/json
{
  "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

GEThttps://api.codespar.dev/v1/commerce-memory/insights/top-counterparties

Counterparties 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

NameTypeRequiredDescription
kind"supplier" | "customer" | "agent_peer"no—
limitintegerno—
sincestring (date-time)noInclusive lower bound on occurred_at.
untilstring (date-time)noExclusive upper bound on occurred_at.

Responses

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

Response 200

FieldTypeRequiredDescription
kind"supplier" | "customer" | "agent_peer"yesThe kind filter of the request, echoed back.
rowsarray of objectyes—
windowobjectyesThe since and until of the request, echoed back, null when not supplied.
Example request
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_KEY
import 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");
Example response 200
application/json
{
  "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

GEThttps://api.codespar.dev/v1/commerce-memory/interactions

List 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

NameTypeRequiredDescription
counterparty_idstringno—
kind"quote" | "negotiation" | "order" | "invoice" | "payment" | "shipment" | "message" | "other"no—
limitintegerno—
sincestring (date-time)noInclusive lower bound on occurred_at.
untilstring (date-time)noExclusive upper bound on occurred_at.

Responses

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

Response 200

FieldTypeRequiredDescription
interactionsarray of objectyes—
Example request
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_KEY
import 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");
Example response 200
application/json
{
  "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

POSThttps://api.codespar.dev/v1/commerce-memory/interactions

Record 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

FieldTypeRequiredDescription
agent_idstringno—
amount_minorintegernoMinor 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_idstringyes—
currencystringno—
kind"quote" | "negotiation" | "order" | "invoice" | "payment" | "shipment" | "message" | "other"yes—
metadataobjectno—
occurred_atstring (date-time)noDefaults to now.
source"agent_execute" | "webhook" | "manual" | "backfill"no—
source_refstringnoThe idempotency key, paired with source. Omit it and every post writes a new row.

Responses

StatusBodyDescription
200objectIdempotent replay. (source, source_ref) already existed in this project; nothing was written and the stored row is returned.
201objectRecorded.
400objectinvalid_body, with the validator's issue list under details.issues.
404objectcounterparty_not_found: the id does not belong to this org and project.
500objectidempotent_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

FieldTypeRequiredDescription
idempotenttrueyes—
interactionobjectyes—
Example request
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: {}
  }
});
Example response 200
application/json
{
  "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

GEThttps://api.codespar.dev/v1/commerce-memory/negotiations

List 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

NameTypeRequiredDescription
counterparty_idstringno—
item_refstringno—
limitintegerno—
outcome"agreed" | "declined" | "pending" | "expired"no—

Responses

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

Response 200

FieldTypeRequiredDescription
negotiationsarray of objectyes—
Example request
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_KEY
import 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");
Example response 200
application/json
{
  "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

POSThttps://api.codespar.dev/v1/commerce-memory/negotiations

Record 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

FieldTypeRequiredDescription
agent_idstringno—
agreed_price_minorintegernoMinor units of currency. Required when outcome is agreed.
closed_atstring (date-time)noRead only when outcome is terminal. Ignored while the outcome is pending.
counterparty_idstringyes—
currencystringyes—
initial_price_minorintegernoMinor units of currency.
itemstringyesFree text description of what was negotiated.
item_refstringnoStructured pointer (sku, contract id, supplier product code). It is the grouping key of the negotiation-stats rollup.
metadataobjectno—
outcome"agreed" | "declined" | "pending" | "expired"noDefaults to pending.
roundsintegernoDefaults to 1.
started_atstring (date-time)noDefaults to now.

Responses

StatusBodyDescription
201objectRecorded.
400objectinvalid_body, with the validator's issue list under details.issues. Includes the outcome: agreed without agreed_price_minor case.
404objectcounterparty_not_found: the id does not belong to this org and project.

Response 201

FieldTypeRequiredDescription
agent_idstring,nullyes—
agreed_price_minorinteger,nullyesMinor units of currency.
closed_atstring,null (date-time)yesNull exactly when outcome is pending.
counterparty_idstringyes—
currencystringyes—
idstringyesneg_ prefixed.
initial_price_minorinteger,nullyesMinor units of currency.
itemstringyes—
item_refstring,nullyes—
metadataobjectyes—
outcome"agreed" | "declined" | "pending" | "expired"yes—
roundsintegeryes—
started_atstring (date-time)yes—
Example request
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: {}
  }
});
Example response 201
application/json
{
  "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

GEThttps://api.codespar.dev/v1/commerce-memory/preferences

List 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

NameTypeRequiredDescription
counterparty_idstringno—
limitintegerno—
source"explicit" | "derived" | "inherited"no—

Responses

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

Response 200

FieldTypeRequiredDescription
preferencesarray of objectyes—
Example request
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_KEY
import 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");
Example response 200
application/json
{
  "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

POSThttps://api.codespar.dev/v1/commerce-memory/preferences

Set 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

FieldTypeRequiredDescription
confidencenumbernoDefaults to 1.
counterparty_idstringyes—
metadataobjectno—
preference_keystringyesLowercase ASCII, digits and underscore. The vocabulary is open: a new key needs no migration.
source"explicit" | "derived" | "inherited"noDefaults to explicit.
value—noAny 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

StatusBodyDescription
201objectCreated, or updated in place.
400objectinvalid_body, with the validator's issue list under details.issues.
404objectcounterparty_not_found: the id does not belong to this org and project.

Response 201

FieldTypeRequiredDescription
confidencenumberyes0 to 1. 1 means an operator asserted it; below that it was derived.
counterparty_idstringyes—
created_atstring (date-time)yes—
idstringyespref_ prefixed.
metadataobjectyes—
preference_keystringyes—
source"explicit" | "derived" | "inherited"yes—
updated_atstring (date-time)yes—
valuestring | number | boolean | array of — | object | nullyesAny JSON value. Always present.
Example request
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: {}
  }
});
Example response 201
application/json
{
  "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": {}
}
Commerce Memory | CodeSpar