Skip to main content

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.

17 min read
View MarkdownEdit on GitHub

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.

FieldTypeDescription
idstringPayment link ID, pl_<...>
slugstringURL slug; global namespace. Auto-generated when omitted
titlestringDisplay title
descriptionstring | nullOptional description
pay_urlstringhttps://gw.codespar.dev/pay/<slug>
acceptsAccept[]One or more settlement rails (see below)
environment"test" | "live"Environment the link settles in
one_timebooleanCloses after the first successful payment
statusstringLifecycle status
use_countnumberSuccessful payments so far
max_usesnumber | nullCap on total payments; null for unlimited
expires_atstring | nullISO 8601, or null
redirect_urlstring | nullWhere a person is sent after paying
activebooleanPaused links cannot be paid
created_atstringISO 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" }
}
  • amount is decimal USDC (up to 6 decimals).
  • pay_to.kind: "provisioned" settles to the CodeSpar wallet of consumer_id.
  • pay_to.kind: "byo" settles to pay_to.address (a 0x EVM address).

Pix / BRL

{
  "rail": "pix",
  "amount": "5.00",
  "currency": "BRL",
  "pix": { "key": "your-pix-key" }
}
  • amount is decimal BRL (up to 2 decimals).
  • pix.key (or pix.celcoin_account) is where the Pix payment lands.

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
}
  • slug is optional; a unique one is generated when omitted.
  • accepts must have at least one rail.
  • one_time, max_uses, expires_at, and redirect_url are all optional.

Response — 201 Created with the payment link object.

GET /v1/payment-links

Response

{ "payment_links": [/* payment link */] }

GET /v1/payment-links/:id — the payment link object, or 404 payment_link_not_found.

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 /v1/payment-links/:id (admin) — 204 No Content. Collect links are config, not audit data; settled receipts are retained independently.

Errors

HTTPcodewhen
400invalid_bodybody failed validation (including a malformed x402/Pix amount)
403forbiddennon-admin caller on an admin endpoint
404payment_link_not_foundunknown id in this project
409slug_conflictthe 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.

GEThttps://api.codespar.dev/v1/payment-links

List 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

StatusBodyDescription
200objectOK

Response 200

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

POSThttps://api.codespar.dev/v1/payment-links

Create 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

FieldTypeRequiredDescription
acceptsarray of object | objectyes—
descriptionstringno—
expires_atstring (date-time)no—
max_usesintegerno—
metadataobjectno—
one_timebooleanno—
redirect_urlstring (uri)no—
slugstringno—
titlestringyes—

Responses

StatusBodyDescription
201objectOK
400objectBad 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.
403objectForbidden. 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.
409objectConflict. That slug is already taken. The namespace is global across tenants, so the holder may not be yours.
502objectBad 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

FieldTypeRequiredDescription
acceptsarray of object | objectyes—
activebooleanyes—
created_atstring (date-time)yes—
descriptionstring,nullyes—
environment"live" | "test"yesTaken from the calling key, never from the request body.
expires_atstring,null (date-time)yes—
gateway_unavailable_reason"gateway_origin_not_configured" | "gateway_origin_is_production"yesWhy 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.
idstringyespl_ + nanoid.
max_usesinteger,nullyes—
one_timebooleanyes—
pay_urlstring,nullyes<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_idstringyes—
redirect_urlstring,nullyes—
slugstringyes—
status"active" | "paid" | "expired" | "void"yesLifecycle, 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.
titlestringyes—
use_countintegeryes—
Example request
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);
}
Example response 201
application/json
{
  "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

GEThttps://api.codespar.dev/v1/payment-links/stats

Collect 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

NameTypeRequiredDescription
window"30d"no—

Responses

StatusBodyDescription
200objectOK
400objectBad Request. window is not one this read computes.

Response 200

FieldTypeRequiredDescription
by_surfaceobjectyesCounts in the window. page and whatsapp have no path yet and answer 0.
fromstring (date-time)yes—
linksarray of objectyesOnly links with at least one payment in the window, most payments first.
paymentsintegeryes—
receivedarray of objectyesOne entry per currency with a payment in this window or the one before. BRL and USDC are never summed.
tostring (date-time)yesThe request time. The window is [from, to).
window"30d"yes—
Example request
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_KEY
import 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");
Example response 200
application/json
{
  "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}

GEThttps://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

NameTypeRequiredDescription
idstringyes—

Responses

StatusBodyDescription
200objectOK
404objectNot Found. No payment link with that id in the caller's project.

Response 200

FieldTypeRequiredDescription
acceptsarray of object | objectyes—
activebooleanyes—
created_atstring (date-time)yes—
descriptionstring,nullyes—
environment"live" | "test"yesTaken from the calling key, never from the request body.
expires_atstring,null (date-time)yes—
gateway_unavailable_reason"gateway_origin_not_configured" | "gateway_origin_is_production"yesWhy 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.
idstringyespl_ + nanoid.
max_usesinteger,nullyes—
one_timebooleanyes—
pay_urlstring,nullyes<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_idstringyes—
redirect_urlstring,nullyes—
slugstringyes—
status"active" | "paid" | "expired" | "void"yesLifecycle, 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.
titlestringyes—
use_countintegeryes—
Example request
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_KEY
import 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"
  }
});
Example response 200
application/json
{
  "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}

PATCHhttps://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

NameTypeRequiredDescription
idstringyes—

Request body

FieldTypeRequiredDescription
activebooleanno—
descriptionstring,nullno—
expires_atstring,null (date-time)no—
redirect_urlstring,null (uri)no—
titlestringno—

Responses

StatusBodyDescription
200objectOK
400objectBad 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.
403objectForbidden. 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.
404objectNot 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

FieldTypeRequiredDescription
acceptsarray of object | objectyes—
activebooleanyes—
created_atstring (date-time)yes—
descriptionstring,nullyes—
environment"live" | "test"yesTaken from the calling key, never from the request body.
expires_atstring,null (date-time)yes—
gateway_unavailable_reason"gateway_origin_not_configured" | "gateway_origin_is_production"yesWhy 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.
idstringyespl_ + nanoid.
max_usesinteger,nullyes—
one_timebooleanyes—
pay_urlstring,nullyes<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_idstringyes—
redirect_urlstring,nullyes—
slugstringyes—
status"active" | "paid" | "expired" | "void"yesLifecycle, 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.
titlestringyes—
use_countintegeryes—
Example request
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);
}
Example response 200
application/json
{
  "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}

DELETEhttps://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

NameTypeRequiredDescription
idstringyes—

Responses

StatusBodyDescription
204—No Content
403objectForbidden. 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.
404objectNot 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.
Example request
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_KEY
import 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

GEThttps://api.codespar.dev/v1/payment-links/{id}/payments

The 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

NameTypeRequiredDescription
idstringyes—

Query parameters

NameTypeRequiredDescription
beforestringno—
limitintegerno—

Responses

StatusBodyDescription
200objectOK
400objectBad 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.
404objectNot Found. No payment link with that id in the caller's project.

Response 200

FieldTypeRequiredDescription
next_beforestring,nullyes—
paymentsarray of objectyes—
Example request
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_KEY
import 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"
  }
});
Example response 200
application/json
{
  "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"
}
Payment Links | CodeSpar