curl -X POST https://api.liddie.io/api/v1/payments \
-H "Authorization: Bearer lid_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-20260713-0001" \
-d '{
"currency": "USDT_TRC20",
"amount": 100,
"merchantOrderId": "SO-1042",
"metadata": { "cart": [1, 2, 3] },
"expiresIn": 3600
}'
const res = await fetch('https://api.liddie.io/api/v1/payments', {
method: 'POST',
headers: {
'Authorization': 'Bearer lid_live_...',
'Content-Type': 'application/json',
'Idempotency-Key': 'order-20260713-0001'
},
body: JSON.stringify({
currency: 'USDT_TRC20',
amount: 100,
merchantOrderId: 'SO-1042',
metadata: { cart: [1, 2, 3] },
expiresIn: 3600
})
});
const payment = await res.json();
{
"paymentId": "6a0000000000000000000001",
"depositAddress": "DemoDogeAddressDoNotSendFunds12345",
"amount": 1049.2885,
"feeAmount": 5.73845459,
"netAmount": 1043.55001323,
"expiresAt": "2026-07-14T00:56:16.246Z",
"quoteExpiresAt": "2026-07-14T00:56:16.246Z",
"invoiceUrl": "/i/exampleslug1",
"currency": "DOGE",
"status": "WAITING"
}
Payments
Create a crypto payment invoice
Create a crypto payment with the Liddie API. Quote a fiat amount, get a unique deposit address and hosted invoice URL. Idempotent via Idempotency-Key.
POST
/
api
/
v1
/
payments
curl -X POST https://api.liddie.io/api/v1/payments \
-H "Authorization: Bearer lid_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-20260713-0001" \
-d '{
"currency": "USDT_TRC20",
"amount": 100,
"merchantOrderId": "SO-1042",
"metadata": { "cart": [1, 2, 3] },
"expiresIn": 3600
}'
const res = await fetch('https://api.liddie.io/api/v1/payments', {
method: 'POST',
headers: {
'Authorization': 'Bearer lid_live_...',
'Content-Type': 'application/json',
'Idempotency-Key': 'order-20260713-0001'
},
body: JSON.stringify({
currency: 'USDT_TRC20',
amount: 100,
merchantOrderId: 'SO-1042',
metadata: { cart: [1, 2, 3] },
expiresIn: 3600
})
});
const payment = await res.json();
{
"paymentId": "6a0000000000000000000001",
"depositAddress": "DemoDogeAddressDoNotSendFunds12345",
"amount": 1049.2885,
"feeAmount": 5.73845459,
"netAmount": 1043.55001323,
"expiresAt": "2026-07-14T00:56:16.246Z",
"quoteExpiresAt": "2026-07-14T00:56:16.246Z",
"invoiceUrl": "/i/exampleslug1",
"currency": "DOGE",
"status": "WAITING"
}
Tell Liddie the fiat amount you want to charge. It quotes that into crypto at the current rate, derives a unique deposit address, and hands you the hosted invoice URL.
curl -X POST https://api.liddie.io/api/v1/payments \
-H "Authorization: Bearer lid_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-20260713-0001" \
-d '{
"currency": "USDT_TRC20",
"amount": 100,
"merchantOrderId": "SO-1042",
"metadata": { "cart": [1, 2, 3] },
"expiresIn": 3600
}'
const res = await fetch('https://api.liddie.io/api/v1/payments', {
method: 'POST',
headers: {
'Authorization': 'Bearer lid_live_...',
'Content-Type': 'application/json',
'Idempotency-Key': 'order-20260713-0001'
},
body: JSON.stringify({
currency: 'USDT_TRC20',
amount: 100,
merchantOrderId: 'SO-1042',
metadata: { cart: [1, 2, 3] },
expiresIn: 3600
})
});
const payment = await res.json();
{
"paymentId": "6a0000000000000000000001",
"depositAddress": "DemoDogeAddressDoNotSendFunds12345",
"amount": 1049.2885,
"feeAmount": 5.73845459,
"netAmount": 1043.55001323,
"expiresAt": "2026-07-14T00:56:16.246Z",
"quoteExpiresAt": "2026-07-14T00:56:16.246Z",
"invoiceUrl": "/i/exampleslug1",
"currency": "DOGE",
"status": "WAITING"
}
When you omit
currency (the AWAITING_CURRENCY path), the 201 body is only { "paymentId", "amount", "invoiceUrl", "status": "AWAITING_CURRENCY" } — and here amount is the fiat value echoed back, not a crypto quantity. There is no depositAddress, feeAmount/netAmount, expiresAt/quoteExpiresAt, or currency yet: those appear only once the payer (or Select pay currency) locks a currency. The fields documented below describe the currency-set response.Always send an
Idempotency-Key — a UUID per order. If your request times out or your process crashes mid-flight, retrying with the same key returns the same paymentId instead of creating a duplicate invoice.Authorization
- API-key scope:
payments:create· JWT team permission:payments:manage - Your merchant account must be active.
Headers
string
Recommended: a UUID per order. 1–255 printable ASCII characters, no spaces. A second POST with the same key returns the same
paymentId instead of creating a duplicate. Takes precedence over the idempotencyKey body field.Body
number
required
Must be greater than 0. This is a fiat amount (your display currency, USD by default) — not a crypto amount. “Charge $75, payable in DOGE.” The response’s
amount is the crypto quantity the payer must send, quoted at the current rate — e.g. {"currency":"DOGE","amount":75} returns amount: 1049.2885 DOGE with fiatAmount: 75, fiatCurrency: "USD" on the detail.string
Ticker the payer will pay in, e.g.
BTC, USDT_TRC20. Omit it and the payer chooses on the invoice page (status AWAITING_CURRENCY). Case-insensitive — upper-cased server-side.string
Your own order reference. Max 256 characters.
any JSON
Max 8 KB. Opaque passthrough, echoed verbatim in webhooks/IPN. See the Webhooks & IPN guide.
string (URI)
Server-to-server IPN target. Max 2048 characters. HTTPS required in production; internal/private addresses are rejected; must match your callback-domain allowlist if you configured one.
string (URI)
Where the hosted invoice redirects the payer after payment. Max 2048 characters. Same URL rules as
ipnCallbackUrl.string (URI)
Redirect target for a terminal non-success outcome (expired / failed / cancelled). Max 2048 characters, same URL rules as
ipnCallbackUrl. Falls back to invoiceCallbackUrl when omitted — set it to split success and cancel redirects.number (seconds)
Invoice lifetime, 60–172800. Values outside that range are rejected with
400 (not clamped) — e.g. 200000 fails validation rather than yielding a 48 h invoice. Omit for the platform default.
Unknown body fields are silently dropped, not rejected. The schema is
additionalProperties: false, but the validator is configured to strip rather than refuse, so a misspelt field (amoutn for amount) does not answer 400 — the request succeeds and the platform default applies to the field you meant to set. Assert on the fields in the response rather than assuming your body arrived intact. This applies to every endpoint on this API.
Response fields
string
24-hex payment id. Use it for Get a payment, cancel, refunds, etc.
string
Unique to this payment. HD-derived: a fresh address is generated from your wallet for every payment — addresses are never reused.
number
The crypto quantity the payer must send, in crypto units of
currency. Your fiat input is echoed on the payment detail as fiatAmount/fiatCurrency.number
Platform commission + network-fee provision, in crypto units. Fee rounds up. With
feeBearer: "customer" the fee is added on top of your price instead (the payer covers it — see grossFiat on the payment detail).number
What you will be credited, in crypto units. Net rounds down.
string (ISO date)
Invoice expiry instant.
string (ISO date)
The crypto quote is only guaranteed until this instant; the hosted invoice refreshes it automatically.
string
The hosted checkout path, not a full URL. Prepend the checkout host to send a payer there:
https://dash.liddie.io/i/exampleslug1 — the checkout is served from dash.liddie.io, not from the API host. Its last segment (exampleslug1) is the public invoiceId for GET /getInvoiceStatus.string
The ticker the payer pays in.
string
Initial lifecycle status —
WAITING, or AWAITING_CURRENCY if currency was omitted. See the Payments overview.Errors
The ones you’ll hit most often:| Status | Body | Why |
|---|---|---|
| 400 | {"ok":false,"error":{"code":"VALIDATION_ERROR","message":"body/amount must be > 0"}} | amount ≤ 0 (rejected at the schema, before the handler) |
| 400 | {"error":"Payment creation failed"} | generic — see the full table below |
| 401/403 | see the Authentication guide | auth/scope problems |
Schema-level failures (bad types, out-of-range values like
amount ≤ 0 or expiresIn out of range) use the {"ok":false,"error":{"code":"VALIDATION_ERROR","message":…}} envelope. Handler-emitted business errors use the flat {"error":"…"} shape shown in the full table below.All error responses
All error responses
| Status | Body | Why |
|---|---|---|
| 400 | {"ok":false,"error":{"code":"VALIDATION_ERROR","message":"body/amount must be > 0"}} | amount ≤ 0 (schema-level) |
| 400 | {"error":"Invalid Idempotency-Key (reserved namespace)"} | a key matching the reserved widget:<24hex>:<token> shape |
| 400 | {"error":"Callback URLs must use HTTPS in production"} | http:// IPN/invoice URL |
| 400 | {"error":"Callback URLs must not point to internal addresses"} | internal/private callback URL |
| 400 | {"error":"metadata exceeds 8192 bytes"} | metadata too big |
| 400 | {"error":"Invalid Idempotency-Key (1-255 printable ASCII characters, no spaces)"} | bad key format |
| 400 | {"error":"Payment creation failed"} | generic: unknown currency, no wallet for it, pending-payments quota reached, amount below the currency minimum, callback host not allowlisted… (details are intentionally not disclosed) |
| 400 | {"error":"No merchant context"} | super-admin JWT (no merchant) called a merchant route |
| 401/403 | see the Authentication guide | auth/scope problems |
See also
- Get a payment: fetch the full detail and fee breakdown once the payer starts paying.
- Webhooks & IPN: get lifecycle transitions pushed to your server instead of polling.
- Cancel a payment: void the invoice while it is still waiting for funds.