Skip to main content

Sellers

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

14 min read
View MarkdownEdit on GitHub

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

Every operation below requires a Bearer token. See Authentication.

POST /v1/sellers

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

Onboard a seller (KYB) and affiliate it at Rinne

Creates the sell-side counterparty. The body mirrors Rinne's create_merchant input: full_name, document_number, document_type, document_tax_type, mcc, contact, address and transfer_configurations are required, the rest are optional. contact and address are passed through as free objects and are not validated field by field here.

IDEMPOTENT ON THE DOCUMENT, and this is the field an integrator most needs to know. Before anything is created the flow searches Rinne for a merchant with the same document and for an existing active affiliation, and the local row is written with ON CONFLICT (org_id, project_id, document_number) that reuses the SAME seller_id instead of minting a second one. A repeat call therefore answers 201 again, with the id you already had. The whole flow is serialized per (org, project, document) under an advisory lock, so two concurrent calls cannot both decide the merchant is new.

linked_consumer_id is set on first write and then kept: a later call that sends a different one does not overwrite it. The other KYB fields are overwritten by the re-POST.

document_number must be a CPF (11 digits) or a CNPJ (14 characters, letters allowed in the twelve base positions per IN RFB 2229/2024) whose check digits verify, and document_type / document_tax_type must agree with it (CPF ↔ PF, CNPJ ↔ PJ | MEI | ME); any of those failing is 400 invalid_body with the offending field in issues[].path, before Rinne is reached. It is then stored with every non-digit stripped. A value that survives the schema but contains no digits at all leaves nothing to store, and that surfaces as 502 seller_document_required, not as 400: the check lives in the provisioning flow, past the point where the body was validated.

The response is the seller row, not Rinne's. status and affiliation_status reflect whatever the affiliation was at that instant, which for a fresh onboarding is normally pending rather than active.

Request body

FieldTypeRequiredDescription
addressobjectyes—
allowed_capture_methodsarray of stringno—
allowed_payment_methodsarray of stringno—
company_logo_urlstringno—
contactobjectyes—
declared_incomenumberno—
declared_revenuenumberno—
document_numberstringyes—
document_tax_type"PJ" | "MEI" | "ME" | "PF"yes—
document_type"CNPJ" | "CPF"yes—
fee_policy_idstringno—
full_namestringyes—
linked_consumer_idstringno—
mccstringyes—
metadataobjectno—
namestringno—
net_worthnumberno—
transfer_configurationsobjectyes—
website_urlstringno—

Responses

StatusBodyDescription
201objectOK
400objectBad Request — the body or query did not match the schema.
502objectBad Gateway. Provisioning did not complete. error carries the failing step's code when the throw had one (for example rinne_create_merchant_failed, rinne_merchant_ambiguous, rinne_credentials_unavailable, seller_document_required) and seller_provisioning_failed otherwise. The code set is not closed: the Rinne codes are built from the call's action name. Nothing was written locally when this is returned.

Response 201

FieldTypeRequiredDescription
affiliation_statusstringyesRinne's own status string, stored verbatim (for example PENDING_APPROVAL, ACTIVE, REJECTED). This is the column with NO CHECK constraint, and that is deliberate: a status Rinne introduces later is stored as it came instead of turning into a hard write failure. Treat it as an open set.
created_atstring (date-time)yes—
document_numberstringyesDigits only. Everything non-numeric is stripped before the row is written.
full_namestringyes—
linked_consumer_idstring,nullyes—
rinne_affiliation_idstring,nullyes—
rinne_merchant_idstring,nullyes—
seller_idstringyesslr_ + nanoid. Stable across a re-POST of the same document.
statusstringyesCodeSpar's normalized lifecycle, derived from affiliation_status by deriveSellerStatus. A CHECK constraint on the column admits exactly five values — pending, affiliation_pending, active, rejected, failed — and a write outside that set fails rather than being stored. Three of the five are what this API produces: ACTIVE maps to active, REJECTED to rejected, and every other Rinne string, recognized or not, to affiliation_pending rather than to a failure state. pending is the column default and no onboarding write leaves it in place; nothing writes failed.
updated_atstring (date-time)yes—
wallet_idstring,nullyesThe BRL wallet this seller's settlements credit. Null until the first settlement lands, because the wallet is provisioned lazily on that credit.
Example request
curl -X POST https://api.codespar.dev/v1/sellers \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "full_name": "Example",
       "name": "Example",
       "document_number": "string",
       "document_type": "CNPJ",
       "document_tax_type": "PJ",
       "mcc": "string",
       "contact": {},
       "address": {},
       "transfer_configurations": {
         "automatic_transfer_enabled": true,
         "transfer_frequency": "DAILY",
         "rail": "PIX",
         "utc_hour_of_day": 0,
         "day_of_week": 0,
         "day_of_month": 0,
         "min_balance": 1000
       },
       "declared_revenue": 0,
       "declared_income": 0,
       "net_worth": 0,
       "company_logo_url": "https://example.com/hook",
       "website_url": "https://example.com/hook",
       "fee_policy_id": "feepolicy_0000000000000000",
       "metadata": {},
       "linked_consumer_id": "linkedconsumer_0000000000000000",
       "allowed_capture_methods": [
         "string"
       ],
       "allowed_payment_methods": [
         "string"
       ]
     }'
POST /v1/sellers HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "full_name": "Example",
  "name": "Example",
  "document_number": "string",
  "document_type": "CNPJ",
  "document_tax_type": "PJ",
  "mcc": "string",
  "contact": {},
  "address": {},
  "transfer_configurations": {
    "automatic_transfer_enabled": true,
    "transfer_frequency": "DAILY",
    "rail": "PIX",
    "utc_hour_of_day": 0,
    "day_of_week": 0,
    "day_of_month": 0,
    "min_balance": 1000
  },
  "declared_revenue": 0,
  "declared_income": 0,
  "net_worth": 0,
  "company_logo_url": "https://example.com/hook",
  "website_url": "https://example.com/hook",
  "fee_policy_id": "feepolicy_0000000000000000",
  "metadata": {},
  "linked_consumer_id": "linkedconsumer_0000000000000000",
  "allowed_capture_methods": [
    "string"
  ],
  "allowed_payment_methods": [
    "string"
  ]
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/sellers",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "full_name": "Example",
      "name": "Example",
      "document_number": "string",
      "document_type": "CNPJ",
      "document_tax_type": "PJ",
      "mcc": "string",
      "contact": {},
      "address": {},
      "transfer_configurations": {
        "automatic_transfer_enabled": True,
        "transfer_frequency": "DAILY",
        "rail": "PIX",
        "utc_hour_of_day": 0,
        "day_of_week": 0,
        "day_of_month": 0,
        "min_balance": 1000
      },
      "declared_revenue": 0,
      "declared_income": 0,
      "net_worth": 0,
      "company_logo_url": "https://example.com/hook",
      "website_url": "https://example.com/hook",
      "fee_policy_id": "feepolicy_0000000000000000",
      "metadata": {},
      "linked_consumer_id": "linkedconsumer_0000000000000000",
      "allowed_capture_methods": [
        "string"
      ],
      "allowed_payment_methods": [
        "string"
      ]
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/sellers", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "full_name": "Example",
    "name": "Example",
    "document_number": "string",
    "document_type": "CNPJ",
    "document_tax_type": "PJ",
    "mcc": "string",
    "contact": {},
    "address": {},
    "transfer_configurations": {
      "automatic_transfer_enabled": true,
      "transfer_frequency": "DAILY",
      "rail": "PIX",
      "utc_hour_of_day": 0,
      "day_of_week": 0,
      "day_of_month": 0,
      "min_balance": 1000
    },
    "declared_revenue": 0,
    "declared_income": 0,
    "net_worth": 0,
    "company_logo_url": "https://example.com/hook",
    "website_url": "https://example.com/hook",
    "fee_policy_id": "feepolicy_0000000000000000",
    "metadata": {},
    "linked_consumer_id": "linkedconsumer_0000000000000000",
    "allowed_capture_methods": [
      "string"
    ],
    "allowed_payment_methods": [
      "string"
    ]
  }),
});

const data = await res.json();
const result = await cs.api.post("/v1/sellers", {
  body: {
    full_name: "Example",
    name: "Example",
    document_number: "string",
    document_type: "CNPJ",
    document_tax_type: "PJ",
    mcc: "string",
    contact: {},
    address: {},
    transfer_configurations: {
      automatic_transfer_enabled: true,
      transfer_frequency: "DAILY",
      rail: "PIX",
      utc_hour_of_day: 0,
      day_of_week: 0,
      day_of_month: 0,
      min_balance: 1000
    },
    declared_revenue: 0,
    declared_income: 0,
    net_worth: 0,
    company_logo_url: "https://example.com/hook",
    website_url: "https://example.com/hook",
    fee_policy_id: "feepolicy_0000000000000000",
    metadata: {},
    linked_consumer_id: "linkedconsumer_0000000000000000",
    allowed_capture_methods: [
      "string"
    ],
    allowed_payment_methods: [
      "string"
    ]
  }
});
Example response 201
application/json
{
  "seller_id": "slr_0000000000000000",
  "status": "string",
  "affiliation_status": "string",
  "rinne_merchant_id": "rinnemerchant_0000000000000000",
  "rinne_affiliation_id": "rinneaffiliation_0000000000000000",
  "document_number": "string",
  "linked_consumer_id": "linkedconsumer_0000000000000000",
  "full_name": "Example",
  "wallet_id": "wlt_0000000000000000",
  "created_at": "2026-01-15T12:00:00.000Z",
  "updated_at": "2026-01-15T12:00:00.000Z"
}

GET /v1/sellers/{sellerId}

GEThttps://api.codespar.dev/v1/sellers/{sellerId}

Read one seller

The stored row, with no call to Rinne. Use /status instead when the answer has to be current rather than last known.

Scoped to the caller's org AND project, so a seller onboarded under a different project of the same organization answers 404 here.

Path parameters

NameTypeRequiredDescription
sellerIdstringyes—

Responses

StatusBodyDescription
200objectOK
404objectNot Found. No seller with that id under the caller's org and project. A seller that exists in another tenant answers identically, so a 404 here is not evidence that the id was never minted.

Response 200

FieldTypeRequiredDescription
affiliation_statusstringyesRinne's own status string, stored verbatim (for example PENDING_APPROVAL, ACTIVE, REJECTED). This is the column with NO CHECK constraint, and that is deliberate: a status Rinne introduces later is stored as it came instead of turning into a hard write failure. Treat it as an open set.
created_atstring (date-time)yes—
document_numberstringyesDigits only. Everything non-numeric is stripped before the row is written.
full_namestringyes—
linked_consumer_idstring,nullyes—
rinne_affiliation_idstring,nullyes—
rinne_merchant_idstring,nullyes—
seller_idstringyesslr_ + nanoid. Stable across a re-POST of the same document.
statusstringyesCodeSpar's normalized lifecycle, derived from affiliation_status by deriveSellerStatus. A CHECK constraint on the column admits exactly five values — pending, affiliation_pending, active, rejected, failed — and a write outside that set fails rather than being stored. Three of the five are what this API produces: ACTIVE maps to active, REJECTED to rejected, and every other Rinne string, recognized or not, to affiliation_pending rather than to a failure state. pending is the column default and no onboarding write leaves it in place; nothing writes failed.
updated_atstring (date-time)yes—
wallet_idstring,nullyesThe BRL wallet this seller's settlements credit. Null until the first settlement lands, because the wallet is provisioned lazily on that credit.
Example request
curl -X GET https://api.codespar.dev/v1/sellers/{sellerId} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/sellers/{sellerId} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

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

const data = await res.json();
const result = await cs.api.get("/v1/sellers/{sellerId}", {
  path: {
    sellerId: "slr_0000000000000000"
  }
});
Example response 200
application/json
{
  "seller_id": "slr_0000000000000000",
  "status": "string",
  "affiliation_status": "string",
  "rinne_merchant_id": "rinnemerchant_0000000000000000",
  "rinne_affiliation_id": "rinneaffiliation_0000000000000000",
  "document_number": "string",
  "linked_consumer_id": "linkedconsumer_0000000000000000",
  "full_name": "Example",
  "wallet_id": "wlt_0000000000000000",
  "created_at": "2026-01-15T12:00:00.000Z",
  "updated_at": "2026-01-15T12:00:00.000Z"
}

GET /v1/sellers/{sellerId}/custody

GEThttps://api.codespar.dev/v1/sellers/{sellerId}/custody

Compare this seller's ledger against Rinne's custody

The pre-payout reconciliation: the wallet's available BRL against what Rinne reports holding for the bound merchant. Both sides are BRL MINOR units (centavos), never decimals and never the 6-decimal atomic units the USDC surface uses.

ledger_exceeds_custody is the state to alarm on: the ledger shows more than Rinne holds, and spending against that balance can fail at payout. unattributed_credit is ordinary while a settlement webhook is in flight, and the pending-settlement read above is where to look next.

A seller with no wallet yet compares as zero on the ledger side rather than erroring, so unattributed_credit on a never funded seller is expected. A seller with no Rinne merchant id answers 409 instead of comparing zero against zero and calling it reconciled.

Path parameters

NameTypeRequiredDescription
sellerIdstringyes—

Responses

StatusBodyDescription
200objectOK
404objectNot Found. No seller with that id under the caller's org and project.
409objectConflict. The seller has no rinne_merchant_id yet, so there is no custody to compare against.
502objectBad Gateway. The balance read failed or could not be understood: rinne_get_balance_failed when Rinne answered non-ok, rinne_get_balance_unparseable when the response carried no balance field this code recognizes, seller_custody_failed as the fallback. The code set is not closed.

Response 200

FieldTypeRequiredDescription
difference_minorstringyesrinne_minor minus ledger_minor. Signed and never clamped: the negative direction is the dangerous one and has to survive to the reader.
ledger_minorstringyesWhat the wallet ledger says is available, BRL minor units (centavos).
notestringyesOne sentence for the same three states, ready to render.
rinne_merchant_idstringyes—
rinne_minorstringyesWhat Rinne reports as this merchant's balance, BRL minor units.
seller_idstringyes—
state"reconciled" | "unattributed_credit" | "ledger_exceeds_custody"yesreconciled when the difference is zero. unattributed_credit when Rinne holds more than the ledger attributed, which is the ordinary state while a settlement webhook is in flight. ledger_exceeds_custody when the ledger shows more than Rinne holds, which is the state a payout can fail on.
Example request
curl -X GET https://api.codespar.dev/v1/sellers/{sellerId}/custody \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/sellers/{sellerId}/custody HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

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

const data = await res.json();
const result = await cs.api.get("/v1/sellers/{sellerId}/custody", {
  path: {
    sellerId: "slr_0000000000000000"
  }
});
Example response 200
application/json
{
  "seller_id": "slr_0000000000000000",
  "rinne_merchant_id": "rinnemerchant_0000000000000000",
  "ledger_minor": "1000",
  "rinne_minor": "1000",
  "difference_minor": "1000",
  "state": "reconciled",
  "note": "string"
}

GET /v1/sellers/{sellerId}/ledger

GEThttps://api.codespar.dev/v1/sellers/{sellerId}/ledger

This seller's wallet ledger, without learning the wallet id

A passthrough to the seller's own wallet ledger, scoped by its wallet_id, so a caller addresses the seller it already knows instead of the internal wallet. Same rows, same page shape and the same query as GET /v1/wallets/{id}/ledger. A settlement credit from Rinne lands here as fund and a reversed one as reverse; every other movement on the same wallet appears too, because this reads the whole ledger for that wallet and not a settlement-only view of it.

Newest first. next_before is the id to send as before_id for the next page, and it is null on the last page. It is only non-null when the page came back exactly full, so a final page that happens to be full yields one more request that returns nothing.

A seller that has never been settled has no wallet yet, and that is a normal state rather than an error: it answers 200 with an empty page. That short-circuit runs BEFORE the query is validated, so a malformed limit or before_id on such a seller also returns the empty page instead of 400.

Path parameters

NameTypeRequiredDescription
sellerIdstringyes—

Query parameters

NameTypeRequiredDescription
before_idstringno—
kind"fund" | "hold" | "release" | "debit" | "reconcile" | "reverse" | "fee"no—
limitintegerno—

Responses

StatusBodyDescription
200objectOK
400objectBad Request — the body or query did not match the schema.
404objectNot Found. No seller with that id under the caller's org and project. A seller that exists in another tenant answers identically, so a 404 here is not evidence that the id was never minted.

Response 200

FieldTypeRequiredDescription
entriesarray of objectyes—
next_beforestring,nullyes—
Example request
curl -X GET https://api.codespar.dev/v1/sellers/{sellerId}/ledger \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/sellers/{sellerId}/ledger HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

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

const data = await res.json();
const result = await cs.api.get("/v1/sellers/{sellerId}/ledger", {
  path: {
    sellerId: "slr_0000000000000000"
  }
});
Example response 200
application/json
{
  "entries": [
    {
      "id": "obj_0000000000000000",
      "wallet_id": "wlt_0000000000000000",
      "org_id": "org_0000000000000000",
      "currency": "BRL",
      "amount_minor": "1000",
      "kind": "fund",
      "mandate_id": "mandate_0000000000000000",
      "attempt_id": "attempt_0000000000000000",
      "external_ref": "string",
      "reconciled_at": "2026-01-15T12:00:00.000Z",
      "posted_at": "2026-01-15T12:00:00.000Z",
      "metadata": {}
    }
  ],
  "next_before": "string"
}

GET /v1/sellers/{sellerId}/pending-settlement

GEThttps://api.codespar.dev/v1/sellers/{sellerId}/pending-settlement

What Rinne reports as sold but not yet settled

A read-through view of the seller's Rinne statement, filtered to the movements explicitly marked unsettled. INFORMATIONAL ONLY: this endpoint never writes to the wallet balance or the wallet ledger. The credit happens later, when the settlement webhook reports the movement settled. Money listed here is coming, it is not spendable.

READ unparsed_entries BEFORE READING THE TOTAL. A movement whose settled field is not a real boolean is excluded rather than guessed, so an unrecognized statement shape produces a SMALLER total, not an error. When unparsed_entries is greater than zero, total_pending_minor: "0" means the statement was not understood, not that nothing is pending. The statement shape has not been verified against live Rinne traffic.

409 is the not-yet case rather than a fault: the seller exists locally but has no Rinne merchant id, so there is nothing to ask about.

Path parameters

NameTypeRequiredDescription
sellerIdstringyes—

Responses

StatusBodyDescription
200objectOK
404objectNot Found. No seller with that id under the caller's org and project.
409objectConflict. The seller has no rinne_merchant_id yet, so it was never provisioned at Rinne and has no statement to read.
502objectBad Gateway. The statement read failed. error carries the failing step's code when the throw had one (for example rinne_get_statement_failed, rinne_credentials_unavailable) and seller_pending_settlement_failed otherwise. The code set is not closed.

Response 200

FieldTypeRequiredDescription
entriesarray of objectyes—
parsed_entriesintegeryesHow many of those carried a settled field this code could read as a boolean.
raw_entriesintegeryesHow many movements the statement returned, before any filtering.
rinne_merchant_idstringyes—
seller_idstringyes—
total_pending_minorstringyesSum of amount_minor over the entries below. Informational only: it is not a balance, it is not spendable, and nothing here is ever posted to the wallet ledger.
unparsed_entriesintegeryesraw_entries minus parsed_entries. Greater than zero means total_pending_minor understates what Rinne reported, so a total of 0 is not evidence that nothing is pending.
Example request
curl -X GET https://api.codespar.dev/v1/sellers/{sellerId}/pending-settlement \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/sellers/{sellerId}/pending-settlement HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

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

const data = await res.json();
const result = await cs.api.get("/v1/sellers/{sellerId}/pending-settlement", {
  path: {
    sellerId: "slr_0000000000000000"
  }
});
Example response 200
application/json
{
  "seller_id": "slr_0000000000000000",
  "rinne_merchant_id": "rinnemerchant_0000000000000000",
  "total_pending_minor": "1000",
  "entries": [
    {
      "id": "obj_0000000000000000",
      "amount_minor": "1000",
      "currency": "BRL",
      "type": "string",
      "operation": "string",
      "transaction_id": "transaction_0000000000000000",
      "created_at": "string",
      "raw": {}
    }
  ],
  "raw_entries": 0,
  "parsed_entries": 0,
  "unparsed_entries": 0
}

GET /v1/sellers/{sellerId}/status

GEThttps://api.codespar.dev/v1/sellers/{sellerId}/status

Refresh a seller's affiliation status from Rinne

Reads the affiliation at Rinne and, when the status changed, writes the new one to the row before answering. On-demand, not a scheduled sweep.

DEGRADES RATHER THAN FAILING, and says so. When the Rinne read does not land the last known row is still returned with 200, carrying stale: true and a refresh_error. Read those two before trusting status: a 200 here is not by itself a confirmation. A seller with nothing at Rinne yet (both provider ids null) is not stale, because there is no upstream status for it to lag, so it comes back with refreshed_at: null and refresh_error: null.

The 502 is the narrower case where the call threw outright, for instance because no Rinne credential could be resolved. A non-ok response from Rinne does not reach it; that is the stale path above.

Path parameters

NameTypeRequiredDescription
sellerIdstringyes—

Responses

StatusBodyDescription
200objectOK
404objectNot Found. No seller with that id under the caller's org and project. A seller that exists in another tenant answers identically, so a 404 here is not evidence that the id was never minted.
502objectBad Gateway. The status refresh threw before a row could be rendered. error is always seller_status_failed; the underlying reason is in message.

Response 200

FieldTypeRequiredDescription
affiliation_statusstringyesRinne's own status string, stored verbatim (for example PENDING_APPROVAL, ACTIVE, REJECTED). This is the column with NO CHECK constraint, and that is deliberate: a status Rinne introduces later is stored as it came instead of turning into a hard write failure. Treat it as an open set.
created_atstring (date-time)yes—
document_numberstringyesDigits only. Everything non-numeric is stripped before the row is written.
full_namestringyes—
linked_consumer_idstring,nullyes—
refresh_errorstring,nullyesWhy the Rinne read did not land. Null on success and on a seller with nothing at Rinne yet.
refreshed_atstring,null (date-time)yesWhen the Rinne read that produced this row landed. Null when no read landed, which covers both a failed read and a seller with nothing at Rinne yet.
rinne_affiliation_idstring,nullyes—
rinne_merchant_idstring,nullyes—
seller_idstringyesslr_ + nanoid. Stable across a re-POST of the same document.
stalebooleanyesDerived as refresh_error !== null, so it can never disagree with the field beside it. True means the body is the last known row, not what Rinne says right now.
statusstringyesCodeSpar's normalized lifecycle, derived from affiliation_status by deriveSellerStatus. A CHECK constraint on the column admits exactly five values — pending, affiliation_pending, active, rejected, failed — and a write outside that set fails rather than being stored. Three of the five are what this API produces: ACTIVE maps to active, REJECTED to rejected, and every other Rinne string, recognized or not, to affiliation_pending rather than to a failure state. pending is the column default and no onboarding write leaves it in place; nothing writes failed.
updated_atstring (date-time)yes—
wallet_idstring,nullyesThe BRL wallet this seller's settlements credit. Null until the first settlement lands, because the wallet is provisioned lazily on that credit.
Example request
curl -X GET https://api.codespar.dev/v1/sellers/{sellerId}/status \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/sellers/{sellerId}/status HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

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

const data = await res.json();
const result = await cs.api.get("/v1/sellers/{sellerId}/status", {
  path: {
    sellerId: "slr_0000000000000000"
  }
});
Example response 200
application/json
{
  "seller_id": "slr_0000000000000000",
  "status": "string",
  "affiliation_status": "string",
  "rinne_merchant_id": "rinnemerchant_0000000000000000",
  "rinne_affiliation_id": "rinneaffiliation_0000000000000000",
  "document_number": "string",
  "linked_consumer_id": "linkedconsumer_0000000000000000",
  "full_name": "Example",
  "wallet_id": "wlt_0000000000000000",
  "created_at": "2026-01-15T12:00:00.000Z",
  "updated_at": "2026-01-15T12:00:00.000Z",
  "refreshed_at": "2026-01-15T12:00:00.000Z",
  "refresh_error": "string",
  "stale": true
}
Sellers | CodeSpar