API docs
Create a Litecoin or Ethereum payment from your server, send your customer to pay, and get told when it's done. Everything is JSON over HTTPS.
Getting started
- Create an account and make a test key on the API keys page.
- Create a payment with the key (below) and open its
checkout_url. - Pay it with testnet LTC or Sepolia ETH and watch the status change.
- Add a webhook endpoint so your shop is told when payments complete.
All requests go to https://staging.interpay.one/api/v1.
Authentication
Send your secret key in the Authorization header. Keep it on your server; never put it in a web page or app.
Authorization: Bearer ip_test_…Keys starting with ip_test_ work in test mode: payments use Litecoin testnet coins and Sepolia ETH, which have no value. Keys starting with ip_live_ take real LTC and ETH and become available once your business is verified. A test key only ever sees test payments, and a live key only live ones.
Check a key with:
curl https://staging.interpay.one/api/v1/merchant \
-H "Authorization: Bearer ip_test_…"
{"id": "cmu…", "name": "Northwind Coffee", "mode": "test", "verification_status": "not_started"}Create a payment
POST /api/v1/invoices
With crypto, prices the amount in that coin at the current rate, holds the price for your payment window (15 minutes unless changed in settings) and assigns an address used for this payment only. Without it, the customer picks LTC or ETH on the checkout page, and the rate and address are locked then; until that happens crypto, address and the crypto amounts are null.
| Field | Type | Description |
|---|---|---|
amount | string | Required. Amount in the currency, with at most 2 decimals, e.g. "42.50". Between 1.00 and 1,000,000.00. |
currency | string | Required. EUR or USD. |
crypto | string | Optional. LTC or ETH, if you've agreed on a coin with your customer. ETH needs at least 5.00. Leave it out to let the customer choose. |
order_id | string | Optional. Your reference, up to 200 characters. |
description | string | Optional. Shown to the customer on the checkout page. |
redirect_url | string | Optional. http(s) URL; the checkout page offers a button back to it once paid. |
metadata | object | Optional. Up to 20 keys with string, number, boolean or null values. Returned as-is. |
curl https://staging.interpay.one/api/v1/invoices \
-H "Authorization: Bearer ip_test_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042" \
-d '{"amount": "42.00", "currency": "EUR", "order_id": "1042",
"redirect_url": "https://shop.example/thanks"}'Returns 201 with the payment object.
Retrieve a payment
GET /api/v1/invoices/:id
Returns the payment object, or 404 if it doesn't exist in this key's mode.
List payments
GET /api/v1/invoices
| Field | Type | Description |
|---|---|---|
limit | number | 1 to 100, default 20. |
starting_after | string | A payment id; returns the payments created before it. Use the last id of the previous page. |
status | string | Only payments with this status. |
crypto | string | Only payments in this coin (LTC or ETH). |
{"object": "list", "data": [ …payment objects, newest first… ], "has_more": true}Cancel a payment
POST /api/v1/invoices/:id/cancel
Cancels a payment that's still new with nothing sent to it, for example when the customer abandons the order. Returns the payment object with status canceled, or 409 invoice_not_cancelable once money has been seen.
The payment object
{
"id": "cmuss7faf000c7bzek7tv79v5",
"object": "invoice",
"mode": "test",
"status": "confirming",
"amount": "42.00",
"currency": "EUR",
"crypto": "LTC",
"amount_crypto": "0.68504323",
"received_crypto": "0.68504323",
"due_crypto": "0",
"address": "tltc1quvwe5a06lwlup798r9v03nd4jg27qjyvurc7u0",
"payment_uri": "litecoin:tltc1quvwe…?amount=0.68504323",
"checkout_url": "https://staging.interpay.one/pay/cmuss7faf000c7bzek7tv79v5",
"required_confirmations": 1,
"rate": {"price": "61.31", "currency": "EUR", "source": "kraken", "fetched_at": "…"},
"order_id": "1042",
"description": null,
"redirect_url": "https://shop.example/thanks",
"metadata": {},
"payments": [
{"txid": "b34348…", "vout": 0, "amount_crypto": "0.68504323",
"confirmations": 0, "late": false, "resolution": null, "seen_at": "…"}
],
"expires_at": "2026-10-03T19:40:34.000Z",
"paid_at": null,
"canceled_at": null,
"created_at": "2026-10-03T19:25:34.000Z"
}| Field | Type | Description |
|---|---|---|
crypto | string | LTC or ETH; null until the customer picks a coin. |
amount_crypto | string | What the customer must send, in crypto. Exact decimal (up to 8 places for LTC, 9 for ETH); never parse it as a float. |
received_crypto | string | Sum of on-time payments to the address. |
due_crypto | string | What is still missing; 0 once enough has arrived. |
payment_uri | string | Wallet link for QR codes: litecoin:<address>?amount=… or, for ETH, ethereum:<address>@<chain id>?value=<wei> (EIP-681). Chain id 1 is mainnet, 11155111 Sepolia. |
required_confirmations | number | Set in your dashboard settings. Defaults: LTC 1 under 50.00 and 3 otherwise (a block every ~2.5 minutes); ETH 12 and 64 (a block every ~12 seconds; 64 is final). |
payments | array | Every transaction output paid to the address. late is true for payments that arrived after the payment closed; they never count toward it. resolution is credited or refunded once you've decided what happens to underpaid or late money, otherwise null. |
expires_at | string | End of the payment window (15 minutes unless changed in your settings). Payments first seen after it are late. |
Statuses
- Awaiting payment
new - Created; nothing sent yet.
- Confirming
confirming - A payment is on the network but not confirmed enough yet, or only part of the amount has arrived and the customer can still send the rest.
- Paid
paid - At least 99.5% of the amount arrived on time and is confirmed. Ship the order. Your balance is credited with what was received, minus 1%.
- Underpaid
underpaid - The window closed with less than 99.5% received and confirmed. Not credited automatically: in the dashboard, credit what arrived to your balance or refund it to the customer.
- Expired
expired - The window closed with nothing received.
- Canceled
canceled - You canceled it before anything was sent.
paid, underpaid, expired and canceled are final. Anything sent to the address afterwards is recorded as a late payment and reported with invoice.late_payment; you decide in the dashboard whether to credit or refund it.
Checkout page
The simplest integration is to redirect the customer to checkout_url. It shows the amount, a QR code, a button that opens their wallet and live progress, and offers a link back to your redirect_url once paid. Don't treat that redirect as proof of payment: rely on the webhook or retrieve the payment.
If you didn't set crypto, the page first asks the customer to pick a coin. To show the payment in your own page instead, create it with crypto and use address, amount_crypto and payment_uri (for QR codes and wallet links).
Ethereum payments are detected when ETH is sent directly to the address. Some exchanges send from smart contracts, which can arrive without being matched to the payment; and tokens such as USDT sent to an ETH payment address are not detected at all.
Stores and products
If you sell through an Interpay storefront, the API reads your catalog. Products are the same in test and live mode; a key sees the products of every store of the business.
GET /api/v1/stores
GET /api/v1/products ?store_id=&status=active&type=digital&limit=&starting_after=
GET /api/v1/products/:id{
"id": "cmv1p…",
"object": "product",
"store_id": "cmv0s…",
"type": "digital",
"status": "active",
"name": "Field Recorder Presets",
"slug": "field-recorder-presets",
"short_description": "48 presets for ambient recordings",
"options": [{"name": "License", "values": ["Personal", "Studio"]}],
"images": [{"id": "cmv1i…", "url": "https://staging.interpay.one/api/media/cmv1f…/image", "alt": null}],
"variants": [
{"id": "cmv1v…", "title": "Personal", "options": ["Personal"], "price": "19.00",
"compare_at_price": null, "sku": "FRP-P", "inventory_quantity": null}
],
"collection_ids": ["cmv1c…"],
"metadata": {},
"created_at": "…",
"updated_at": "…"
}inventory_quantity is null when the product doesn't count stock. Prices are in the store's currency, which GET /api/v1/stores returns along with each store's address and whether its checkout takes test or live payments (checkout_mode).
Orders
Orders come from your storefront and payment links. Each one opens an Interpay payment, so the payment endpoints and webhooks above work for them too; the order adds what was bought, by whom and what happened next. Like payments, orders belong to a mode: a test key sees test orders only.
GET /api/v1/orders ?store_id=&status=&payment_status=&fulfillment_status=&email=&limit=&starting_after=
GET /api/v1/orders/:id
POST /api/v1/orders/:id/fulfill {"tracking_number": "1Z999…", "tracking_url": "https://…"}fulfill marks a paid order as fulfilled and completes it, with optional tracking the customer sees on their order page. Digital items are delivered automatically once an order is paid, so you only need it for physical products and services. It answers 409 invalid_state for orders that aren't paid or are already closed.
| Status | Field | Description |
|---|---|---|
pending | status | Placed; waiting for the payment. |
paid | status | Paid in full. Digital items are delivered; anything else waits for you. |
processing | status | You started working on it. |
completed | status | Fulfilled. Final, unless refunded. |
canceled | status | Given up or canceled; stock went back. |
refunded | status | Money sent back to the customer. |
underpaid, overpaid | payment_status | The customer sent too little or too much. Underpaid orders wait for your decision in the dashboard; overpaid ones too if your checkout settings say so. |
The order object
{
"id": "cmv2o…",
"object": "order",
"mode": "test",
"number": 1042,
"store": {"id": "cmv0s…", "slug": "northwind", "name": "Northwind Coffee"},
"source": "storefront",
"status": "paid",
"payment_status": "paid",
"fulfillment_status": "unfulfilled",
"email": "[email protected]",
"customer_id": "cmv2c…",
"customer_name": "Maya R.",
"currency": "EUR",
"subtotal": "38.00",
"discount": "3.80",
"discount_code": "WELCOME10",
"shipping": "4.90",
"tax": "0.00",
"tax_included": false,
"total": "39.10",
"crypto": "LTC",
"overpaid_crypto": null,
"invoice_id": "cmv2i…",
"payment_link_id": null,
"items": [
{"id": "cmv2l…", "product_id": "cmv1p…", "variant_id": "cmv1v…", "type": "physical",
"name": "House Blend", "variant": "1 kg", "sku": "HB-1000", "quantity": 2,
"unit_price": "19.00", "discount": "3.80", "total": "34.20"}
],
"shipping_address": {"name": "Maya R.", "line1": "…", "city": "…", "postalCode": "…", "country": "DE"},
"customer_note": null,
"tracking_number": null,
"tracking_url": null,
"created_at": "…",
"paid_at": "…",
"fulfilled_at": null,
"completed_at": null,
"canceled_at": null,
"refunded_at": null
}Amounts are decimal strings in the order's currency. The link to the customer's order page is never part of the API: it unlocks their downloads.
Webhooks
Add endpoints on the Webhooks page or with the API. Each one gets a signing secret, shown only when it's created.
POST /api/v1/webhook_endpoints {"url": "https://shop.example/hooks/interpay"}
GET /api/v1/webhook_endpoints
DELETE /api/v1/webhook_endpoints/:idLive endpoints must use https. Test endpoints may also use http, so you can point them at your own machine while developing.
| Event | Description | |
|---|---|---|
invoice.confirming | A payment was first seen for a new payment. | |
invoice.paid | The payment is complete. This is the one to fulfil orders on. | |
invoice.underpaid | Closed with too little received. | |
invoice.expired | Closed with nothing received. | |
invoice.late_payment | Money arrived after the payment closed. | |
invoice.canceled | You canceled the payment. | |
invoice.payment_credited | Underpaid or late money was credited to your balance. | |
invoice.payment_refunded | Underpaid or late money is being sent back to the customer. | |
order.created | A customer placed an order in your storefront or through a payment link. data is the order object. | |
order.paid | The order's payment is complete. | |
order.fulfilled | You marked the order fulfilled. | |
order.completed | The order is done. Sent right after order.fulfilled; digital orders get both once they're delivered. | |
order.canceled | The order was canceled or its payment given up. | |
order.refunded | Money for the order was sent back. | |
test.ping | Sent by the Send test event button in the dashboard. data holds only a message. |
{
"id": "evt_4f1c…",
"type": "invoice.paid",
"created_at": "2026-10-03T19:31:02.000Z",
"mode": "test",
"data": { …the payment object… }
}Respond with any 2xx status within 10 seconds. Otherwise Interpay retries after 1 minute, 5 minutes, 30 minutes, 2, 6, 12 and 24 hours. The same event can arrive more than once, so use its id or the payment status to ignore repeats.
Verifying webhooks
Every request carries an Interpay-Signature header: t=<unix time>,v1=<hex>. v1 is the HMAC-SHA256 of <t>.<raw body> with your endpoint secret. Check it against the raw body, before parsing, and reject old timestamps.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyInterpay(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
const t = Number(parts.t);
if (!parts.v1 || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1, "hex");
return given.length === expected.length && timingSafeEqual(expected, given);
}Safe retries
Send an Idempotency-Key header (up to 200 characters, such as your order number) when creating a payment. If the request times out and you send it again with the same key, you get the payment from the first request instead of a second one.
Errors
Errors use a 4xx or 5xx status and the same shape:
{"error": {"code": "invalid_amount", "message": "amount must be a decimal with at most 2 places, e.g. \"42.50\"."}}| Code | Status | Description |
|---|---|---|
invalid_api_key | 401 | Missing, unknown or revoked key. |
invalid_json | 400 | The body isn't valid JSON. |
invalid_request | 400 | A field is missing or has the wrong type; the message names it. |
invalid_amount | 400 | amount isn't a decimal with at most 2 places. |
amount_out_of_range | 400 | amount is below 1.00 or above 1,000,000.00. |
invalid_url | 400 | Webhook URL isn't allowed (see Webhooks). |
too_many_endpoints | 400 | Up to 10 webhook endpoints per mode. |
not_found | 404 | No such object in this key's mode. |
invoice_not_cancelable | 409 | The payment isn't new any more, or money was already seen. |
rate_unavailable | 503 | The exchange rate couldn't be fetched. Retry shortly with the same Idempotency-Key. |
crypto_unavailable | 400 | That coin can't be used: not accepted in your settings, not set up, or the amount is below its minimum. |
rate_limited | 429 | Over 300 requests a minute for this key. Wait for the Retry-After seconds. |
mode_unavailable | 503 | This mode isn't set up on the server. |
Storefront CSS
In the storefront editor, under Theme › Custom CSS, you can add your own styles. They load on your storefront pages only: never on checkout, order pages or the Interpay dashboard, so the amount, address, QR code and time left always look the way customers expect. Write every selector inside .store; the class names, variables and attributes below are kept stable for that purpose.
.store .store-hero-heading {
letter-spacing: -0.03em;
}
.store[data-scheme="dark"] .store-product-name {
font-weight: 500;
}
.store #section-s4k2m9q1x .store-button {
background: var(--s-accent);
color: var(--s-on-accent);
}Class names
| Selector | Description | |
|---|---|---|
.store | The whole storefront. Carries the theme variables and layout attributes below. | |
.store-announcement | Announcement bar | |
.store-header | Header; .store-logo, .store-nav-link and .store-search inside it | |
.store-section | Every section; .store-section-hero, .store-section-faq… for one kind | |
#section-<id> | One particular section. Its id is shown under the section's settings. | |
.store-section-head | A section's heading block: .store-eyebrow, .store-heading, .store-text | |
.store-hero | Hero: .store-hero-heading, .store-hero-text, .store-hero-media | |
.store-product-grid | Product grids | |
.store-product | A product card: .store-product-name, .store-price, .store-badge | |
.store-product-view | Product page: .store-gallery, .store-buy-box, .store-add-to-cart, .store-buy-now | |
.store-collection | A collection tile | |
.store-features | Highlights: .store-feature, .store-feature-icon, .store-feature-title | |
.store-testimonial | A testimonial: .store-testimonial-author, .store-testimonial-role | |
.store-faq-item | One FAQ question and answer | |
.store-prose | Formatted text from rich text settings and pages | |
.store-button | Buttons; .store-link for text links | |
.store-cart-drawer | The cart that slides in; .store-cart for the cart page | |
.store-footer | Footer: .store-footer-menu, .store-footer-contact, .store-footer-legal |
Theme variables
Set on .store from your theme's colors and fonts. Use them to stay in step when you change the palette.
| Variable | Description | |
|---|---|---|
--s-bg | Page background | |
--s-surface | Cards, panels, fields | |
--s-surface-2 | A step darker or lighter than the surface | |
--s-text | Text | |
--s-muted | Muted text | |
--s-border | Borders; --s-border-strong for a firmer line | |
--s-primary | Primary; --s-primary-hover, --s-on-primary for text on it | |
--s-secondary | Secondary; --s-on-secondary for text on it | |
--s-accent | Accent; --s-on-accent for text on it | |
--s-success | Success | |
--s-font-heading | Heading font stack | |
--s-font-body | Body font stack | |
--s-radius | Corner radius; --s-radius-button, --s-radius-image | |
--s-max | Content width | |
--s-gutter | Space at the page edges | |
--s-gap | Space between sections |
Layout attributes
Your layout choices, as attributes on .store, e.g. .store[data-buttons="pill"].
| Attribute | Description | |
|---|---|---|
data-theme | linen, market, noir, spotlight, vault | |
data-scheme | light, dark (from the background color) | |
data-header | inline, centered, split | |
data-cards | plain, framed, overlay | |
data-ratio | square, portrait, landscape | |
data-buttons | solid, outline, pill | |
data-density | comfortable, compact | |
data-product-layout | split, stacked, wide |
Each section's own id is shown under its settings in the editor. Up to 50,000 characters. Anything in it that could end the style element is neutralized, and CSS can't run scripts.