Cart
5 operations under /v1/cart (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.
POST /v1/cart/checkout
https://api.codespar.dev/v1/cart/checkoutan order exists on the merchant's side, in whatever pending state that merchant uses
Finalize a cart into something payable
Takes a cart to the end of the merchant's own checkout and returns the instrument that settles it. STILL NOT A PAYMENT: what comes back is a Pix copia-e-cola or a checkout URL, and paying it is a separate act on a separate rail. What HAS happened by the time this returns is that an order exists on the merchant's side, in whatever pending state that merchant uses.
WHICH FIELD CARRIES THE RESULT DEPENDS ON THE TIER, and you should read both rather than assume. A Tier 0 merchant (the hosted browser drove its storefront) fills pix_copia_e_cola with the store's real, payable BR code. A Tier 1 merchant (a connected API, Nuvemshop today) fills checkout_url with the storefront page where the buyer completes payment through the merchant's own PSP, and leaves the Pix field empty. rail tells you which one answered.
TWO WAYS TO SAY WHAT IS BEING BOUGHT, and they are not interchangeable. Merchants with a catalog take items. Mercado Livre has no buyer-purchase API at all, so it takes url — the listing page — and refuses without it (meli_url_required). Sending neither is items_required.
MERCADO LIVRE BUYS ON A PERSON'S OWN ACCOUNT. Pass consumer_id and the checkout resolves THAT consumer's connected Mercado Livre context, so the order lands on their account rather than a shared one. In live this is required, not preferred: without a connected context the call is refused with meli_not_connected rather than quietly falling back to the shared demo context, which is test-only. The connection is made first through POST /v1/cart/meli/connect/start.
THE BUYER AND ADDRESS FIELDS ARE OPTIONAL IN THE SCHEMA AND REQUIRED IN LIVE, and the difference is deliberate. In TEST the hosted browser fills sandbox defaults so an integration can run end to end without inventing a real person. In LIVE the checkout REFUSES — buyer_identity_required, shipping_address_required — rather than fabricate identity or shipping details on an order that will really be placed and really be delivered somewhere. The refusal message names the missing fields. Read the optionality as an environment rule, not as permission to omit them on a real order.
SLOW BY CONSTRUCTION ON TIER 0. A browser-driven checkout is a real browser walking a real storefront; the worker is allowed a retry budget of roughly 165 seconds plus a final in-flight attempt, and the client waits past that. Allow at least 230 seconds and do not retry on timeout without checking the merchant: a second attempt can place a second order.
A RAIL THAT CANNOT FINISH SYNCHRONOUSLY IS REFUSED, not queued. If a connected rail comes back from its own session step in a non-terminal state, the answer is 502 rail_async_not_supported. There is no session-to-rail persistence behind this route, so there would be nothing to resume; saying so is more useful than handing back a handle that leads nowhere.
WHAT IS NOT HERE. No idempotency key, so a retried request is a retried checkout. No route to read a checkout back by id. No cancellation: an order placed on the merchant is cancelled at the merchant.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
address | object | no | — |
buyer | object | no | — |
consumer_id | string | no | — |
items | array of object | no | — |
merchant | string | yes | — |
url | string (uri) | no | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | The body did not match the schema — for example a url that is not a URL, or a country that is not two characters. |
422 | object | No rail supports this merchant at all. |
502 | object | The merchant, the hosted browser, or a connected rail failed. rail_async_not_supported is the one that will not resolve on retry: the rail answered with a non-terminal status and this route has nowhere to park it. |
503 | object | No hosted browser is configured for this deployment, so no Tier 0 checkout can run. An operator's configuration to supply; retrying cannot fix it. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
checkout_url | string | no | TIER 1. The storefront page where the buyer completes payment through the merchant's own PSP. Empty on Tier 0. |
merchant | string | no | — |
order_form_id | string | no | The merchant's own order handle, where the rail exposes one. |
order_status | string | no | The merchant's own status string for the order just placed. The vocabulary is the merchant's, not ours. |
pix_copia_e_cola | string | no | TIER 0. The store's real, payable Pix BR code, minted by the merchant's own checkout. Pay it on a Pix rail; nothing in this call moved money. |
rail | string | yes | Which rail finalized the order. |
total_minor | integer | no | Minor units. Absent when the rail did not return a priced total; do not infer zero. |
curl -X POST https://api.codespar.dev/v1/cart/checkout \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"merchant": "string",
"items": [
{
"variant_id": "variant_0000000000000000",
"quantity": 0,
"seller": "string"
}
],
"url": "https://example.com/hook",
"consumer_id": "csm_0000000000000000",
"buyer": {
"email": "person@example.com",
"firstName": "Example",
"lastName": "Example",
"document": "string",
"phone": "string"
},
"address": {
"postalCode": "string",
"street": "string",
"number": "string",
"neighborhood": "string",
"city": "string",
"state": "string",
"complement": "string",
"country": "string"
}
}'POST /v1/cart/checkout HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json
{
"merchant": "string",
"items": [
{
"variant_id": "variant_0000000000000000",
"quantity": 0,
"seller": "string"
}
],
"url": "https://example.com/hook",
"consumer_id": "csm_0000000000000000",
"buyer": {
"email": "person@example.com",
"firstName": "Example",
"lastName": "Example",
"document": "string",
"phone": "string"
},
"address": {
"postalCode": "string",
"street": "string",
"number": "string",
"neighborhood": "string",
"city": "string",
"state": "string",
"complement": "string",
"country": "string"
}
}import os
import requests
res = requests.post(
"https://api.codespar.dev/v1/cart/checkout",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
json={
"merchant": "string",
"items": [
{
"variant_id": "variant_0000000000000000",
"quantity": 0,
"seller": "string"
}
],
"url": "https://example.com/hook",
"consumer_id": "csm_0000000000000000",
"buyer": {
"email": "person@example.com",
"firstName": "Example",
"lastName": "Example",
"document": "string",
"phone": "string"
},
"address": {
"postalCode": "string",
"street": "string",
"number": "string",
"neighborhood": "string",
"city": "string",
"state": "string",
"complement": "string",
"country": "string"
}
},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/cart/checkout", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"merchant": "string",
"items": [
{
"variant_id": "variant_0000000000000000",
"quantity": 0,
"seller": "string"
}
],
"url": "https://example.com/hook",
"consumer_id": "csm_0000000000000000",
"buyer": {
"email": "person@example.com",
"firstName": "Example",
"lastName": "Example",
"document": "string",
"phone": "string"
},
"address": {
"postalCode": "string",
"street": "string",
"number": "string",
"neighborhood": "string",
"city": "string",
"state": "string",
"complement": "string",
"country": "string"
}
}),
});
const data = await res.json();const r = await cs.api.response("post", "/v1/cart/checkout", {
body: {
merchant: "string",
items: [
{
variant_id: "variant_0000000000000000",
quantity: 0,
seller: "string"
}
],
url: "https://example.com/hook",
consumer_id: "csm_0000000000000000",
buyer: {
email: "person@example.com",
firstName: "Example",
lastName: "Example",
document: "string",
phone: "string"
},
address: {
postalCode: "string",
street: "string",
number: "string",
neighborhood: "string",
city: "string",
state: "string",
complement: "string",
country: "string"
}
}
});
// r.status is one of the documented statuses (200, 422),
// each with its own body shape in r.data; nothing here throws on 422.
if (r.ok) {
console.log(r.data);
}{
"merchant": "string",
"rail": "string",
"order_form_id": "orderform_0000000000000000",
"total_minor": 1,
"pix_copia_e_cola": "string",
"checkout_url": "https://example.com/hook",
"order_status": "string"
}POST /v1/cart/ifood/connect/start
https://api.codespar.dev/v1/cart/ifood/connect/startOpen an iFood login for a shopper (connect-once, and iFood cannot be bought from)
Same mechanism as the Mercado Livre start: a hosted browser on a fresh persistent context, opened on ifood.com.br, and a live-view URL the SHOPPER logs into (phone number plus SMS code).
READ THIS BEFORE ASKING ANYONE TO LOG IN. iFood has NO checkout on this API, in live or in test. That is why every response below carries checkout_supported: false and a note saying so in words: the disclosure has to reach the person who is about to type an SMS code, and an agent relaying only the final result of a connect would otherwise never see it. The refusal on the checkout side is not a capability gap that might resolve itself on retry either. runStoreCheckout keeps iFood in a connect-only set and refuses with 400 merchant_not_supported before it looks at any infrastructure, telling the caller not to retry. The login is stored for a capability that does not exist yet.
BEFORE this call returns, the server writes a pending row binding the session it just opened to the context it issued and to the calling org and project. That row is what makes a finish possible, it is single use, and it stops being finishable 30 minutes after the start. A shopper who never logs in costs nothing: the row simply expires.
THE REQUEST BODY IS NOT READ. The handler passes only the merchant and the caller's own authenticated context, so there is no field to send and none to get wrong. One caveat, measured against this repository's Fastify 5 rather than assumed: a request that sets Content-Type: application/json and then sends an EMPTY or malformed body is refused 400 by the framework's body parser before the handler runs, with Fastify's default error body (statusCode, code, error, message, where code is FST_ERR_CTP_EMPTY_JSON_BODY or FST_ERR_CTP_INVALID_JSON_BODY). That is a different shape from the { error, message } below and its code is in neither enum. Sending {}, or sending no body and no content-type, reaches the handler.
SLOWER THAN THE MERCADO LIVRE START. iFood sits behind bot management, and the worker deliberately waits about 15 seconds for that challenge to clear before handing over the live view, so the shopper does not open it on an interstitial. The worker client allows the call at least 120 seconds.
Scope: connections:write, not one of the cart:* scopes. Starting a login is the same act as beginning any other authorization dance that ends in a stored credential, and a key allowed only to build carts should not be able to begin one that later spends on a person's account.
A 502 does not tell you how far the attempt got. The hosted browser call and the pending-row insert are inside the same try, so the code is identical whether the worker never answered or the row failed to write after a browser session was already open. When it is the second, the open session is left to time out on its own. Retrying is safe: every start mints a new context and a new session.
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
502 | object | The hosted browser worker refused or failed, or the pending session could not be recorded. Not terminal: retry mints a new context and session. |
503 | object | No browser worker is configured for this deployment, so no connect can be started at all. Terminal for the caller: retrying cannot fix it, and it is an operator's configuration to supply. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
checkout_supported | false | yes | Always present and always false on this route. Its absence on the Mercado Livre response is the signal that the merchant CAN be bought from; do not read a missing field as false. |
context_id | string | yes | The persistent browser context the login is about to be written into, fresh for this connect. Bind it to nothing yourself: the server remembers which context it issued for this session and later checkouts resolve it from the stored mapping. |
live_view_url | string | yes | The interactive URL a HUMAN opens to log into their own marketplace account. MAY COME BACK AS AN EMPTY STRING, and the call still answers 200: the worker builds it as debuggerFullscreenUrl || debuggerUrl || "" from the hosted browser's debug endpoint, and nothing between there and the response checks it. An empty value means the start failed in practice even though it succeeded on the wire. Discard the session_id and start another connect; there is nothing for the shopper to open. |
merchant | "ifood" | yes | — |
note | string | yes | A fixed disclosure sentence, held as a constant in cart/connect-core.ts and repeated verbatim by the finish call: iFood checkout is not supported yet, the login is stored for a future capability, and no purchase can be made on iFood today in live or in test. |
session_id | string | yes | The live browser session. It is also the handle for the finish call, and the server keeps a single-use pending row for it. Nobody else can present it: a finish is refused unless the row was started by the same org and project. |
curl -X POST https://api.codespar.dev/v1/cart/ifood/connect/start \
-H "Authorization: Bearer $CODESPAR_API_KEY"POST /v1/cart/ifood/connect/start HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.post(
"https://api.codespar.dev/v1/cart/ifood/connect/start",
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/cart/ifood/connect/start", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.post("/v1/cart/ifood/connect/start");{
"merchant": "ifood",
"context_id": "context_0000000000000000",
"session_id": "ses_0000000000000000",
"live_view_url": "https://example.com/hook",
"checkout_supported": false,
"note": "string"
}POST /v1/cart/meli/connect/start
https://api.codespar.dev/v1/cart/meli/connect/startOpen a Mercado Livre login for a shopper (connect-once, step 1 of 2)
Opens a hosted browser on a fresh persistent context, points it at mercadolivre.com.br, and returns a live-view URL. The SHOPPER opens that URL and logs into their own Mercado Livre account, 2FA included. The login lands in the context, not here.
WHAT IT BUYS YOU LATER. Once the matching finish call has run, a Mercado Livre checkout made for that consumer drives the browser on THEIR account instead of a shared one. Until then this call has changed nothing a shopper can see.
BEFORE this call returns, the server writes a pending row binding the session it just opened to the context it issued and to the calling org and project. That row is what makes a finish possible, it is single use, and it stops being finishable 30 minutes after the start. A shopper who never logs in costs nothing: the row simply expires.
THE REQUEST BODY IS NOT READ. The handler passes only the merchant and the caller's own authenticated context, so there is no field to send and none to get wrong. One caveat, measured against this repository's Fastify 5 rather than assumed: a request that sets Content-Type: application/json and then sends an EMPTY or malformed body is refused 400 by the framework's body parser before the handler runs, with Fastify's default error body (statusCode, code, error, message, where code is FST_ERR_CTP_EMPTY_JSON_BODY or FST_ERR_CTP_INVALID_JSON_BODY). That is a different shape from the { error, message } below and its code is in neither enum. Sending {}, or sending no body and no content-type, reaches the handler.
SLOW BY CONSTRUCTION. The call waits on a real browser starting up and loading the site; the worker client allows it at least 90 seconds. Set a client timeout to match rather than retrying into a second abandoned session.
Scope: connections:write, not one of the cart:* scopes. Starting a login is the same act as beginning any other authorization dance that ends in a stored credential, and a key allowed only to build carts should not be able to begin one that later spends on a person's account.
A 502 does not tell you how far the attempt got. The hosted browser call and the pending-row insert are inside the same try, so the code is identical whether the worker never answered or the row failed to write after a browser session was already open. When it is the second, the open session is left to time out on its own. Retrying is safe: every start mints a new context and a new session.
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
502 | object | The hosted browser worker refused or failed, or the pending session could not be recorded. Not terminal: retry mints a new context and session. |
503 | object | No browser worker is configured for this deployment, so no connect can be started at all. Terminal for the caller: retrying cannot fix it, and it is an operator's configuration to supply. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
context_id | string | yes | The persistent browser context the login is about to be written into, fresh for this connect. Bind it to nothing yourself: the server remembers which context it issued for this session and later checkouts resolve it from the stored mapping. |
live_view_url | string | yes | The interactive URL a HUMAN opens to log into their own marketplace account. MAY COME BACK AS AN EMPTY STRING, and the call still answers 200: the worker builds it as debuggerFullscreenUrl || debuggerUrl || "" from the hosted browser's debug endpoint, and nothing between there and the response checks it. An empty value means the start failed in practice even though it succeeded on the wire. Discard the session_id and start another connect; there is nothing for the shopper to open. |
merchant | "meli" | yes | — |
session_id | string | yes | The live browser session. It is also the handle for the finish call, and the server keeps a single-use pending row for it. Nobody else can present it: a finish is refused unless the row was started by the same org and project. |
curl -X POST https://api.codespar.dev/v1/cart/meli/connect/start \
-H "Authorization: Bearer $CODESPAR_API_KEY"POST /v1/cart/meli/connect/start HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.post(
"https://api.codespar.dev/v1/cart/meli/connect/start",
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/cart/meli/connect/start", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.post("/v1/cart/meli/connect/start");{
"merchant": "meli",
"context_id": "context_0000000000000000",
"session_id": "ses_0000000000000000",
"live_view_url": "https://example.com/hook"
}POST /v1/cart/nuvemshop/connect/start
https://api.codespar.dev/v1/cart/nuvemshop/connect/startBegin a Nuvemshop app install for a store (OAuth, step 1 of 2)
Mints a one-time state and returns the Nuvemshop install URL a MERCHANT opens to authorize the CodeSpar app on their own store. Nothing is connected when this returns: the connection exists after the merchant authorizes and Nuvemshop redirects to redirect_uri, which the server exchanges for a store token out of band.
THIS IS A DIFFERENT KIND OF CONNECT FROM THE MARKETPLACE ONES. meli and ifood open a hosted browser and ask a SHOPPER to log in, and what persists is a browser context. This is ordinary OAuth against a store's admin, the person who opens the link is the MERCHANT, and what persists is an API token. Once it exists, this project's checkouts on that store stop going through a browser and go through Nuvemshop's API instead (Tier 1).
THE DOMAIN IS CHECKED AGAINST NUVEMSHOP'S OWN HOSTING, not merely parsed. merchant_domain must end with a Nuvemshop-hosted suffix (.lojavirtualnuvem.com.br, .tiendanube.com, and the platform's other hosted suffixes); a store's custom domain is refused with 400 even though it serves the same shop. Send the hosted domain.
country PICKS THE API ROOT, not a language. br resolves the Brazilian root; ar and mx resolve the international one. Omitting it is fine; it becomes a resolver default rather than an error.
THE STATE IS BOUND TO THE CALLER'S ORG AND PROJECT before the URL is handed back, which is what makes the unauthenticated callback safe: the redirect carries no bearer, so the tenant is recovered from the state row and nothing else. Treat the URL as single use and do not hand the same one to two merchants.
THIS ROUTE ANSWERS ITS OWN 401, unlike the rest of the client surface. The shared preHandler admits service-to-service callers that carry no org or project, and this handler needs both to bind the state, so it refuses again with { error: "auth_required" } — a body no generic 401 handler produces. The 503 is the other one worth reading: it means this DEPLOYMENT has no Nuvemshop app registered (NUVEMSHOP_APP_ID and friends unset), which no caller can fix by retrying.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
country | "br" | "ar" | "mx" | no | — |
merchant_domain | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | The body did not match the schema. The usual cause is a merchant_domain that is a store's custom domain rather than its Nuvemshop-hosted one; the issue's message says which suffixes are accepted. |
401 | object | The caller is authenticated but carries no org and project — a service-to-service key, which the shared preHandler admits and this route cannot use. A different body from the cross-cutting 401. |
502 | object | The state could not be minted. Safe to retry: nothing was handed to the merchant. |
503 | object | This deployment has no Nuvemshop app registered, so no install can be started. An operator sets NUVEMSHOP_APP_ID, NUVEMSHOP_APP_SECRET and NUVEMSHOP_REDIRECT_URI; retrying cannot fix it. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
install_url | string | yes | The Nuvemshop authorize URL the MERCHANT opens. It carries the one-time state; hand it to one merchant and treat it as spent once opened. |
redirect_uri | string | yes | Where Nuvemshop sends the merchant after they authorize. Echoed back so an integrator can confirm it matches what the app is registered with — a mismatch fails at Nuvemshop, not here. |
curl -X POST https://api.codespar.dev/v1/cart/nuvemshop/connect/start \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"merchant_domain": "string",
"country": "br"
}'POST /v1/cart/nuvemshop/connect/start HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json
{
"merchant_domain": "string",
"country": "br"
}import os
import requests
res = requests.post(
"https://api.codespar.dev/v1/cart/nuvemshop/connect/start",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
json={
"merchant_domain": "string",
"country": "br"
},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/cart/nuvemshop/connect/start", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"merchant_domain": "string",
"country": "br"
}),
});
const data = await res.json();const result = await cs.api.post("/v1/cart/nuvemshop/connect/start", {
body: {
merchant_domain: "string",
country: "br"
}
});{
"install_url": "https://example.com/hook",
"redirect_uri": "https://example.com/hook"
}POST /v1/cart/sessions
https://api.codespar.dev/v1/cart/sessionsPrice a cart at a merchant
Builds a cart at the merchant and returns it priced, with line items, totals and the payment methods the merchant's checkout accepts. NOTHING IS BOUGHT and no money moves: this is the quote step, and the numbers in totals are what the merchant says the order would cost right now.
WHICH RAIL ANSWERS IS RESOLVED FOR YOU. The dispatcher tries its rails in registration order and the first one that claims the merchant handles the call; the rail that won is named in rail, so you can tell a connected-API answer from a browser-driven one without knowing the routing table. When no rail claims the merchant, the answer is 422 no_cart_rail, and that is a statement about coverage, not about the merchant being closed or the items being unavailable.
IDENTIFIERS COME FROM A SEARCH, NOT FROM GUESSWORK. variant_id is the merchant's own id for a specific purchasable variant (a size, a colour), which is the granularity a cart needs; a product id will not do. seller is only meaningful on marketplaces where the same variant is offered by several sellers.
LIMITS ARE THE VALIDATOR'S, and they refuse rather than truncate: at least 1 and at most 50 line items, and a quantity between 1 and 99 per line.
THE SESSION IS NOT A RESERVATION. Nothing here holds stock or freezes a price, and there is no route to read a session back later: the object in the 200 is the whole of it. Treat id as the merchant-side handle it is and re-price rather than assume the quote survives.
A TIER 0 (BROWSER-DRIVEN) MERCHANT MAY ANSWER 422 HERE EVEN THOUGH IT SUPPORTS CHECKOUT. The browser rail's own session step is deliberately not implemented — its cart is assembled during checkout, in one browser run — so for those merchants POST /v1/cart/checkout is the entry point and this route is not a required first step.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
items | array of object | yes | — |
merchant | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | The body did not match the schema. Most often an empty items, a quantity outside 1..99, or a variant_id that is not a string. |
422 | object | No rail supports this merchant, or the rail that does cannot assemble a cart outside a checkout run. Not a transient failure: retrying the same merchant answers the same way until a rail covers it. |
502 | object | The merchant, or the hosted browser in front of it, failed or refused. message relays the underlying failure. Safe to retry: no cart persisted here. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
accepts_pix | boolean | no | Convenience mirror of payment_handlers. ABSENT means the rail did not say, not false. |
checkout_url | string | no | A hosted checkout page, where the rail exposes one at this step. |
currency | string | yes | — |
fulfillment_address | object | no | — |
fulfillment_options | array of object | no | — |
id | string | yes | The merchant-side handle for this cart. Not a reservation and not readable back through this API. |
line_items | array of object | yes | — |
merchant | string | yes | — |
payment_handlers | array of object | no | — |
rail | string | yes | Which rail answered (vtex, nuvemshop, browser, acp, ucp, ...). The practical split is connected-API rails against the browser rail, which is what decides latency and what a checkout can return. |
raw | — | no | The rail's own object, passed through for debugging. Its shape is the merchant's and changes without notice; nothing in this API depends on it and neither should you. |
status | "in_progress" | "not_ready_for_payment" | "ready_for_payment" | "completed" | "canceled" | yes | ready_for_payment is the one that matters: the cart can be taken to checkout. not_ready_for_payment means the merchant still wants an address or a fulfillment choice, which this route does not collect. canceled is terminal for this cart. |
totals | array of object | yes | — |
curl -X POST https://api.codespar.dev/v1/cart/sessions \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"merchant": "string",
"items": [
{
"variant_id": "variant_0000000000000000",
"quantity": 0,
"seller": "string"
}
]
}'POST /v1/cart/sessions HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json
{
"merchant": "string",
"items": [
{
"variant_id": "variant_0000000000000000",
"quantity": 0,
"seller": "string"
}
]
}import os
import requests
res = requests.post(
"https://api.codespar.dev/v1/cart/sessions",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
json={
"merchant": "string",
"items": [
{
"variant_id": "variant_0000000000000000",
"quantity": 0,
"seller": "string"
}
]
},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/cart/sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"merchant": "string",
"items": [
{
"variant_id": "variant_0000000000000000",
"quantity": 0,
"seller": "string"
}
]
}),
});
const data = await res.json();const r = await cs.api.response("post", "/v1/cart/sessions", {
body: {
merchant: "string",
items: [
{
variant_id: "variant_0000000000000000",
quantity: 0,
seller: "string"
}
]
}
});
// r.status is one of the documented statuses (200, 422),
// each with its own body shape in r.data; nothing here throws on 422.
if (r.ok) {
console.log(r.data);
}{
"id": "obj_0000000000000000",
"rail": "string",
"merchant": "string",
"status": "in_progress",
"currency": "BRL",
"line_items": [
{
"id": "obj_0000000000000000",
"item": {
"id": "obj_0000000000000000",
"name": "Example",
"unit_amount": 1000
},
"quantity": 0,
"name": "Example",
"unit_amount": 1000,
"sku": "string",
"variant_id": "variant_0000000000000000",
"images": [
"string"
],
"availability_status": "string",
"totals": [
{
"type": "string",
"display_text": "string",
"amount": 1000
}
]
}
],
"totals": [
{
"type": "string",
"display_text": "string",
"amount": 1000
}
],
"fulfillment_options": [
{
"type": "shipping",
"id": "obj_0000000000000000",
"title": "Example",
"totals": [
{
"type": "string",
"display_text": "string",
"amount": 1000
}
]
}
],
"fulfillment_address": {
"name": "Example",
"line_one": "string",
"line_two": "string",
"city": "string",
"state": "string",
"country": "string",
"postal_code": "string"
},
"payment_handlers": [
{
"id": "obj_0000000000000000",
"name": "Example",
"kind": "pix",
"psp": "string"
}
],
"accepts_pix": true,
"checkout_url": "https://example.com/hook"
}Collect
21 operations under /v1/collect (GET POST PATCH PUT): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
Pix in
2 operations under /v1/consumers/{consumerId}/pix (GET): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.