The InstantPay API

One API key, one invoice, one webhook. Create a payment request in EUR or USD, send the customer to the hosted checkout, get a signed webhook when it is paid. This page is generated from the OpenAPI 3 schema at /openapi.json.

pay.instantnode.eu/v1 API key auth JSON in, JSON out OpenAPI 3.0.3
20
Endpoints
8
Resource groups
97
Coins accepted
REST / JSON
Over HTTPS

Get started

1

Create an API key

In the dashboard under API keys. Keys look like ik_… and are shown once; use a test key while you build.

2

Send it with every request

As X-API-Key or Authorization: Bearer. Both work the same.

3

Create an invoice, redirect the customer

The response carries a checkoutUrl. When the customer pays, your webhook receives invoice.confirmed.

Good to know

Errors are { error, message }. Add an Idempotency-Key header to POST /invoices so a retried request never makes two invoices.

bash
$ curl -X POST https://pay.instantnode.eu/v1/invoices \
     -H "X-API-Key: ik_…" -H "Content-Type: application/json" \
     -d '{"amount":"19.99","currency":"EUR","orderId":"order-1042"}'

# 201 Created
{
  "id": "3f9c1b2e-…",
  "status": "created",
  "paid": false,
  "amount": "19.99",
  "currency": "EUR",
  "checkoutUrl": "https://pay.instantnode.eu/pay/57rW…"
}

Guide

Overview

InstantPay lets your website take crypto payments without running a wallet yourself. Your customer pays in SOL, USDC, USDT, ETH, BNB or BTC on a checkout page hosted by us; you receive a signed webhook and a credit in EUR or USD on your InstantPay balance. Every confirmed payment costs a flat 1.66 % of the invoice amount. You pay yourself out to your own wallet whenever you like, in SOL, a stablecoin, ETH, BNB, POL, TRX, BTC or LTC.

A payment goes like this:

  1. Your server creates an invoice with a price in EUR or USD and gets back a checkout URL.
  2. The customer pays on the checkout page (or in an overlay on your site): picks a coin, sees the address and the exact amount, sends it. The rate is locked at that moment.
  3. InstantPay watches the chain. As soon as the payment is final, your webhook URL receives invoice.confirmed.
  4. You deliver. The invoice amount minus the fee is on your balance; request a payout in the coin of your choice whenever you like.

What you need: an InstantNode account, a server that can make HTTPS calls (Node, PHP, anything else works) and a public https:// URL that receives webhooks. You never handle coins, addresses or exchange rates yourself.

Try the whole flow first: our demo shop at http://5.230.154.241 is an ordinary store wired to InstantPay in test mode. Buy something, pay with the "Simulate payment" button and watch the order flip to paid.

Getting started

Six steps from nothing to a live integration. Steps 1 to 5 need no approval; you can build and test while we review your account.

  1. Sign in. Open pay.instantnode.eu/dashboard and sign in with your InstantNode account. A merchant account is created for you in the state pending.
  2. Complete your account. Under Account, fill in company, website and a short description of what you sell, accept the terms and submit. InstantNode reviews it and activates you, usually within a day.
  3. Copy your test key. Under API keys you already have a live and a test pair: a secret key (sk_test_…, for your server) and a public key (pk_test_…, for the browser). Reveal the test secret key and put it in your server configuration, never in browser code.
  4. Add a webhook endpoint. Under Webhooks, add the https:// address on your server that should receive events, then press Send test event on the endpoint page to check that your signature verification works.
  5. Make a test purchase. Create an invoice with the test key (see below), open the checkout URL, press Simulate payment. A few seconds later your webhook receives invoice.confirmed with livemode: false.
  6. Go live. Once your account is active, switch the dashboard to live mode, reveal the live secret key (shown once) and swap it for the test key; add a live webhook endpoint. Invoices from a live key are paid with real coins and credited to your balance.

Ways to integrate

All ways use the same API; the only difference is where the customer pays. One rule holds for all of them: the invoice is always created on your server, never in the browser, otherwise your API key is public.

Hosted checkout

The simplest way. Your server creates the invoice and redirects the customer to checkoutUrl. After the payment the customer comes back to your redirectUrl. Works in every shop, needs no JavaScript.

js
// Node (Express) - the SDK is a single file: /sdk/node/instapay.mjs
import { InstantPay } from './instapay.mjs';
const pay = new InstantPay({ baseUrl: 'https://pay.instantnode.eu', apiKey: process.env.INSTAPAY_KEY });

app.post('/checkout', async (req, res) => {
  const invoice = await pay.createInvoice({
    amount: '19.99',
    currency: 'EUR',
    orderId: order.id,
    description: 'T-shirt, blue, M',
    redirectUrl: 'https://shop.example/thanks?order=' + order.id,
    cancelUrl: 'https://shop.example/cart',
  }, order.id);                 // idempotency key: a retry returns the same invoice
  res.redirect(invoice.checkoutUrl);
});
php
// PHP - /sdk/php/InstaPay.php
require 'InstaPay.php';
$pay = new InstantPay('https://pay.instantnode.eu', getenv('INSTAPAY_KEY'));
$invoice = $pay->createInvoice([
  'amount' => '19.99', 'currency' => 'EUR', 'orderId' => (string) $orderId,
  'redirectUrl' => 'https://shop.example/thanks?order=' . $orderId,
], (string) $orderId);
header('Location: ' . $invoice['checkoutUrl']);

No code at all. Under Payment links, create a link with a fixed amount or an open amount (the customer types it, within the bounds you set), reusable or single use, with an optional expiry and success URL. The link is a page at https://pay.instantnode.eu/l/<slug> that creates an invoice for whoever opens it; you get the URL, a QR code and an embed snippet. Every invoice from a link carries paymentLinkId and the order id link:<slug>:<n>, so the webhook and the credit work exactly the same. The link page counts uses and revenue and lists its payments.

Overlay widget

The customer stays on your page; the checkout opens in an overlay. Load instapay.js, then open it with the token, which is the last part of checkoutUrl and comes from your server. If you would rather not pass the token through your page, the browser can fetch it with your public key: POST /v1/public/checkout-sessions with { "invoiceId" } answers { token, checkoutUrl } for an invoice your server created; a public key can do nothing else.

html
<script src="https://pay.instantnode.eu/instapay.js"></script>
<script>
  InstantPay.open({
    token: token,                         // last segment of checkoutUrl
    lang: 'en',                           // or 'de'
    onStatus:  (s) => console.log(s.status),
    onPaid:    (s) => location.href = '/thanks',
    onExpired: (s) => alert('The payment window has expired'),
    onClose:   ()  => console.log('closed'),
    closeOnPaid: true,
  });
</script>

The widget polls the status every four seconds and calls onPaid as soon as the payment is confirmed. Still treat that as a courtesy for the customer: only the webhook decides whether an order is paid.

Your own payment page

Create the invoice, call POST /v1/invoices/:id/select with the coin the customer chose, and show depositAddress and amountExpected yourself. Poll GET /v1/invoices/:id or wait for the webhook.

Creating an invoice

POST /v1/invoices with your key in the X-API-Key header. Amounts travel as strings; never do money maths with floating point numbers.

bash
curl -X POST https://pay.instantnode.eu/v1/invoices \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1001" \
  -d '{
    "amount": "19.99",
    "currency": "EUR",
    "orderId": "ORD-1001",
    "description": "T-shirt, blue, M",
    "redirectUrl": "https://shop.example/thanks?order=1001",
    "cancelUrl": "https://shop.example/cart",
    "metadata": {"sku": "shirt-blue-m"},
    "ttlSeconds": 1200
  }'
FieldRequiredMeaning
amountyesPrice as a string with at most two decimals, e.g. "19.99"
currencyyesEUR or USD
orderIdnoYour order number. Unique per account; the same orderId returns the same invoice
descriptionnoShown to the customer on the checkout
redirectUrl / cancelUrlnoWhere the customer goes after paying or cancelling
webhookUrlnoOverrides the default webhook URL from your settings for this invoice
metadatanoAny JSON object up to 4 KB; comes back in every webhook
assetnoPreselect the coin (e.g. USDC_SOL) and skip the picker on the checkout
ttlSecondsnoHow long the invoice stays payable: default 1200 (20 minutes), 60 to 86400
customerEmailnoThe customer gets a receipt mail once the payment is confirmed; also shown to you on the payment

The response is the invoice (201):

json
{
  "id": "f987fead-cbc0-47e1-b9c1-02423c18ec97",
  "orderId": "ORD-1001",
  "status": "created",
  "paid": false,
  "amount": "19.99",
  "currency": "EUR",
  "checkoutUrl": "https://pay.instantnode.eu/pay/Tx7CVPbpJk7IcTkdKMCKCCKt",
  "expiresAt": "2026-09-19T21:20:13.527Z",
  "asset": null, "amountExpected": null, "amountReceived": null, "depositAddress": null
}

Limits: 60 calls per minute per key (429), at most 1000 open invoices at a time (429 too_many_open_invoices). Sending the same Idempotency-Key with a different body answers 409. Fields you leave out come from your Checkout settings: time to pay (ttlSeconds), tolerance, success and cancel URL, and the coins offered on the checkout.

Webhooks

The webhook is the part that matters. The redirect after the payment is only cosmetic: the customer can close the browser and still send the coins, so only deliver goods after the event with paid: true has arrived.

Under Webhooks you add up to ten endpoints per mode (live and test are separate lists). Each endpoint has its own signing secret (whsec_…), a list of event types it subscribes to (* for all) and a health line. InstantPay sends a POST with a JSON body to every subscribed endpoint. The URL must be https:// with a public hostname; redirects are not followed and your server has 10 seconds to answer.

X-InstantPay-Signature: t=1758315600,v1=<hex hmac-sha256>
X-InstantPay-Event: invoice.confirmed
X-InstantPay-Livemode: true
X-InstantPay-Delivery: 42
Idempotency-Key: evt_01K5N3Y7Z2Q8XW6M3R9V4T1B5C

The same headers are also sent with the X-InstaPay- spelling from before, so an existing receiver keeps working.

Verifying the signature

The signature is an HMAC-SHA256 of "<t>.<raw body>" with the endpoint's secret (revealed on the endpoint page after a fresh sign-in). Verify the raw bytes you received, not a re-serialised object, and reject timestamps older than five minutes.

js
// Node (Express) - verifyWebhook comes from the SDK
import { verifyWebhook } from './instapay.mjs';

app.post('/webhooks/instantpay', express.raw({ type: '*/*' }), (req, res) => {
  const raw = req.body.toString('utf8');
  if (!verifyWebhook(process.env.INSTANTPAY_WEBHOOK_SECRET, req.get('x-instantpay-signature'), raw)) {
    return res.status(400).send('bad signature');
  }
  const event = JSON.parse(raw);
  if (event.livemode === false) return res.json({ ok: true });      // a test event: never deliver
  const invoice = event.data.object;
  if (event.type === 'invoice.confirmed' && invoice.paid) markOrderPaid(invoice.orderId);
  res.json({ ok: true });                                            // answer fast, work afterwards
});
php
// PHP
$raw = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_INSTANTPAY_SIGNATURE'] ?? '';
$event = InstantPay::parseWebhook(getenv('INSTANTPAY_WEBHOOK_SECRET'), $signature, $raw);
if ($event === null) { http_response_code(400); exit('bad signature'); }
if (($event['livemode'] ?? true) === false) { echo 'ok'; exit; }
$invoice = $event['data']['object'];
if ($event['type'] === 'invoice.confirmed' && $invoice['paid']) markOrderPaid($invoice['orderId']);
echo 'ok';

The event body

data.object is the object exactly as GET /v1/invoices/:id (or /v1/payouts/:id) returns it, so a webhook and a read never disagree.

json
{
  "id": "evt_01K5N3Y7Z2Q8XW6M3R9V4T1B5C",
  "object": "event",
  "type": "invoice.confirmed",
  "livemode": true,
  "created": "2026-09-19T21:05:00.000Z",
  "data": {
    "object": {
      "object": "invoice",
      "id": "f987fead-cbc0-47e1-b9c1-02423c18ec97",
      "orderId": "ORD-1001",
      "status": "confirmed",
      "paid": true,
      "livemode": true,
      "amount": "19.99",
      "currency": "EUR",
      "asset": "SOL",
      "amountExpected": "0.206789123",
      "amountReceived": "0.206789123",
      "metadata": {"sku": "shirt-blue-m"},
      "createdAt": "2026-09-19T21:00:13.527Z",
      "expiresAt": "2026-09-19T21:20:13.527Z",
      "confirmedAt": "2026-09-19T21:05:00.000Z"
    }
  }
}
EventWhen
invoice.createdThe invoice exists
invoice.detectedA transaction was seen but is not final yet
invoice.confirmedPaid. paid is true
invoice.overpaidPaid, more than needed; everything that arrived is credited. paid is true
invoice.underpaidToo little arrived; you can accept the partial payment in the dashboard
invoice.expiredNothing arrived before the deadline
invoice.paid_latePaid after the deadline; see Fees, balance and payouts
invoice.cancelled / invoice.failedCancelled, or something went wrong
deposit.confirmedMoney landed on a customer's static address (customers feature)
payout.paid / payout.failedA payout to your wallet finished; data.object is the payout
refund.paid / refund.failedA refund to a customer finished; data.object is the refund
account.approved / account.suspendedYour account changed state

An endpoint can also be set to the legacy body (event, livemode and invoice / deposit / payout at the top level, priceAmount instead of amount), which is what integrations from before endpoints existed receive; invoice.overpaid arrives there as invoice.confirmed with overpaid: true. New integrations should use the default.

Rules for a robust receiver

  • Answer 2xx within 10 seconds and do the real work afterwards. Anything else is retried with growing gaps (5 s, 20 s, 1 min, 3 min, 10 min, 30 min, 1 h, 2 h, 4 h, 8 h) - ten attempts in total, then the delivery is marked dead. An endpoint that fails 20 deliveries in a row over three days or more is switched off and you get an e-mail; enable it again on its page once it is fixed.
  • Be idempotent. There is exactly one event per thing that happened, but a delivery can be repeated (retries, the Resend button). Use the Idempotency-Key header or event.id to skip what you have already processed.
  • Check livemode. Events from test invoices and from the Send test event button carry livemode: false. They are for checking your code, never for shipping goods.
  • Reconcile when in doubt. GET /v1/events lists every event of your account, newest first, and GET /v1/invoices/:id answers with paid. The Events tab in the dashboard shows the same list with every delivery and a Resend to endpoint button.

Test mode

A test key behaves exactly like a real one, except that its invoices are never paid with coins. On the checkout the customer (that is, you) sees a "Test mode" banner with a Simulate payment button; your server can do the same through the API. Test invoices are credited to a separate test balance that you see only with the Live/Test switch in the dashboard set to test; it is never payable and never real money. Their webhooks carry livemode: false.

bash
# pay a test invoice from your server - "case" may be exact, under or over
curl -X POST https://pay.instantnode.eu/v1/invoices/<id>/simulate \
  -H "X-API-Key: sk_test_..." -H "Content-Type: application/json" \
  -d '{"case": "exact"}'

Checklist before you go live:

  • Your webhook rejects a wrong signature (400) and accepts the test event from the dashboard.
  • An order is only marked paid after invoice.confirmed with paid: true and livemode: true.
  • A repeated delivery does not create a second shipment.
  • invoice.underpaid and invoice.expired show something sensible to the customer.
  • The test key is replaced by a live key in your configuration, and the test key is revoked.

Invoice status

paid is the field to look at: it is true for confirmed and overpaid, and false for everything else.

StatusMeaningpaid
createdCreated, coin not chosen yetno
awaiting_paymentAddress shown, waiting for the transferno
detectedTransaction seen, not final yetno
confirmedPaid and finalyes
overpaidPaid, more than neededyes
underpaidToo little arrived; you can accept what came in (dashboard, up to 7 days after expiry)no
expiredDeadline passed, nothing arrivedno
paid_latePaid after the deadlineno
cancelled / failedCancelled, or an error occurredno

A customer who sends slightly too little (up to 0.5 %) still counts as paid. Below that the invoice becomes underpaid.

Fees, balance and payouts

  • Fee. Every confirmed payment is credited to your balance in the invoice currency minus the platform fee, 1.66 % by default. Invoice 100.00 € → credit 98.34 €. The fee is frozen when the invoice is created, so a later change only affects new invoices.
  • Overpaid. You are credited everything that arrived, at the rate locked for the invoice, minus the fee. The customer's money is yours.
  • Paid late. Up to 60 minutes after the deadline the payment is credited automatically at the locked rate. Later ones wait for you: Accept late payment on the invoice credits what arrived at the current rate; the price move since the invoice is yours.
  • Underpaid. Nothing is credited by itself. Accept partial payment on the invoice credits what arrived at the locked rate, minus the fee; possible until 7 days after the invoice expired, afterwards through support.
  • Balance. Balance in the dashboard shows three numbers per currency: available (what you can pay out), pending (payments detected on chain but not confirmed yet) and reserved (open payouts and refunds, already deducted). Below them every entry: sale, fee, payout, payout_reversal, refund, refund_reversal and adjustment, with a running balance and a CSV export for your bookkeeping.
  • Refunds. From a paid invoice, Refund sends part of the credited amount back to an address the customer names, in the coin they paid with or in your payout coin. The fiat amount leaves your balance at once; the network fee comes out of it, and you see the exact numbers before you confirm. Refunds up to 200 € of accounts approved 30 days ago or more go out by themselves, larger ones are checked by InstantNode. refund.paid / refund.failed arrive as webhooks; GET /v1/refunds lists them. Test invoices cannot be refunded.
  • Payouts. Under Payouts, pick the coin you want to be paid in (SOL by default; also USDC and USDT on Solana, ETH, USDT and USDC on Ethereum, BNB, USDT on BSC, POL, TRX, USDT on TRON, BTC and LTC), save your wallet address for that coin and request a payout. Your balance stays in EUR/USD; the coin is only what the payout is sent as. Minimum 10 € (or $), at most 2,000 per day, one open payout at a time. Each coin keeps its own address, and a new address has to be 24 hours old before it can receive a payout. Requests up to 500 € are approved automatically within a minute; larger ones are checked by InstantNode. Before you confirm, the dashboard shows a quote (rate, coin amount, estimated network fee, what arrives), valid for 60 seconds; the network fee comes out of the payout. The crypto amount is fixed again at approval and sent to your wallet. You receive payout.paid or payout.failed as a webhook; the payout.asset field tells you which coin was sent, networkFeeCrypto what the transfer cost.
  • Automatic payouts. Under Payouts set a threshold, a schedule (as soon as it is reached, at most once a day, at most once a week) and an amount to keep in the balance; the rule requests payouts by itself, marked Automatic. To automate from your own system, POST /v1/payouts with { "currency": "EUR", "amount": "250.00" } (or "amount": "all") does exactly what the dashboard form does, with the same checks; it answers 201 with the payout object, 409 payout_open while one is open, and 400 test_mode for a test key.

Your account

  • Application. Under Account you fill in the business profile: name shown to customers, legal name, website, business category, country, VAT id (optional), expected monthly volume and what you sell, then accept the merchant terms. InstantNode reviews it, usually within a day; you get a mail either way. Test keys work while you wait.
  • Terms. When the merchant terms change you get a mail, and the next dashboard visit shows the new version with a checkbox before anything else opens; API keys and webhooks keep working meanwhile. The current version is always at /terms.
  • Notifications. Under Notifications you choose which mails you want (payment received, payout or refund sent or failed, webhook endpoint disabled, account status, new sign-in from a new device), add up to 5 extra recipients, and optionally a Discord or Slack webhook that gets the same events as a short message.
  • Sessions. Account lists every signed-in browser with device, IP and last activity; sign one out or all others. A sign-in from a device we had not seen on your account is mailed to you.
  • Data export. Create export builds a zip with your profile, invoices, ledger, payouts, refunds, endpoints, events and payment links as JSON. The link works for 60 minutes and is shown once; a fresh sign-in is asked for.
  • Closing the account. Possible once the live balance is zero and no payout or refund is open. Keys and endpoints are revoked at once, all sessions end, the records stay for the retention period.

API reference

Base URL https://pay.instantnode.eu. Every /v1 call needs X-API-Key: sk_live_… (or Authorization: Bearer …); a key from before the pairs (ik_<id>.<secret>) keeps working. Keys live under API keys: a secret key per mode for your server (full access, shown once, rotate with a 24 h overlap, optional IP allowlist) and a public key per mode for the browser, which is only accepted on GET /v1/public/config, POST /v1/public/checkout-sessions and the checkout status. Everything else answers 403 public_key_not_allowed to a public key.

ScopeAllows
invoices:readReading invoices, currencies, your account, balance, ledger and payouts
invoices:writeCreating invoices, preselecting a coin, simulating test payments

Writing calls need an active account (403 merchant_not_approved while pending, 403 merchant_suspended when suspended). A test key may write while the account is still pending. Reading always works.

Every endpoint with parameters, bodies and responses is in the interactive reference below. You can try requests there with your own key.

Errors come as JSON with a stable error code:

json
{"error": "invalid_request", "message": "amount: Required", "requestId": "…"}
CodeHTTPMeaning
unauthorized401Key missing or wrong
forbidden403The key lacks the scope
public_key_not_allowed403A public key was used outside /v1/public/*
ip_not_allowed403The key has an IP allowlist and this address is not on it
key_expired401The key was rotated and its 24 h grace period is over
merchant_not_approved403Account not active yet; submit the application in the dashboard
merchant_suspended403Account suspended; open invoices are still settled
not_found404No such invoice for this key
invalid_request400A field is missing or malformed
invalid_amount400Amount not positive or more than two decimals
price_currency_not_allowed400Only EUR and USD
unsafe_webhook_url400Not https, not a public hostname, or a private target
selection_failed400Coin unavailable, amount too small or invoice expired
not_test_invoice403simulate was called on a live invoice
idempotency_conflict409Same Idempotency-Key, different body
too_many_open_invoices429More than 1000 open invoices
amount_too_large400Above the maximum invoice amount set for your account
maintenance503InstantPay is in maintenance: new live invoices and payout requests are paused for a moment; retry after the Retry-After seconds
Too Many Requests429Rate limit; see the retry-after header
internal_error500Tell us the requestId

SDKs and examples

Everything is a single file you copy into your project; there is nothing to install.

  • Node: /sdk/node/instapay.mjs - createInvoice, getInvoice, listInvoices, selectAsset, getMerchant, listLedger, listPayouts, getPayout, createPayout, listRefunds, getRefund, simulatePayment, listEvents, getEvent, verifyWebhook, parseWebhook. The constructor takes the secret key; parseWebhook understands both the default and the legacy body.
  • PHP: /sdk/php/InstaPay.php - the same methods for PHP 8 (class InstantPay).
  • Complete shop: /examples/demo-shop/server.mjs is the source of the demo shop: products, hosted checkout, overlay, webhook receiver and order list in one file.
  • Plain HTML overlay: /examples/plain-html.html.
  • WooCommerce: /examples/woocommerce-instapay.php together with the PHP SDK in wp-content/plugins/instapay/; enter base URL, key and webhook secret under WooCommerce → Settings → Payments.

Questions or a stuck integration: [email protected].

Full reference · try requests live with your key