Accept stablecoin payments from customers via a hosted checkout page. You create a payment link — Woldy handles the rest.
What a payment link is#
A payment link is a request for payment: the amount you're charging, an optional message and order reference, and a deadline. It gives you a hosted checkout URL to send to your customer.The link itself doesn't carry money. Each attempt to pay it creates a charge — the record of what actually happened on-chain. One link can produce several charges: if an attempt doesn't go through, the customer's next attempt creates a new one. See Charges for how to read them.A payment link is single-use by result, not by attempt: once one charge succeeds, the link is spent and no further attempts are accepted.
Two Ways to Create a Payment Link#
| Dashboard | REST API |
|---|
| Best for | Non-technical merchants, one-off payments | Developers, automated checkout flows |
| Setup time | < 1 min | ~30 min |
| Requires code | No | Yes |
| Webhooks | ✅ | ✅ |
Links created through the Dashboard come back with "source": "DASHBOARD", links created through the API with "source": "API".Links created through the API are always "type": "FIXED" — you set the amount. Flexible links, where the customer chooses how much to pay, can only be created from the Dashboard.
Quick Start#
Step 1 — Create a payment link#
POST /v1/payment-links with price_amount, price_currency, and expires_at. Optionally pass message (shown on the checkout page), return_url (where the customer lands after paying), and external_id — your own order reference, stored as-is.| Field | Constraint |
|---|
price_amount | Decimal string, greater than zero |
price_currency | USD only |
expires_at | In the future, no more than 31 days out |
message | Max 512 characters |
return_url | Absolute http/https URL, max 2048 characters |
external_id | Max 255 characters |
The response carries the checkout URL in payment_url:{
"id": "pl_AAAA...",
"branch_id": "cb_AAAA...",
"reference_type": "PAYMENT_LINK",
"status": "PENDING",
"source": "API",
"type": "FIXED",
"chain_id": null,
"price_currency": "USD",
"price_amount": "10.00",
"pay_currency": null,
"pay_amount": null,
"message": "Order #12345",
"payment_url": "https://gateway.woldy.xyz/pl/AZ-X2wuJfQ-aFbMIEo_Klg",
"return_url": "https://example.com/success",
"external_id": "order_123",
"expires_at": "2026-08-01T00:00:00Z",
"created_at": "2026-07-20T14:15:22.123Z",
"updated_at": "2026-07-20T14:15:22.123Z"
}
There is no idempotency key — sending the same body twice creates two separate links.Optional fields you didn't set come back as empty strings rather than null, so check for both if you branch on them.Step 2 — Send the customer to payment_url#
Redirect your customer to the payment_url from the response. On that page the customer will:1.
Connect their wallet via WalletConnect
2.
Review the payment amount
3.
Sign and submit the transaction
Woldy handles everything from here. No additional code needed on your side.Step 3 — Listen for the result#
Subscribe to payment_link.* events to follow the request through its lifecycle:| Event | Fires when | Link status after |
|---|
payment_link.created | The link was created | PENDING |
payment_link.processing | A customer started paying — a charge is in flight | PROCESSING |
payment_link.attempt_failed | That attempt failed or was abandoned | back to PENDING |
payment_link.completed | An attempt succeeded — fulfil the order here | COMPLETED |
payment_link.expired | The deadline passed with no successful payment | EXPIRED |
payment_link.cancelled | You cancelled the link | CANCELLED |
processing and attempt_failed can repeat: each retry produces another pair. Only completed means you've been paid.To also see the individual attempts — including the exact token and network paid — subscribe to charge.* as well.See Webhooks for setup, event payloads, and signature verification.Step 4 — (Optional) Poll for status#
GET /v1/payment-links/{payment_link_id} returns the link's current status — whether the request was fulfilled. For the attempt-by-attempt history, query charges by reference — see Charges.
Amounts#
| Field | Meaning |
|---|
price_amount / price_currency | What you charged. price_currency is USD today |
pay_amount / pay_currency | What was actually paid, copied from the successful charge. null until the link is COMPLETED |
chain_id | The network of the link's most recent charge, in CAIP-2 format — for example eip155:8453 for Base. null until someone has attempted payment |
pay_* is a convenience copy so you can read the outcome without a second request. The source of truth for what was paid is always the charge itself.USD and stablecoins are treated 1:1 today, so pay_amount matches price_amount numerically. When fiat pricing arrives these will diverge — read them as separate values from the start.
Statuses#
| Status | Meaning |
|---|
PENDING | Waiting to be paid |
PROCESSING | A customer is paying — an attempt is in flight |
COMPLETED | An attempt succeeded — the link is spent |
EXPIRED | expires_at passed with no successful payment |
CANCELLED | You cancelled it |
PENDING and PROCESSING are both open states: the link hasn't been paid yet. A failed attempt returns the link to PENDING, ready for the customer to try again — only a successful charge moves it to COMPLETED, no matter how many attempts came before.Use PROCESSING to show "payment in progress" in your UI, but wait for COMPLETED before fulfilling the order.
Listing links#
GET /v1/payment-links returns the links belonging to your branch:| Filter | Value |
|---|
statuses | Comma-separated statuses, e.g. PENDING,COMPLETED |
sources | Comma-separated: DASHBOARD, API |
ids | Comma-separated link UIDs |
external_id | Exact match on your own reference |
created_at_from / created_at_to | ISO-8601 range |
limit / offset | limit 1–100, defaults to 50 |
Unrecognized filter values are ignored rather than rejected, so a typo returns unfiltered results instead of an error.meta.next is always present in the response and doesn't indicate that more results exist — compare offset + limit against meta.total to know when to stop.
Cancelling a link#
POST /v1/payment-links/{payment_link_id}/cancel takes no body.Only PENDING links can be cancelled — a link that is PROCESSING, COMPLETED, EXPIRED, or already CANCELLED returns 400. Cancelling closes any charge still open. Amount and deadline can't be edited — create a new link instead.
API Reference#
Errors#
Every error uses the same envelope. Validation failures list the offending fields in source:{
"code": 400,
"message": "One or more fields have an error",
"source": {
"price_amount": "Must be greater than zero",
"expires_at": "Must be in future"
}
}
| Status | Meaning |
|---|
400 Bad Request | Validation failed, or the link isn't PENDING and can't be cancelled |
401 Unauthorized | Missing or invalid API key, wrong environment, or blocked by the token's IP allowlist |
403 Forbidden | The token is read-only and this is a write operation |
404 Not Found | No link with that ID — including links that belong to another branch |
429 Too Many Requests | Rate limit exceeded. Check the Retry-After header |
500 Internal Server Error | Something went wrong on our end — contact support |
Modified at 2026-08-19 16:09:48