Payment Links
7 operations under /v1/payment-links (GET POST PATCH DELETE): 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.
Collect is CodeSpar's direct-charge product, and shareable payment links are its first surface. Collect is in early access.
REST API for Collect payment links. A payment link is a shareable URL where the
product IS the payment: an agent reads it and pays over x402/USDC, or a person
pays via a Pix QR. One link can advertise more than one settlement method (rail)
at once. The gateway verifies the payment before fulfillment and seals a
hash-chained receipt. Served at pay_url = https://gw.codespar.dev/pay/<slug>.
Base URL: https://api.codespar.dev
All endpoints require authentication. See Authentication.
Endpoints marked admin require a bearer api key OR service auth with an
x-codespar-user header for an account admin/owner. Collect links are
project-scoped.
Payment link object
| Field | Type | Description |
|---|---|---|
id | string | Payment link ID, pl_<...> |
slug | string | URL slug; global namespace. Auto-generated when omitted |
title | string | Display title |
description | string | null | Optional description |
pay_url | string | https://gw.codespar.dev/pay/<slug> |
accepts | Accept[] | One or more settlement rails (see below) |
environment | "test" | "live" | Environment the link settles in |
one_time | boolean | Closes after the first successful payment |
status | string | Lifecycle status |
use_count | number | Successful payments so far |
max_uses | number | null | Cap on total payments; null for unlimited |
expires_at | string | null | ISO 8601, or null |
redirect_url | string | null | Where a person is sent after paying |
active | boolean | Paused links cannot be paid |
created_at | string | ISO 8601 |
Accept (settlement rail)
accepts[] carries one entry per rail. A link can offer x402/USDC, Pix/BRL, or
both; the payer uses whichever they are provisioned for.
x402 / USDC
{
"rail": "x402",
"amount": "0.01",
"currency": "USDC",
"pay_to": { "kind": "provisioned", "consumer_id": "your-consumer" }
}amountis decimal USDC (up to 6 decimals).pay_to.kind: "provisioned"settles to the CodeSpar wallet ofconsumer_id.pay_to.kind: "byo"settles topay_to.address(a0xEVM address).
Pix / BRL
{
"rail": "pix",
"amount": "5.00",
"currency": "BRL",
"pix": { "key": "your-pix-key" }
}amountis decimal BRL (up to 2 decimals).pix.key(orpix.celcoin_account) is where the Pix payment lands.
Create a payment link
POST /v1/payment-links (admin)
Body
{
"title": "Coffee",
"description": "One coffee",
"accepts": [
{ "rail": "x402", "amount": "0.01", "pay_to": { "kind": "provisioned", "consumer_id": "your-consumer" } },
{ "rail": "pix", "amount": "5.00", "pix": { "key": "your-pix-key" } }
],
"one_time": false,
"max_uses": 10
}slugis optional; a unique one is generated when omitted.acceptsmust have at least one rail.one_time,max_uses,expires_at, andredirect_urlare all optional.
Response — 201 Created with the payment link object.
List payment links
GET /v1/payment-links
Response
{ "payment_links": [/* payment link */] }Get a payment link
GET /v1/payment-links/:id — the payment link object, or 404 payment_link_not_found.
Update a payment link
PATCH /v1/payment-links/:id (admin)
Body (at least one field)
{ "title": "...", "description": "...", "active": false, "expires_at": "2026-12-31T00:00:00Z", "redirect_url": "https://..." }Set active: false to pause the link (it can no longer be paid); true to resume.
The accepts rails are fixed at creation and are not editable.
Delete a payment link
DELETE /v1/payment-links/:id (admin) — 204 No Content. Collect links are
config, not audit data; settled receipts are retained independently.
Errors
| HTTP | code | when |
|---|---|---|
400 | invalid_body | body failed validation (including a malformed x402/Pix amount) |
403 | forbidden | non-admin caller on an admin endpoint |
404 | payment_link_not_found | unknown id in this project |
409 | slug_conflict | the slug is already taken |
Every operation, from the spec
Generated from the published OpenAPI document, so it never drifts from what the API actually serves. The section above is written by hand and carries what a schema cannot: the object model, field rules, and the order to call things in.
GET /v1/payment-links
https://api.codespar.dev/v1/payment-linksList the payment links in this project
Every link under the caller's org AND project, newest first. Not paginated and not filtered: there is no query parameter on this route, so a growing project returns a growing array.
Unlike the three writes on this resource, the list carries no role gate at all.
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
payment_links | array of object | yes | — |
curl -X GET https://api.codespar.dev/v1/payment-links \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/payment-links HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/payment-links",
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/payment-links", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.get("/v1/payment-links");{
"payment_links": [
{
"id": "obj_0000000000000000",
"project_id": "prj_0000000000000000",
"slug": "example",
"title": "Example",
"description": "string",
"pay_url": "https://example.com/hook",
"gateway_unavailable_reason": "gateway_origin_not_configured",
"accepts": [
{
"rail": "x402",
"currency": "USDC",
"amount_atomic": "1000",
"pay_to": "string",
"pay_to_kind": "byo",
"pay_to_account_name": "Example",
"consumer_id": "csm_0000000000000000"
}
],
"environment": "live",
"one_time": true,
"status": "active",
"use_count": 1,
"max_uses": 1,
"expires_at": "2026-01-15T12:00:00.000Z",
"redirect_url": "https://example.com/hook",
"active": true,
"created_at": "2026-01-15T12:00:00.000Z"
}
]
}POST /v1/payment-links
https://api.codespar.dev/v1/payment-linksCreate a multi-rail payment link
A shareable fixed-amount payment where the payment itself is the product, served at pay_url. One entry in accepts per rail, each with its own explicit price: there is NO FX between them, so the x402 amount and the pix amount are two independent prices and nothing reconciles them. A rail may appear at most once; a duplicate is refused with invalid_rail.
Amounts go in as decimal strings and come back converted: x402 takes USDC with up to 6 decimals and returns amount_atomic, pix takes BRL with up to 2 decimals and returns amount_minor. Zero and negative are refused.
For x402, pay_to.kind: "byo" requires a 0x EVM address; "provisioned" requires consumer_id and provisions the receiving wallet during this request, which is why this write can answer 502. For pix, at least one of pix.key or pix.celcoin_account must be present.
slug is optional and a 12-character lowercase alphanumeric one is generated when it is omitted. THE SLUG NAMESPACE IS GLOBAL, not per tenant, because it is the public gateway key: a 409 slug_conflict can therefore be caused by another organization's link, and retrying will not clear it. environment is taken from the calling key and cannot be set in the body. metadata is stored but is not echoed in any response on this surface.
Role-gated: see the 403 below, and read it before assuming the gate runs for your credential.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
accepts | array of object | object | yes | — |
description | string | no | — |
expires_at | string (date-time) | no | — |
max_uses | integer | no | — |
metadata | object | no | — |
one_time | boolean | no | — |
redirect_url | string (uri) | no | — |
slug | string | no | — |
title | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
201 | object | OK |
400 | object | Bad Request. invalid_body when the body did not match the schema (the Zod issues are in details.issues) or when a rail's amount is not a valid positive amount for that rail. invalid_rail when a rail appears twice. invalid_payto when the payee is incomplete for the rail: a byo x402 without a 0x address, a provisioned x402 without consumer_id, or a pix with neither key nor account. |
403 | object | Forbidden. This write is gated on an organization role, and the guard refused. It is reachable with an OAuth access token or a service credential: either no acting user was forwarded in the x-codespar-user header, or the user it named does not hold at least admin in the organization. Re-sending the same header will not help in the second case — promote that user, or forward one who already qualifies. AN API KEY IS NOT REFUSED HERE TODAY. The role requirement is not enforced for that credential at this version, so a key alone completes the write and a 200 is not evidence that anyone holding admin authorized it. Plan for the header: it is the arm that will be closed. |
409 | object | Conflict. That slug is already taken. The namespace is global across tenants, so the holder may not be yours. |
502 | object | Bad Gateway. A provisioned x402 payee could not be given a receiving wallet. Nothing was created; the whole link is refused rather than stored with one rail missing. |
Response 201
| Field | Type | Required | Description |
|---|---|---|---|
accepts | array of object | object | yes | — |
active | boolean | yes | — |
created_at | string (date-time) | yes | — |
description | string,null | yes | — |
environment | "live" | "test" | yes | Taken from the calling key, never from the request body. |
expires_at | string,null (date-time) | yes | — |
gateway_unavailable_reason | "gateway_origin_not_configured" | "gateway_origin_is_production" | yes | Why the gateway URL is null: this deployment is not production and names no gateway origin of its own (gateway_origin_not_configured), or names a production host (gateway_origin_is_production). Null when the URL is present. Production's gateway is never used as a fallback outside production. |
id | string | yes | pl_ + nanoid. |
max_uses | integer,null | yes | — |
one_time | boolean | yes | — |
pay_url | string,null | yes | <gateway origin>/pay/<slug> (production: https://gw.codespar.dev/pay/<slug>). Built from the slug, not stored. Null where this deployment has no gateway; see gateway_unavailable_reason. |
project_id | string | yes | — |
redirect_url | string,null | yes | — |
slug | string | yes | — |
status | "active" | "paid" | "expired" | "void" | yes | Lifecycle, separate from active. active is the operator switch a PATCH flips; status is what the link's own use has made of it. A link is payable only when active is true, status is neither void nor paid, the expiry has not passed and the use cap has not been reached. |
title | string | yes | — |
use_count | integer | yes | — |
curl -X POST https://api.codespar.dev/v1/payment-links \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"slug": "example",
"title": "Example",
"description": "string",
"accepts": [
{
"rail": "x402",
"amount": "1000",
"currency": "USDC",
"pay_to": {
"kind": "byo",
"address": "string",
"consumer_id": "csm_0000000000000000"
}
}
],
"one_time": true,
"max_uses": 1,
"expires_at": "2026-01-15T12:00:00.000Z",
"redirect_url": "https://example.com/hook",
"metadata": {}
}'POST /v1/payment-links HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json
{
"slug": "example",
"title": "Example",
"description": "string",
"accepts": [
{
"rail": "x402",
"amount": "1000",
"currency": "USDC",
"pay_to": {
"kind": "byo",
"address": "string",
"consumer_id": "csm_0000000000000000"
}
}
],
"one_time": true,
"max_uses": 1,
"expires_at": "2026-01-15T12:00:00.000Z",
"redirect_url": "https://example.com/hook",
"metadata": {}
}import os
import requests
res = requests.post(
"https://api.codespar.dev/v1/payment-links",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
json={
"slug": "example",
"title": "Example",
"description": "string",
"accepts": [
{
"rail": "x402",
"amount": "1000",
"currency": "USDC",
"pay_to": {
"kind": "byo",
"address": "string",
"consumer_id": "csm_0000000000000000"
}
}
],
"one_time": True,
"max_uses": 1,
"expires_at": "2026-01-15T12:00:00.000Z",
"redirect_url": "https://example.com/hook",
"metadata": {}
},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/payment-links", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"slug": "example",
"title": "Example",
"description": "string",
"accepts": [
{
"rail": "x402",
"amount": "1000",
"currency": "USDC",
"pay_to": {
"kind": "byo",
"address": "string",
"consumer_id": "csm_0000000000000000"
}
}
],
"one_time": true,
"max_uses": 1,
"expires_at": "2026-01-15T12:00:00.000Z",
"redirect_url": "https://example.com/hook",
"metadata": {}
}),
});
const data = await res.json();const r = await cs.api.response("post", "/v1/payment-links", {
body: {
slug: "example",
title: "Example",
description: "string",
accepts: [
{
rail: "x402",
amount: "1000",
currency: "USDC",
pay_to: {
kind: "byo",
address: "string",
consumer_id: "csm_0000000000000000"
}
}
],
one_time: true,
max_uses: 1,
expires_at: "2026-01-15T12:00:00.000Z",
redirect_url: "https://example.com/hook",
metadata: {}
}
});
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
console.log(r.data);
}{
"id": "obj_0000000000000000",
"project_id": "prj_0000000000000000",
"slug": "example",
"title": "Example",
"description": "string",
"pay_url": "https://example.com/hook",
"gateway_unavailable_reason": "gateway_origin_not_configured",
"accepts": [
{
"rail": "x402",
"currency": "USDC",
"amount_atomic": "1000",
"pay_to": "string",
"pay_to_kind": "byo",
"pay_to_account_name": "Example",
"consumer_id": "csm_0000000000000000"
}
],
"environment": "live",
"one_time": true,
"status": "active",
"use_count": 1,
"max_uses": 1,
"expires_at": "2026-01-15T12:00:00.000Z",
"redirect_url": "https://example.com/hook",
"active": true,
"created_at": "2026-01-15T12:00:00.000Z"
}GET /v1/payment-links/stats
https://api.codespar.dev/v1/payment-links/statsCollect totals for this project's payment links
Totals over the last 30 days, [from, to), computed from the seller-side payment records only: one record per settled link payment, on either rail, written in the link owner's project when the payment settles. Payments made before those records existed are not counted.
Money is reported per currency and BRL and USDC are never added together. A Pix payment whose settlement was uncertain is not counted until it settles.
window accepts only 30d, which is also the default.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
window | "30d" | no | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | Bad Request. window is not one this read computes. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
by_surface | object | yes | Counts in the window. page and whatsapp have no path yet and answer 0. |
from | string (date-time) | yes | — |
links | array of object | yes | Only links with at least one payment in the window, most payments first. |
payments | integer | yes | — |
received | array of object | yes | One entry per currency with a payment in this window or the one before. BRL and USDC are never summed. |
to | string (date-time) | yes | The request time. The window is [from, to). |
window | "30d" | yes | — |
curl -X GET https://api.codespar.dev/v1/payment-links/stats \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/payment-links/stats HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/payment-links/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/payment-links/stats", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.get("/v1/payment-links/stats");{
"window": "30d",
"from": "2026-01-15T12:00:00.000Z",
"to": "2026-01-15T12:00:00.000Z",
"received": [
{
"currency": "BRL",
"amount_minor": "1000",
"previous_amount_minor": "1000"
}
],
"payments": 0,
"by_surface": {
"agent_x402": 0,
"agent_pix": 0,
"page": 0,
"whatsapp": 0
},
"links": [
{
"link_id": "link_0000000000000000",
"payments": 0,
"agent_payments": 0,
"volume": [
{
"currency": "BRL",
"amount_minor": "1000"
}
]
}
]
}GET /v1/payment-links/{id}
https://api.codespar.dev/v1/payment-links/{id}Read one payment link
Addressed by id within the caller's org AND project, the same scope as the list. A link belonging to another project of the same organization, or to another organization, answers 404 with the same body as an id that was never minted.
metadata is not part of this response. There is no operation on this surface that reads it back.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
404 | object | Not Found. No payment link with that id in the caller's project. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
accepts | array of object | object | yes | — |
active | boolean | yes | — |
created_at | string (date-time) | yes | — |
description | string,null | yes | — |
environment | "live" | "test" | yes | Taken from the calling key, never from the request body. |
expires_at | string,null (date-time) | yes | — |
gateway_unavailable_reason | "gateway_origin_not_configured" | "gateway_origin_is_production" | yes | Why the gateway URL is null: this deployment is not production and names no gateway origin of its own (gateway_origin_not_configured), or names a production host (gateway_origin_is_production). Null when the URL is present. Production's gateway is never used as a fallback outside production. |
id | string | yes | pl_ + nanoid. |
max_uses | integer,null | yes | — |
one_time | boolean | yes | — |
pay_url | string,null | yes | <gateway origin>/pay/<slug> (production: https://gw.codespar.dev/pay/<slug>). Built from the slug, not stored. Null where this deployment has no gateway; see gateway_unavailable_reason. |
project_id | string | yes | — |
redirect_url | string,null | yes | — |
slug | string | yes | — |
status | "active" | "paid" | "expired" | "void" | yes | Lifecycle, separate from active. active is the operator switch a PATCH flips; status is what the link's own use has made of it. A link is payable only when active is true, status is neither void nor paid, the expiry has not passed and the use cap has not been reached. |
title | string | yes | — |
use_count | integer | yes | — |
curl -X GET https://api.codespar.dev/v1/payment-links/{id} \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/payment-links/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/payment-links/{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/payment-links/{id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.get("/v1/payment-links/{id}", {
path: {
id: "pl_0000000000000000"
}
});{
"id": "obj_0000000000000000",
"project_id": "prj_0000000000000000",
"slug": "example",
"title": "Example",
"description": "string",
"pay_url": "https://example.com/hook",
"gateway_unavailable_reason": "gateway_origin_not_configured",
"accepts": [
{
"rail": "x402",
"currency": "USDC",
"amount_atomic": "1000",
"pay_to": "string",
"pay_to_kind": "byo",
"pay_to_account_name": "Example",
"consumer_id": "csm_0000000000000000"
}
],
"environment": "live",
"one_time": true,
"status": "active",
"use_count": 1,
"max_uses": 1,
"expires_at": "2026-01-15T12:00:00.000Z",
"redirect_url": "https://example.com/hook",
"active": true,
"created_at": "2026-01-15T12:00:00.000Z"
}PATCH /v1/payment-links/{id}
https://api.codespar.dev/v1/payment-links/{id}Update a payment link's title, copy, expiry, redirect or active flag
Five fields and no others: title, description, active, expires_at, redirect_url. Price, rails, slug, one_time and max_uses are NOT patchable. A body naming one of them has it STRIPPED IN SILENCE rather than refused, so a patch mixing a valid field with a non-patchable one applies the valid half and says nothing about the other. The case that does surface is a patch made entirely of non-patchable fields: it strips down to an empty object, and the empty-patch guard refuses that with 400. Repricing means creating another link.
An omitted field is left alone. description, expires_at and redirect_url additionally accept an explicit null, which CLEARS the column; the difference between absent and null is load bearing here.
active: false takes the link out of service without deleting it, and is the reversible way to stop a live link. It does not change status.
Scoped like the read: the statement names the row by id, org_id and project_id, so a link that belongs to another project of the same organization answers 404, because the update matched no row.
Role-gated: see the 403 below, and read it before assuming the gate runs for your credential.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | — |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
active | boolean | no | — |
description | string,null | no | — |
expires_at | string,null (date-time) | no | — |
redirect_url | string,null (uri) | no | — |
title | string | no | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | Bad Request. The patch did not match the schema, which includes the empty patch and any field outside the five above. The Zod issues are in details.issues. |
403 | object | Forbidden. This write is gated on an organization role, and the guard refused. It is reachable with an OAuth access token or a service credential: either no acting user was forwarded in the x-codespar-user header, or the user it named does not hold at least admin in the organization. Re-sending the same header will not help in the second case — promote that user, or forward one who already qualifies. AN API KEY IS NOT REFUSED HERE TODAY. The role requirement is not enforced for that credential at this version, so a key alone completes the write and a 200 is not evidence that anyone holding admin authorized it. Plan for the header: it is the arm that will be closed. |
404 | object | Not Found. No payment link with that id reachable from this key: either no such id in the caller's organization, or one that lives in another of the organization's projects. The two are not distinguished. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
accepts | array of object | object | yes | — |
active | boolean | yes | — |
created_at | string (date-time) | yes | — |
description | string,null | yes | — |
environment | "live" | "test" | yes | Taken from the calling key, never from the request body. |
expires_at | string,null (date-time) | yes | — |
gateway_unavailable_reason | "gateway_origin_not_configured" | "gateway_origin_is_production" | yes | Why the gateway URL is null: this deployment is not production and names no gateway origin of its own (gateway_origin_not_configured), or names a production host (gateway_origin_is_production). Null when the URL is present. Production's gateway is never used as a fallback outside production. |
id | string | yes | pl_ + nanoid. |
max_uses | integer,null | yes | — |
one_time | boolean | yes | — |
pay_url | string,null | yes | <gateway origin>/pay/<slug> (production: https://gw.codespar.dev/pay/<slug>). Built from the slug, not stored. Null where this deployment has no gateway; see gateway_unavailable_reason. |
project_id | string | yes | — |
redirect_url | string,null | yes | — |
slug | string | yes | — |
status | "active" | "paid" | "expired" | "void" | yes | Lifecycle, separate from active. active is the operator switch a PATCH flips; status is what the link's own use has made of it. A link is payable only when active is true, status is neither void nor paid, the expiry has not passed and the use cap has not been reached. |
title | string | yes | — |
use_count | integer | yes | — |
curl -X PATCH https://api.codespar.dev/v1/payment-links/{id} \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Example",
"description": "string",
"active": true,
"expires_at": "2026-01-15T12:00:00.000Z",
"redirect_url": "https://example.com/hook"
}'PATCH /v1/payment-links/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json
{
"title": "Example",
"description": "string",
"active": true,
"expires_at": "2026-01-15T12:00:00.000Z",
"redirect_url": "https://example.com/hook"
}import os
import requests
res = requests.patch(
"https://api.codespar.dev/v1/payment-links/{id}",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
json={
"title": "Example",
"description": "string",
"active": True,
"expires_at": "2026-01-15T12:00:00.000Z",
"redirect_url": "https://example.com/hook"
},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/payment-links/{id}", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"title": "Example",
"description": "string",
"active": true,
"expires_at": "2026-01-15T12:00:00.000Z",
"redirect_url": "https://example.com/hook"
}),
});
const data = await res.json();const r = await cs.api.response("patch", "/v1/payment-links/{id}", {
path: {
id: "pl_0000000000000000"
},
body: {
title: "Example",
description: "string",
active: true,
expires_at: "2026-01-15T12:00:00.000Z",
redirect_url: "https://example.com/hook"
}
});
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
console.log(r.data);
}{
"id": "obj_0000000000000000",
"project_id": "prj_0000000000000000",
"slug": "example",
"title": "Example",
"description": "string",
"pay_url": "https://example.com/hook",
"gateway_unavailable_reason": "gateway_origin_not_configured",
"accepts": [
{
"rail": "x402",
"currency": "USDC",
"amount_atomic": "1000",
"pay_to": "string",
"pay_to_kind": "byo",
"pay_to_account_name": "Example",
"consumer_id": "csm_0000000000000000"
}
],
"environment": "live",
"one_time": true,
"status": "active",
"use_count": 1,
"max_uses": 1,
"expires_at": "2026-01-15T12:00:00.000Z",
"redirect_url": "https://example.com/hook",
"active": true,
"created_at": "2026-01-15T12:00:00.000Z"
}DELETE /v1/payment-links/{id}
https://api.codespar.dev/v1/payment-links/{id}Delete a payment link
A HARD delete of the row, not a soft one: the link is configuration rather than audit data, so the row is not retained and the operation cannot be undone. It touches only the payment_links row, and no foreign key points at that table, so nothing cascades. The seller-side records of payments already made through the link are kept and still count in /v1/payment-links/stats; the per-link payments read answers 404 once the link is gone.
Prefer PATCH { "active": false } when the intent is to stop a live link, which is reversible and keeps the slug held. Deleting RELEASES the slug back into the global namespace, where anyone can take it.
Scoped like the read and the PATCH: the statement names the row by id, org_id and project_id, so a link in another project of the same organization is not deletable with that key and answers 404.
Not idempotent in its status code: a second delete of the same id answers 404, because the row count is what decides.
Role-gated: see the 403 below, and read it before assuming the gate runs for your credential.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
204 | — | No Content |
403 | object | Forbidden. This write is gated on an organization role, and the guard refused. It is reachable with an OAuth access token or a service credential: either no acting user was forwarded in the x-codespar-user header, or the user it named does not hold at least admin in the organization. Re-sending the same header will not help in the second case — promote that user, or forward one who already qualifies. AN API KEY IS NOT REFUSED HERE TODAY. The role requirement is not enforced for that credential at this version, so a key alone completes the write and a 200 is not evidence that anyone holding admin authorized it. Plan for the header: it is the arm that will be closed. |
404 | object | Not Found. No payment link with that id reachable from this key: either no such id in the caller's organization, or one that lives in another of the organization's projects. The two are not distinguished. |
curl -X DELETE https://api.codespar.dev/v1/payment-links/{id} \
-H "Authorization: Bearer $CODESPAR_API_KEY"DELETE /v1/payment-links/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.delete(
"https://api.codespar.dev/v1/payment-links/{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/payment-links/{id}", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const r = await cs.api.response("delete", "/v1/payment-links/{id}", {
path: {
id: "pl_0000000000000000"
}
});
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
console.log(r.data);
}GET /v1/payment-links/{id}/payments
https://api.codespar.dev/v1/payment-links/{id}/paymentsThe payments one payment link received
One entry per settled payment of this link, newest first, read from the seller-side record written in this project when the payment settled. The payer's own records stay in the payer's organization and are not part of it.
Paginated: send next_before back as before for the next page; it is null on the last page and opaque otherwise. limit is 1 to 100, default 20.
The link is addressed within the caller's org and project, like the read by id: a link of another project answers 404.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | — |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
before | string | no | — |
limit | integer | no | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | Bad Request. limit is outside 1 to 100, or before is not a cursor this read issued for this link. The Zod issues, when there are any, are in details.issues. |
404 | object | Not Found. No payment link with that id in the caller's project. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
next_before | string,null | yes | — |
payments | array of object | yes | — |
curl -X GET https://api.codespar.dev/v1/payment-links/{id}/payments \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/payment-links/{id}/payments HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/payment-links/{id}/payments",
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/payment-links/{id}/payments", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.get("/v1/payment-links/{id}/payments", {
path: {
id: "pl_0000000000000000"
}
});{
"payments": [
{
"id": "obj_0000000000000000",
"link_id": "link_0000000000000000",
"surface": "agent_x402",
"rail": "x402",
"currency": "BRL",
"amount_minor": "1000",
"payer": {
"kind": "agent",
"label": "Example"
},
"txid": "string",
"ledger_entry_id": "ledgerentry_0000000000000000",
"settled_at": "2026-01-15T12:00:00.000Z"
}
],
"next_before": "string"
}Gate
1 operation under /v1/gate/stats (GET): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
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.