interpay

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

  1. Create an account and make a test key on the API keys page.
  2. Create a payment with the key (below) and open its checkout_url.
  3. Pay it with testnet LTC or Sepolia ETH and watch the status change.
  4. 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.

FieldTypeDescription
amountstringRequired. Amount in the currency, with at most 2 decimals, e.g. "42.50". Between 1.00 and 1,000,000.00.
currencystringRequired. EUR or USD.
cryptostringOptional. 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_idstringOptional. Your reference, up to 200 characters.
descriptionstringOptional. Shown to the customer on the checkout page.
redirect_urlstringOptional. http(s) URL; the checkout page offers a button back to it once paid.
metadataobjectOptional. 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

FieldTypeDescription
limitnumber1 to 100, default 20.
starting_afterstringA payment id; returns the payments created before it. Use the last id of the previous page.
statusstringOnly payments with this status.
cryptostringOnly 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"
}
FieldTypeDescription
cryptostringLTC or ETH; null until the customer picks a coin.
amount_cryptostringWhat the customer must send, in crypto. Exact decimal (up to 8 places for LTC, 9 for ETH); never parse it as a float.
received_cryptostringSum of on-time payments to the address.
due_cryptostringWhat is still missing; 0 once enough has arrived.
payment_uristringWallet 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_confirmationsnumberSet 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).
paymentsarrayEvery 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_atstringEnd 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.

StatusFieldDescription
pendingstatusPlaced; waiting for the payment.
paidstatusPaid in full. Digital items are delivered; anything else waits for you.
processingstatusYou started working on it.
completedstatusFulfilled. Final, unless refunded.
canceledstatusGiven up or canceled; stock went back.
refundedstatusMoney sent back to the customer.
underpaid, overpaidpayment_statusThe 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/:id

Live endpoints must use https. Test endpoints may also use http, so you can point them at your own machine while developing.

EventDescription
invoice.confirmingA payment was first seen for a new payment.
invoice.paidThe payment is complete. This is the one to fulfil orders on.
invoice.underpaidClosed with too little received.
invoice.expiredClosed with nothing received.
invoice.late_paymentMoney arrived after the payment closed.
invoice.canceledYou canceled the payment.
invoice.payment_creditedUnderpaid or late money was credited to your balance.
invoice.payment_refundedUnderpaid or late money is being sent back to the customer.
order.createdA customer placed an order in your storefront or through a payment link. data is the order object.
order.paidThe order's payment is complete.
order.fulfilledYou marked the order fulfilled.
order.completedThe order is done. Sent right after order.fulfilled; digital orders get both once they're delivered.
order.canceledThe order was canceled or its payment given up.
order.refundedMoney for the order was sent back.
test.pingSent 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\"."}}
CodeStatusDescription
invalid_api_key401Missing, unknown or revoked key.
invalid_json400The body isn't valid JSON.
invalid_request400A field is missing or has the wrong type; the message names it.
invalid_amount400amount isn't a decimal with at most 2 places.
amount_out_of_range400amount is below 1.00 or above 1,000,000.00.
invalid_url400Webhook URL isn't allowed (see Webhooks).
too_many_endpoints400Up to 10 webhook endpoints per mode.
not_found404No such object in this key's mode.
invoice_not_cancelable409The payment isn't new any more, or money was already seen.
rate_unavailable503The exchange rate couldn't be fetched. Retry shortly with the same Idempotency-Key.
crypto_unavailable400That coin can't be used: not accepted in your settings, not set up, or the amount is below its minimum.
rate_limited429Over 300 requests a minute for this key. Wait for the Retry-After seconds.
mode_unavailable503This 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

SelectorDescription
.storeThe whole storefront. Carries the theme variables and layout attributes below.
.store-announcementAnnouncement bar
.store-headerHeader; .store-logo, .store-nav-link and .store-search inside it
.store-sectionEvery 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-headA section's heading block: .store-eyebrow, .store-heading, .store-text
.store-heroHero: .store-hero-heading, .store-hero-text, .store-hero-media
.store-product-gridProduct grids
.store-productA product card: .store-product-name, .store-price, .store-badge
.store-product-viewProduct page: .store-gallery, .store-buy-box, .store-add-to-cart, .store-buy-now
.store-collectionA collection tile
.store-featuresHighlights: .store-feature, .store-feature-icon, .store-feature-title
.store-testimonialA testimonial: .store-testimonial-author, .store-testimonial-role
.store-faq-itemOne FAQ question and answer
.store-proseFormatted text from rich text settings and pages
.store-buttonButtons; .store-link for text links
.store-cart-drawerThe cart that slides in; .store-cart for the cart page
.store-footerFooter: .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.

VariableDescription
--s-bgPage background
--s-surfaceCards, panels, fields
--s-surface-2A step darker or lighter than the surface
--s-textText
--s-mutedMuted text
--s-borderBorders; --s-border-strong for a firmer line
--s-primaryPrimary; --s-primary-hover, --s-on-primary for text on it
--s-secondarySecondary; --s-on-secondary for text on it
--s-accentAccent; --s-on-accent for text on it
--s-successSuccess
--s-font-headingHeading font stack
--s-font-bodyBody font stack
--s-radiusCorner radius; --s-radius-button, --s-radius-image
--s-maxContent width
--s-gutterSpace at the page edges
--s-gapSpace between sections

Layout attributes

Your layout choices, as attributes on .store, e.g. .store[data-buttons="pill"].

AttributeDescription
data-themelinen, market, noir, spotlight, vault
data-schemelight, dark (from the background color)
data-headerinline, centered, split
data-cardsplain, framed, overlay
data-ratiosquare, portrait, landscape
data-buttonssolid, outline, pill
data-densitycomfortable, compact
data-product-layoutsplit, 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.