Skip to main content
Private alpha. The x402 payment-link surface is ecosystem-partner access only and not yet stable. Endpoint shapes, error codes, and chain support will change before general availability — coordinate with your account manager before any production integration.
A payment link is a shareable URL that carries a signed PurchaseIntent. A person opens it in a browser and is bounced to the canonical payer-facing resource. A machine hits the same URL with Accept: application/json and receives a 402 Payment Required challenge over x402. Both paths converge on the same signature. One URL, two audiences, one PINT underneath.
This guide is for partners in the x402 private alpha. If you have not yet read the Payment Links and x402 concept page, start there — it frames the mental model this quickstart assumes.
01 / PREREQUISITES

Before you start.

Three things have to be in place before the first call lands. Each is a one-time setup step for the owner account.
  • A Platform API credential. Every owner-facing route (POST /v0/payment-links, list, detail) requires a user-status gate — the authenticated account must be onboarded to at least pending. See authentication for the JWT flow.
  • A signer wallet. The PurchaseIntent carried by the link is an EIP-712 payload signed by the owner’s wallet. The signature is the cryptographic anchor — the link’s slug is literally the signature bytes, base64url-encoded. See PINTs for the mint flow, scope catalogue, and the canonical EIP-712 domain.
  • A payer-side wallet that speaks x402. Any EIP-3009-capable wallet on a supported chain can settle the link. Browser payers land on the hosted pay view; agents negotiate the 402 handshake directly.
The x402 surface is in private alpha for ecosystem partners. Access is staged — the contract below is the shape partners build against today, not a GA promise.
02 / CREATE A LINK

Sign once, share forever.

A payment link is created with a single POST. The owner signs the inner PurchaseIntent with their wallet, submits that payload alongside the hex ECDSA signature, and the API returns a PaymentLinkResponse whose slug is deterministically derived from the signature bytes. Re-signing the same intent produces the same slug — this is intentional and guards against duplicate issuance.
1

Sign the PurchaseIntent payload

The inner payload is an EIP-712 PurchaseIntent struct. See PINTs for the domain separator, type definitions, and a signing walkthrough. The output is a 130-hex-character ECDSA signature (0x + r + s + v).
2

POST to /v0/payment-links

Submit the payload, signature, accepted chains, and an expiry. The API verifies the signature against the payload, mints the underlying PINT, derives the slug, persists the link, and returns the full response with navigation.
3

Share the slug or the pay URL

The owner keeps two artifacts: the slug (the canonical ID) and _links.pay.href (the public URL to share with a payer).

Request

See POST /v0/payment-links for the full parameter and response schema reference.
curl -X POST https://api.sumvin.com/v0/payment-links \
  -H "x-juno-jwt: <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "pint": {
      "wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD78",
      "nonce": 1,
      "statement": "Settle my $100 invoice",
      "scopes": [
        "sr:us:pint:spend:execute?max=100000000&asset=USDC@sei&chain_id=1329"
      ],
      "resources": [],
      "max_amount": 100000000,
      "max_amount_token": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD78",
      "expires_at": 1740003600
    },
    "signature": "0x1b2c3d...<130 hex chars total>",
    "accepted_chains": [1329],
    "expires_at": 1740003600000,
    "max_uses": 1,
    "description": "Invoice #4821 — Acme Corp"
  }'

Request fields

FieldTypeRequiredDescription
pintobjectYesThe EIP-712 PurchaseIntent payload the owner signed. See PINTs for the field catalogue. Inner expires_at is epoch seconds.
signaturestringYes0x-prefixed hex ECDSA signature. Must match ^0x[0-9a-fA-F]{130}$.
accepted_chainsarrayYesChain IDs the owner will accept settlement on. At least one; see supported chains.
expires_atintegerYesLink expiry as epoch milliseconds. Must be in the future and within 90 days from now.
max_usesintegerNoSettlement cap (≥1). Omit or null for unlimited redemptions until expiry.
fee_policyobjectNoOpaque JSON passed through to settlement. Shape is not stable — do not depend on specific keys without coordination.
descriptionstringNoHuman-readable label, max 280 characters. Whitespace is stripped.
The link’s expires_at is epoch milliseconds. The inner pint.expires_at is epoch seconds — the EIP-712 convention the signed payload carries. The API normalises both to milliseconds on the server, and the effective expiry is the stricter of the two. See section 05.

Response — 201 Created

{
  "payment_link": {
    "slug": "q79Rx34K...87charsTotal",
    "pint_uri": "sr:us:pint:abc123",
    "status": "pending",
    "description": "Invoice #4821 — Acme Corp",
    "amount": "100000000",
    "asset": {
      "symbol": "USDC",
      "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
      "decimals": 6
    },
    "pay_to": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD78",
    "accepted_chains": [1329],
    "fee_policy": null,
    "max_uses": 1,
    "usage_count": 0,
    "payer_addr": null,
    "tx_hash": null,
    "settle_chain_id": null,
    "created_at": 1739999000000,
    "expires_at": 1740003600000,
    "settled_at": null
  },
  "_links": {
    "self":   { "href": "/v0/payment-links/q79Rx34K..." },
    "public": { "href": "/v0/payment-links/public/q79Rx34K..." },
    "pay":    { "href": "/pay/q79Rx34K..." },
    "pint":   { "href": "/v0/pint/sr%3Aus%3Apint%3Aabc123" }
  }
}

Key response fields

FieldDescription
slugURL-safe base64 of the signature bytes (no padding). Validated by ^[A-Za-z0-9_-]{16,128}$. Use as {slug} in subsequent requests.
pint_uriCanonical identifier for the underlying PINT. Used as the path segment in _links.pint (URL-encoded).
statusLifecycle state: pending, settled, or expired. See section 05.
amountSettlement amount as a decimal string in the asset’s smallest unit (mirrors pint.max_amount).
assetSymbol, contract address, and decimals of the asset being paid. address is null for fiat-denominated assets.
pay_toWallet address that receives settlement funds (pint.wallet).
accepted_chainsChain IDs a payer may settle on.
usage_countNumber of settlements recorded against the link so far.
_links.payThe public pay URL — this is what you share with humans and agents.
_links.publicThe payer-facing JSON view, safe to render without authentication.
_links.pintThe PINT URI, URL-encoded for direct resolution.
The slug is the signature. Two links cannot exist for the same intent.
03 / SHARE AND INSPECT

Three surfaces, one truth.

A payment link speaks through three surfaces: the shareable pay URL, the public payer view, and the owner-scoped inspection endpoints. They are three windows onto the same persisted object — nothing about the link changes based on which surface reads it.

The shareable pay URL — /pay/{slug}

See GET /pay/{slug}. This is the URL you hand to a payer. It is protocol-agnostic: a browser gets an HTML page that meta-refreshes to the public view; an agent sets Accept: application/json and begins the x402 handshake. See section 04 for the agent path.
# Browser path — HTML meta-refresh to the public resource
curl -H "Accept: text/html" https://api.sumvin.com/pay/q79Rx34K...
The pay URL is unauthenticated and rate-limited at 60 requests per minute per IP.

The public payer view — GET /v0/payment-links/public/{slug}

See GET /v0/payment-links/public/{slug}. A payer-facing JSON projection safe to render without authentication. It omits what the owner should not share publicly and includes the full signed EIP-712 mandate so a wallet or agent can verify the signature locally before settling.
curl https://api.sumvin.com/v0/payment-links/public/q79Rx34K...
{
  "payment_link": {
    "slug": "q79Rx34K...",
    "status": "pending",
    "description": "Invoice #4821 — Acme Corp",
    "pay_to": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD78",
    "amount": "100000000",
    "asset": {
      "symbol": "USDC",
      "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
      "decimals": 6
    },
    "accepted_chains": [1329],
    "fee_policy": null,
    "expires_at": 1740003600000,
    "created_at": 1739999000000,
    "settled_at": null,
    "tx_hash": null,
    "settle_chain_id": null,
    "payer_addr": null,
    "eip712_message": { "primaryType": "PurchaseIntent", "domain": { "...": "..." }, "message": { "...": "..." } },
    "signature": "0x1b2c3d...",
    "nonce": 1
  },
  "_links": {
    "self": { "href": "/v0/payment-links/public/q79Rx34K..." },
    "pay":  { "href": "/pay/q79Rx34K..." },
    "pint": { "href": "/v0/pint/sr%3Aus%3Apint%3Aabc123" }
  }
}
This endpoint has its own rate limits and returns 410 Gone with PAYMENT_LINK_EXPIRED once the effective expiry has passed.

Owner inspection — authenticated

The owner can fetch a single link or list their links with filters. Both endpoints require the same JWT used for creation. Get one link:
curl https://api.sumvin.com/v0/payment-links/q79Rx34K... \
  -H "x-juno-jwt: <token>"
Full schema: GET /v0/payment-links/{slug}. Returns the same PaymentLinkResponse shape as POST. If the slug does not exist — or exists but belongs to another user — the response is 404 PAYMENT_LINK_NOT_FOUND. Ownership is not disclosed. List with filters:
curl "https://api.sumvin.com/v0/payment-links?status=pending&from_date=2026-04-01&to_date=2026-04-30&limit=25" \
  -H "x-juno-jwt: <token>"
Full schema: GET /v0/payment-links.
Query paramTypeDescription
statusstringFilter by lifecycle state: pending, settled, or expired.
from_datestringStart of creation-date range. Accepts epoch ms or YYYY-MM-DD.
to_datestringEnd of creation-date range. Same format. Inverted ranges return 400 GENERAL_VALIDATION_ERROR.
limitintegerPage size, 1–100. Default 50.
offsetintegerPage offset, ≥0. Default 0.
The response carries HAL pagination with all filters preserved across pages:
{
  "data": [ /* list of PaymentLinkData */ ],
  "total": 142,
  "limit": 25,
  "offset": 0,
  "_links": {
    "self":  { "href": "/v0/payment-links?status=pending&from_date=..." },
    "first": { "href": "/v0/payment-links?...&offset=0" },
    "prev":  null,
    "next":  { "href": "/v0/payment-links?...&offset=25" },
    "last":  { "href": "/v0/payment-links?...&offset=125" }
  }
}
The pay URL is for payers. The inspection endpoints are for owners. Same object, different audiences.
04 / THE x402 HANDSHAKE

Two round trips. One signature.

An agent that resolves /pay/{slug} sets Accept: application/json and receives a 402 Payment Required response carrying an x402 v2 challenge. The agent signs the challenge with its payer wallet, base64-encodes the signed envelope, and retries the same URL with the envelope in the PAYMENT-SIGNATURE header. The server validates the envelope, dispatches to the x402 facilitator, and returns a settlement receipt.

The 402 challenge shape

On the first hit (Accept: application/json, no PAYMENT-SIGNATURE), the response is 402 X402PaymentRequired:
{
  "x402Version": 2,
  "error": null,
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:1329",
      "maxAmountRequired": "100000000",
      "resource": "/pay/q79Rx34K...",
      "description": "Invoice #4821 — Acme Corp",
      "mimeType": "application/json",
      "payTo": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD78",
      "maxTimeoutSeconds": 300,
      "asset": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
      "outputSchema": null,
      "extra": null
    }
  ],
  "extensions": null
}
One accepts entry is returned per chain in the owner’s accepted_chains, in the order the owner provided. Only the exact scheme is supported today; an envelope carrying a different scheme is rejected with 400 PAYMENT_LINK_X402_UNSUPPORTED_SCHEME.

The retry

The agent constructs an x402 signed-payload envelope, base64-encodes it (standard base64, not base64url), and retries:
curl https://api.sumvin.com/pay/q79Rx34K... \
  -H "Accept: application/json" \
  -H "PAYMENT-SIGNATURE: <base64-encoded signed envelope>"
The envelope itself is validated at the route layer — x402Version, scheme, and network are checked against the link’s accepted set. The inner payload is scheme-specific (EIP-3009 fields for exact on EVM) and is carried opaquely to the facilitator. A successful retry returns 200 X402SettlementResponse:
{
  "success": true,
  "transaction": "0xababab...",
  "network": "eip155:1329",
  "payer": "0xcdcdcd...",
  "receipt_url": "/v0/payment-links/public/q79Rx34K...",
  "_links": {
    "self":      { "href": "/pay/q79Rx34K..." },
    "canonical": { "href": "/v0/payment-links/public/q79Rx34K..." }
  }
}
A retry against a link that has already been settled returns 409 PAYMENT_LINK_ALREADY_SETTLED regardless of envelope validity — the check sits above the signature branch. The signature does not reopen a closed link.
05 / LIFECYCLE AND EXPIRY

Pending. Settled. Expired.

A payment link has three lifecycle states. settled and expired are terminal.
StateMeaningFields populated
pendingIssued, unsettled, and inside the effective expiry window.Creation fields only.
settledA payer successfully redeemed the link.settled_at, tx_hash, settle_chain_id, payer_addr, usage_count.
expiredThe effective expiry passed before settlement. Terminal.Creation fields only.
Effective expiry is the stricter of payment_link.expires_at and the underlying pint.expires_at. Both are persisted as epoch milliseconds — the API normalises the PINT’s seconds-valued EIP-712 input on the server. Whichever deadline comes first closes the window for both the public view and the agent pay route. Once a link is settled, further retries return 409 PAYMENT_LINK_ALREADY_SETTLED. The public view continues to render — settled_at, tx_hash, and payer_addr are now populated and serve as a permanent receipt. A settled link is a permanent record, not a deleted one.
06 / ERRORS

RFC 7807, everywhere.

Every error is returned as RFC 7807 Problem Details with a stable error_code. The ones a partner will encounter most:
StatusError codeWhen you hit it
400GENERAL_VALIDATION_ERRORMalformed slug, invalid or inverted date range on list.
400PAYMENT_LINK_X402_INVALID_PAYLOADPAYMENT-SIGNATURE envelope was not parseable.
400PAYMENT_LINK_X402_VERSION_MISMATCHEnvelope x402Version is not 2.
400PAYMENT_LINK_X402_UNSUPPORTED_SCHEMEEnvelope scheme is not exact.
400PAYMENT_LINK_X402_UNSUPPORTED_NETWORKEnvelope network is not in the link’s accepted_chains.
401Missing or invalid JWT on an owner-scoped route.
403User-status gate rejected the caller (account not yet at pending).
404PAYMENT_LINK_NOT_FOUNDSlug does not exist, or exists but is owned by another user.
409PAYMENT_LINK_SLUG_CONFLICTSlug collision on create — retry the request.
409PAYMENT_LINK_ALREADY_SETTLEDRetry against a link whose transaction_id is populated.
410PAYMENT_LINK_EXPIREDEffective expiry has passed.
422Request body failed schema validation.
424Upstream PINT issuance failed during link creation.
42960-per-minute per-IP rate limit on public endpoints.
The instance field on each error points to the route that emitted it; the trace_id field ties it to your Logfire span for support. One format, one code space — read the code and act.
07 / SEE ALSO

Payment Links and x402

The concept page — why the URL and the 402 carry the same signature, and what the verifier checks.

Purchase Intents (PINTs)

The signed primitive underneath every payment link. Mint flow, scopes, and EIP-712 shape.

Accept a PINT at checkout

The verifier-side flow — what a merchant does when a payer presents a PINT JWT.

Error handling

Full RFC 7807 reference and the stable error-code catalogue.

Reference

Supported chains

At private-alpha launch, Sei is the settlement network. Base, Optimism, and Tempo follow shortly after. The accepted_chains field stays enum-typed across the roster — code against the enum and new networks surface without an API change.
Chain IDNetworkAlpha status
1329SeiLaunch chain
1328Sei TestnetAvailable for integration testing
8453BaseComing soon
10OptimismComing soon
TempoComing soon

Lifecycle statuses

ValueTerminal?
pendingNo
settledYes
expiredYes

x402 schemes

ValueSupported today
exactYes

Questions.

The link accepts settlements until its effective expiry. Most integrations want max_uses: 1 — it is the safer default for invoice-style flows.
The owner view mirrors the create response — it is the authoritative object, including pint_uri and usage_count useful for reconciliation. The public view adds the full EIP-712 message, signature, and nonce so a payer wallet can verify locally before settling. Neither view exposes anything that would compromise the link.
Sei at launch. Base, Optimism, and Tempo follow shortly after. The Sei testnet (1328) is available for integration testing. The accepted_chains field stays enum-typed across the roster — code against the enum and new networks surface without an API change.