StartFirst payment
Start

Accept your first payment

Create an API key, create an invoice, send the customer to checkout and mark the order paid from the webhook.

Markdown

This page connects a shop to Turmite. You create an API key, create an invoice, send the customer to the checkout page and mark the order paid from the webhook. Do it on a testnet install first.

Before you start

The examples use https://pay.example.com for Turmite and https://shop.example.com for the shop.

1. Create an API key

  1. In the dashboard, open API keys.
  2. Under Create a key, enter a label and your two-factor code. Click Create key.
  3. Copy the key. It starts with tm_. The dashboard shows it once.
  4. Store it in the shop's server environment as TM_API_KEY.

Keep the key on the server. Never put it in browser code. A key works only for its own project.

2. Copy the IPN secret

Turmite signs each webhook with the project's IPN secret.

  1. In the dashboard, open Settings.
  2. Under IPN secret, enter your two-factor code and click Show secret.
  3. Copy the secret.
  4. Store it in the shop's server environment as TM_IPN_SECRET.

To replace the secret later, click Rotate under IPN secret.

3. Check the connection

curl https://pay.example.com/v1/status

The response is {"message":"OK"}. Then list the tickers that your project accepts:

curl https://pay.example.com/v1/currencies -H "x-api-key: $TM_API_KEY"

4. Create an invoice

Create an invoice when the customer checks out. The customer chooses the coin on the Turmite checkout page.

curl https://pay.example.com/v1/invoice \
  -H "x-api-key: $TM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "price_amount": 25,
    "price_currency": "usd",
    "order_id": "ORDER-1001",
    "order_description": "Order 1001",
    "ipn_callback_url": "https://shop.example.com/turmite/ipn",
    "success_url": "https://shop.example.com/orders/ORDER-1001",
    "cancel_url": "https://shop.example.com/cart"
  }'

The same request in Node.js 18 or later:

const res = await fetch('https://pay.example.com/v1/invoice', {
  method: 'POST',
  headers: {
    'x-api-key': process.env.TM_API_KEY,
    'content-type': 'application/json',
  },
  body: JSON.stringify({
    price_amount: 25,
    price_currency: 'usd',
    order_id: 'ORDER-1001',
    order_description: 'Order 1001',
    ipn_callback_url: 'https://shop.example.com/turmite/ipn',
    success_url: 'https://shop.example.com/orders/ORDER-1001',
    cancel_url: 'https://shop.example.com/cart',
  }),
});
if (!res.ok) throw new Error(`Turmite returned ${res.status}`);
const invoice = await res.json();

In PHP:

$ch = curl_init('https://pay.example.com/v1/invoice');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 20,
    CURLOPT_HTTPHEADER => [
        'x-api-key: ' . getenv('TM_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'price_amount' => 25,
        'price_currency' => 'usd',
        'order_id' => 'ORDER-1001',
        'order_description' => 'Order 1001',
        'ipn_callback_url' => 'https://shop.example.com/turmite/ipn',
        'success_url' => 'https://shop.example.com/orders/ORDER-1001',
        'cancel_url' => 'https://shop.example.com/cart',
    ]),
]);
$invoice = json_decode(curl_exec($ch), true);

In Python:

import os, requests

r = requests.post(
    "https://pay.example.com/v1/invoice",
    headers={"x-api-key": os.environ["TM_API_KEY"]},
    json={
        "price_amount": 25,
        "price_currency": "usd",
        "order_id": "ORDER-1001",
        "order_description": "Order 1001",
        "ipn_callback_url": "https://shop.example.com/turmite/ipn",
        "success_url": "https://shop.example.com/orders/ORDER-1001",
        "cancel_url": "https://shop.example.com/cart",
    },
    timeout=20,
)
r.raise_for_status()
invoice = r.json()

The response has the checkout link in invoice_url:

{
  "id": "4522625843",
  "token_id": "k3J9xQ2vT8mZ5wR1nB7cY4pL",
  "order_id": "ORDER-1001",
  "order_description": "Order 1001",
  "price_amount": "25",
  "price_currency": "usd",
  "pay_currency": null,
  "ipn_callback_url": "https://shop.example.com/turmite/ipn",
  "invoice_url": "https://pay.example.com/i/k3J9xQ2vT8mZ5wR1nB7cY4pL",
  "success_url": "https://shop.example.com/orders/ORDER-1001",
  "cancel_url": "https://shop.example.com/cart",
  "created_at": "2026-10-23T12:00:00.000Z",
  "updated_at": "2026-10-23T12:00:00.000Z",
  "expiration_estimate_date": "2026-10-24T12:00:00.000Z"
}

id is numeric text and price_amount is a decimal string. Save id with the order.

5. Send the customer to checkout

Redirect the customer to invoice_url. The checkout page:

  1. Shows the amount and the coins that the project accepts.
  2. Creates a payment when the customer chooses a coin.
  3. Shows the deposit address, the exact amount, a QR code and a countdown.
  4. Updates the status every few seconds. It works with JavaScript off.
  5. Shows a button back to success_url when the payment is complete.

The return button is only a link. Do not mark the order paid when the customer visits success_url. Use the webhook in step 6.

See Checkout for branding and the return URL.

Use your own payment page

To choose the coin in your shop, call POST /v1/payment with pay_currency. The response has pay_address and pay_amount. Your shop shows them to the customer, or sends the customer to payment_url, the hosted payment page for that coin. See Merchant API.

6. Handle the webhook

Turmite sends a POST to ipn_callback_url at each status change. The body is the payment object. The x-turmite-sig header holds the hex HMAC-SHA512 of the body with keys sorted, signed with the IPN secret.

An example body:

{
  "payment_id": 5524759814,
  "invoice_id": 4522625843,
  "payment_status": "finished",
  "pay_address": "tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk",
  "payin_extra_id": null,
  "price_amount": 25,
  "price_currency": "usd",
  "pay_amount": 0.00031245,
  "actually_paid": 0.00031245,
  "pay_currency": "btc",
  "order_id": "ORDER-1001",
  "order_description": "Order 1001",
  "purchase_id": "6084744717",
  "outcome_amount": 0.00031245,
  "outcome_currency": "btc",
  "network": "btc",
  "network_precision": 8,
  "smart_contract": null,
  "payin_hash": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
  "payout_hash": null,
  "payment_url": "https://pay.example.com/pay/Q7mX2pK9vT4nB8cR1sW6yZ3h",
  "created_at": "2026-10-23T12:00:00.000Z",
  "updated_at": "2026-10-23T12:41:07.000Z",
  "expiration_estimate_date": "2026-10-23T13:00:00.000Z"
}

In the webhook, payment_id and invoice_id are numbers and purchase_id is a string. Amounts are numbers. network is a lowercase code.

A handler in Node.js with Express and the TypeScript SDK. verifyIpnSignature makes the key-sorted JSON from the raw body and keeps the text of each number:

import express from 'express';
import { isPaid, verifyIpnSignature } from '@turmite/sdk';

const app = express();

app.post('/turmite/ipn', express.raw({ type: 'application/json' }), async (req, res) => {
  const ok = await verifyIpnSignature(req.body, req.get('x-turmite-sig'), process.env.TM_IPN_SECRET);
  if (!ok) return res.sendStatus(401);

  const payment = JSON.parse(req.body.toString('utf8'));
  if (isPaid(payment.payment_status)) {
    await markOrderPaid(payment.order_id, payment.payment_id); // must be idempotent
  }
  res.sendStatus(200);
});

PHP, Python, Go and Ruby versions, and the timestamped x-turmite-signature, are in Webhooks.

Rules for the handler:

  • Verify the signature before you read any field.
  • Ship the order only on finished.
  • Check that order_id and invoice_id match an order that you created.
  • Make the handler idempotent. The same status can arrive more than once.
  • Respond with a 2xx status fast. Do slow work, for example emails, after the response.
  • Do not depend on the order of deliveries. Turmite does not guarantee it. Use payment_status and updated_at from the body.
  • Turmite waits 30 seconds for each response. A slower response counts as a failure and Turmite retries.

See Payment statuses for every status and what to do.

7. Check the status without a webhook

If your shop missed a webhook, ask for the payment:

curl https://pay.example.com/v1/payment/5524759814 -H "x-api-key: $TM_API_KEY"

Use this as a fallback only. Do not poll in a tight loop.

8. Test on testnet

  1. On a testnet install, enable one network and set its wallet. Base Sepolia or Solana Devnet are fast.
  2. Get test coins from a public faucet for that network. On Solana Devnet you can run solana airdrop 1 <address> --url devnet.
  3. Create an invoice and open invoice_url.
  4. Pay the exact amount from your test wallet.
  5. Watch the payment in the dashboard under Payments.
  6. Check that your handler received each status change up to finished.

On testnet, ipn_callback_url can be an http:// URL or a private address. On mainnet it must be a public https:// URL.

To test the handler without real coins, run turmite serve --demo on your computer. Create a payment, open it in the dashboard and use Simulate a deposit. Turmite then sends the IPN as for a real deposit. See Quick start.

Next steps

  • Webhooks: project endpoints, retries and the second signature.
  • Payouts: refunds and supplier payments from the hot wallet.
  • Go-live checklist: before you switch the shop to mainnet.