> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sumvin.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Create an Atomic Money Payment Link

> From a signed PurchaseIntent to a shareable URL an agent or a person can settle — end to end against the live contract.

<Snippet file="product-term-disambiguation.mdx" />

<Warning>
  **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.
</Warning>

A payment link is part of **Atomic Money** — Sumvin's payments, ramps, cards, and settlement layer. It is a shareable URL that carries a signed [PurchaseIntent](/identity/pint): the user's **Stamped Mandate** authorising the payment, delivered on the wire as a `PINT`. 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](/concepts/payment-links-x402). Both paths converge on the same signature. **One URL, two audiences, one PINT underneath.**

<Info>
  This guide is for partners in the x402 private alpha. If you have not yet read the [Payment Links and x402](/concepts/payment-links-x402) concept page, start there — it frames the mental model this quickstart assumes.
</Info>

<div className="sv-section-eyebrow">01 / PREREQUISITES</div>

## 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](/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](/identity/pint) 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.

<div className="sv-section-eyebrow">02 / CREATE A LINK</div>

## 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.

<Steps>
  <Step title="Sign the PurchaseIntent payload">
    The inner payload is an EIP-712 `PurchaseIntent` struct. See [PINTs](/identity/pint) for the domain separator, type definitions, and a signing walkthrough. The output is a 130-hex-character ECDSA signature (`0x` + `r` + `s` + `v`).
  </Step>

  <Step title={<>POST to <code>/v0/payment-links</code></>}>
    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 <Tooltip headline="HAL" tip="Hypertext Application Language — the link format used on every Sumvin response." cta="Glossary →" href="/glossary">HAL</Tooltip> navigation.
  </Step>

  <Step title="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).
  </Step>
</Steps>

### Request

See [`POST /v0/payment-links`](/api-reference/payment-links/create-a-payment-link) for the full parameter and response schema reference.

```bash theme={null}
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

| Field             | Type    | Required | Description                                                                                                                                          |
| ----------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pint`            | object  | Yes      | The EIP-712 `PurchaseIntent` payload the owner signed. See [PINTs](/identity/pint) for the field catalogue. Inner `expires_at` is epoch **seconds**. |
| `signature`       | string  | Yes      | `0x`-prefixed hex ECDSA signature. Must match `^0x[0-9a-fA-F]{130}$`.                                                                                |
| `accepted_chains` | array   | Yes      | Chain IDs the owner will accept settlement on. At least one; see [supported chains](#supported-chains).                                              |
| `expires_at`      | integer | Yes      | Link expiry as epoch **milliseconds**. Must be in the future and within 90 days from now.                                                            |
| `max_uses`        | integer | No       | Settlement cap (`≥1`). Omit or `null` for unlimited redemptions until expiry.                                                                        |
| `fee_policy`      | object  | No       | Opaque JSON passed through to settlement. Shape is not stable — do not depend on specific keys without coordination.                                 |
| `description`     | string  | No       | Human-readable label, max 280 characters. Whitespace is stripped.                                                                                    |

<Warning>
  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](#05-lifecycle-and-expiry).
</Warning>

### Response — 201 Created

```json theme={null}
{
  "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

| Field             | Description                                                                                                                          |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `slug`            | URL-safe base64 of the signature bytes (no padding). Validated by `^[A-Za-z0-9_-]{16,128}$`. Use as `{slug}` in subsequent requests. |
| `pint_uri`        | Canonical identifier for the underlying PINT. Used as the path segment in `_links.pint` (URL-encoded).                               |
| `status`          | Lifecycle state: `pending`, `settled`, or `expired`. See [section 05](#05-lifecycle-and-expiry).                                     |
| `amount`          | Settlement amount as a decimal string in the asset's smallest unit (mirrors `pint.max_amount`).                                      |
| `asset`           | Symbol, contract address, and decimals of the asset being paid. `address` is `null` for fiat-denominated assets.                     |
| `pay_to`          | Wallet address that receives settlement funds (`pint.wallet`).                                                                       |
| `accepted_chains` | Chain IDs a payer may settle on.                                                                                                     |
| `usage_count`     | Number of settlements recorded against the link so far.                                                                              |
| `_links.pay`      | The public pay URL — this is what you share with humans **and** agents.                                                              |
| `_links.public`   | The payer-facing JSON view, safe to render without authentication.                                                                   |
| `_links.pint`     | The PINT URI, URL-encoded for direct resolution.                                                                                     |

**The slug is the signature. Two links cannot exist for the same intent.**

<div className="sv-section-eyebrow">03 / SHARE AND INSPECT</div>

## 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}`](/api-reference/payment-links/open-or-settle-a-payment-link).

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](#04-the-x402-handshake) for the agent path.

```bash theme={null}
# 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}`](/api-reference/payment-links/get-a-payment-link-public).

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.

```bash theme={null}
curl https://api.sumvin.com/v0/payment-links/public/q79Rx34K...
```

```json theme={null}
{
  "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](/identity/authentication#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:**

```bash theme={null}
curl https://api.sumvin.com/v0/payment-links/q79Rx34K... \
  -H "x-juno-jwt: <token>"
```

Full schema: [`GET /v0/payment-links/{slug}`](/api-reference/payment-links/get-a-payment-link-by-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:**

```bash theme={null}
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`](/api-reference/payment-links/list-payment-links).

| Query param | Type    | Description                                                                                     |
| ----------- | ------- | ----------------------------------------------------------------------------------------------- |
| `status`    | string  | Filter by lifecycle state: `pending`, `settled`, or `expired`.                                  |
| `from_date` | string  | Start of creation-date range. Accepts epoch ms or `YYYY-MM-DD`.                                 |
| `to_date`   | string  | End of creation-date range. Same format. Inverted ranges return `400 GENERAL_VALIDATION_ERROR`. |
| `limit`     | integer | Page size, `1–100`. Default `50`.                                                               |
| `offset`    | integer | Page offset, `≥0`. Default `0`.                                                                 |

The response carries [HAL pagination](/api-conventions#hypermedia-links-hal) with all filters preserved across pages:

```json theme={null}
{
  "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.**

<div className="sv-section-eyebrow">04 / THE x402 HANDSHAKE</div>

## 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.

```mermaid theme={null}
sequenceDiagram
  participant A as Agent
  participant S as Sumvin /pay/{slug}
  participant F as x402 Facilitator

  A->>S: GET /pay/{slug}<br/>Accept: application/json
  S-->>A: 402 Payment Required<br/>{ accepts: [...] }
  A->>A: Sign challenge with payer wallet
  A->>S: GET /pay/{slug}<br/>PAYMENT-SIGNATURE: <base64 envelope>
  S->>F: Dispatch settlement
  F-->>S: Transaction receipt
  S-->>A: 200 OK<br/>{ success, transaction, receipt_url }
```

### The 402 challenge shape

On the first hit (`Accept: application/json`, no `PAYMENT-SIGNATURE`), the response is `402 X402PaymentRequired`:

```json theme={null}
{
  "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:

```bash theme={null}
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`:

```json theme={null}
{
  "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.**

<div className="sv-section-eyebrow">05 / LIFECYCLE AND EXPIRY</div>

## Pending. Settled. Expired.

A payment link has three lifecycle states. `settled` and `expired` are terminal.

```mermaid theme={null}
stateDiagram-v2
  [*] --> pending : POST /v0/payment-links
  pending --> settled : x402 settlement succeeds
  pending --> expired : effective expiry passes
  settled --> [*]
  expired --> [*]
```

| State     | Meaning                                                    | Fields populated                                                         |
| --------- | ---------------------------------------------------------- | ------------------------------------------------------------------------ |
| `pending` | Issued, unsettled, and inside the effective expiry window. | Creation fields only.                                                    |
| `settled` | A payer successfully redeemed the link.                    | `settled_at`, `tx_hash`, `settle_chain_id`, `payer_addr`, `usage_count`. |
| `expired` | The 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.**

<div className="sv-section-eyebrow">06 / ERRORS</div>

## RFC 7807, everywhere.

Every error is returned as [RFC 7807 Problem Details](/error-handling) with a stable `error_code`. The ones a partner will encounter most:

| Status | Error code                              | When you hit it                                                      |
| ------ | --------------------------------------- | -------------------------------------------------------------------- |
| 400    | `GENERAL_VALIDATION_ERROR`              | Malformed slug, invalid or inverted date range on list.              |
| 400    | `PAYMENT_LINK_X402_INVALID_PAYLOAD`     | `PAYMENT-SIGNATURE` envelope was not parseable.                      |
| 400    | `PAYMENT_LINK_X402_VERSION_MISMATCH`    | Envelope `x402Version` is not `2`.                                   |
| 400    | `PAYMENT_LINK_X402_UNSUPPORTED_SCHEME`  | Envelope scheme is not `exact`.                                      |
| 400    | `PAYMENT_LINK_X402_UNSUPPORTED_NETWORK` | Envelope network is not in the link's `accepted_chains`.             |
| 401    | —                                       | Missing or invalid JWT on an owner-scoped route.                     |
| 403    | —                                       | User-status gate rejected the caller (account not yet at `pending`). |
| 404    | `PAYMENT_LINK_NOT_FOUND`                | Slug does not exist, or exists but is owned by another user.         |
| 409    | `PAYMENT_LINK_SLUG_CONFLICT`            | Slug collision on create — retry the request.                        |
| 409    | `PAYMENT_LINK_ALREADY_SETTLED`          | Retry against a link whose `transaction_id` is populated.            |
| 410    | `PAYMENT_LINK_EXPIRED`                  | Effective expiry has passed.                                         |
| 422    | —                                       | Request body failed schema validation.                               |
| 424    | —                                       | Upstream PINT issuance failed during link creation.                  |
| 429    | —                                       | 60-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.**

<div className="sv-section-eyebrow">07 / SEE ALSO</div>

## Related.

<CardGroup cols={2}>
  <Card title="Payment Links and x402" icon="link-2" href="/concepts/payment-links-x402">
    The concept page — why the URL and the 402 carry the same signature, and what the verifier checks.
  </Card>

  <Card title="Purchase Intents (PINTs)" icon="file-text" href="/identity/pint">
    The signed primitive underneath every payment link. Mint flow, scopes, and EIP-712 shape.
  </Card>

  <Card title="Accept a PINT at checkout" icon="shopping-cart" href="/merchant/quickstarts/accept-pint-checkout">
    The verifier-side flow — what a merchant does when a payer presents a PINT JWT.
  </Card>

  <Card title="Error handling" icon="triangle-alert" href="/error-handling">
    Full RFC 7807 reference and the stable error-code catalogue.
  </Card>
</CardGroup>

## 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 ID | Network     | Alpha status                      |
| -------- | ----------- | --------------------------------- |
| 1329     | Sei         | Launch chain                      |
| 1328     | Sei Testnet | Available for integration testing |
| 8453     | Base        | Coming soon                       |
| 10       | Optimism    | Coming soon                       |
| —        | Tempo       | Coming soon                       |

### Lifecycle statuses

| Value     | Terminal? |
| --------- | --------- |
| `pending` | No        |
| `settled` | Yes       |
| `expired` | Yes       |

### x402 schemes

| Value   | Supported today |
| ------- | --------------- |
| `exact` | Yes             |

## Questions.

<AccordionGroup>
  <Accordion title="Can the same PurchaseIntent back two links?">
    No. The slug is the signature bytes, url-safe base64-encoded — signing the same intent twice produces the same slug, and the second create call returns `409 PAYMENT_LINK_SLUG_CONFLICT` rather than issuing a duplicate. This is intended: one signed intent, one link.
  </Accordion>

  <Accordion title="What happens if `max_uses` is omitted?">
    The link accepts settlements until its effective expiry. Most integrations want `max_uses: 1` — it is the safer default for invoice-style flows.
  </Accordion>

  <Accordion title="Why do owner and public views return different shapes?">
    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.
  </Accordion>

  <Accordion title="Which chains will be supported first?">
    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.
  </Accordion>
</AccordionGroup>
