// developers · api reference

Docs

Everything you need to integrate the crypt.pe gateway. Stripe-style API surface, HMAC-signed webhooks, non-custodial — funds settle directly to your wallet.

Quickstart

  1. Sign up at crypt.pe/signup and add your wallet address.
  2. Upgrade to Business ($29/mo) to unlock API keys and the gateway. The personal payment link stays free — only programmatic orders require a paid plan.
  3. Open the dashboard → Gateway card → click new key. Save the sk_live_* and whsec_* values.
  4. From your backend, create an order:
curl -X POST https://crypt.pe/api/v1/payments \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "amount_usd": 49,
    "accepted_coins": ["eth","matic","usdc-erc20"],
    "return_url":   "https://your-site.com/thanks",
    "webhook_url":  "https://your-site.com/webhooks/cryptpe"
  }'

The response includes checkout_url. Redirect your customer there. When payment confirms on-chain, we POST a signed event to your webhook_url.

Authentication

All gateway endpoints use bearer authentication with your sk_live_* key. Never expose the secret key in client-side code. The companion pk_live_* publishable key is reserved for future browser SDKs; today it is not strictly required.

Authorization: Bearer sk_live_<your-secret-key>

Keys are revocable from the dashboard. Revoked keys fail all subsequent requests with HTTP 401.

Payments

POST/api/v1/payments

Create a new order. Returns the order plus a checkout_url you should redirect the customer to.

// optional headers

  • Idempotency-Key — replays with the same key return the original order.

// request body

{
  "amount_usd":      49,
  "accepted_coins":  ["eth","matic","usdc-erc20"],   // optional, default: all enabled
  "return_url":      "https://your-site.com/thanks", // optional
  "webhook_url":     "https://your-site.com/webhooks/cryptpe",
  "customer_email":  "buyer@example.com",            // optional
  "metadata":        { "wc_order_id": 1234 },        // optional
  "expires_in_minutes": 30                            // optional, default 30, max 1440
}

// response

{
  "order_id":     "cp_QxAy6YABSYiIDIDEquW2ir",
  "status":       "pending",
  "amount_usd":   49,
  "accepted_coins": ["eth","matic","usdc-erc20"],
  "expires_at":   "2026-05-30T20:30:00+00:00",
  "checkout_url": "https://crypt.pe/checkout/cp_QxAy6YABSYiIDIDEquW2ir",
  "merchant":     { "username": "...", "display_name": "...", "accent_color": "#10D982" }
}
GET/api/v1/payments/{order_id}

Fetch an order you previously created. Use this to poll status if your webhook handler is offline; webhooks are still the recommended channel.

Hosted checkout

The checkout_url we return is a public page that runs end-to-end. You don't normally need to call these endpoints; they exist so you could build a custom checkout UI if you ever wanted one.

GET/api/orders/{order_id}/public

Returns the order without authentication — used by the checkout page. Stale pending orders past expires_at are auto-flipped to expired on read.

POST/api/orders/{order_id}/select-coin

Customer picks which coin to pay in. Locks chosen_coin, chosen_address, and expected_amount_crypto on the order so the webhook matcher has stable values.

{ "coin": "matic" }

Webhooks

When an order changes status we POST a JSON event to your webhook_url. Verify every webhook — assume anyone can fire a fake one at your endpoint.

Event types

  • payment.confirmed — the on-chain transfer matched the expected amount within ±2% tolerance. Mark the customer's order as paid.
  • payment.underpaid — less than the expected amount arrived. Mark as on-hold for manual review.
  • payment.overpaid — more than expected. Refund the difference manually or absorb it.
  • payment.expired — the order expired before any payment was received.

Sample body

{
  "id":      "evt_1706713200_a1b2c3d4",
  "type":    "payment.confirmed",
  "created": "2026-05-30T14:00:00+00:00",
  "data": {
    "order_id":      "cp_QxAy6YABSYiIDIDEquW2ir",
    "status":        "confirmed",
    "amount_usd":    49,
    "coin":          "matic",
    "amount_crypto": 136.111,
    "tx_hash":       "0xabc...",
    "network":       "MATIC_MAINNET",
    "customer_email": "buyer@example.com",
    "metadata":      { "wc_order_id": 1234 }
  }
}

Signature verification

Every request carries an X-Cryptpe-Signature header in the format t=<unix-ts>,v1=<hex>. Re-compute HMAC-SHA256 of "<t>.<raw-body>" with your whsec_* and compare constant-time.

// Node.js using our SDK
const Cryptpe = require('./cryptpe');
const cryptpe = new Cryptpe(process.env.CRYPTPE_SECRET_KEY);

app.post('/webhooks/cryptpe', express.raw({type:'application/json'}), (req, res) => {
  try {
    const event = cryptpe.webhooks.verify(
      req.body,
      req.headers['x-cryptpe-signature'],
      process.env.CRYPTPE_WEBHOOK_SECRET,
    );
    // event.type === 'payment.confirmed' → mark order paid
    res.json({ received: true });
  } catch (e) {
    res.status(400).send('invalid signature');
  }
});
// PHP — matches what our WooCommerce plugin uses
$body = file_get_contents('php://input');
$sig  = $_SERVER['HTTP_X_CRYPTPE_SIGNATURE'];
preg_match('/t=(\d+),v1=([0-9a-f]+)/', $sig, $m);
[$_, $t, $v1] = $m;
$expected = hash_hmac('sha256', $t.'.'.$body, getenv('CRYPTPE_WEBHOOK_SECRET'));
if (!hash_equals($expected, $v1)) { http_response_code(400); exit('bad sig'); }
$event = json_decode($body, true);

Delivery & retries

We attempt delivery up to 3 times with exponential backoff (1s, 5s, 25s). Respond with any 2xx status to acknowledge. Non-2xx, timeouts (>10s), and connection errors trigger retries. Every attempt is logged on our side and visible in the dashboard.

Errors

We return standard HTTP status codes and a JSON body with a single detail string.

  • 400 — your request was malformed (missing fields, invalid coins, amount ≤ 0).
  • 401 — missing or invalid Authorization header.
  • 404 — order not found (or you don't own it).
  • 503 — price feed temporarily unavailable. Retry in a few seconds.

Testing

Use the test webhook button in your dashboard's Gateway card to fire a sample payment.confirmed event at your handler — without spending any crypto. The signature is computed with your real whsec_* so you can validate end-to-end.

SDKs & plugins

Drop-in integrations for popular stacks. The raw HTTP API works from anything that can speak JSON.

// Python SDK · JS button widget · Magento 2 (all live) · Shopify integration guide

Operator: HD wallets (self-hosted)

Self-hosting crypt.pe? Configure HD-derived receiving addresses so every order gets its own on-chain destination — auto-detect becomes an unambiguous address → order_id lookup instead of a fuzzy amount-and-time match. Two trust modes are supported; pick whichever matches your operations posture.

Mode A — xpub-only (recommended)

The host receives only an extended public key per rail. It can derive every incoming-funds address forever, but cannot move funds. The matching private key stays on a hardware wallet or air-gapped machine — that's the machine you use when it's time to sweep.

Derive each xpub at the change-chain level (one above the address index) and paste into the env var listed below.

# Each xpub covers all non-hardened children — i.e. /0, /1, /2, …
# Derive AT the change-chain level (NOT at the address-index level).

CRYPTPE_EVM_XPUB="xpub6D…"   # path: m/44'/60'/0'/0   (Ethereum / Polygon / Base / Arbitrum)
CRYPTPE_BTC_XPUB="zpub6t…"   # path: m/84'/0'/0'/0    (Bitcoin native segwit, bc1q…)
CRYPTPE_TRON_XPUB="xpub6…"   # path: m/44'/195'/0'/0  (Tron, base58 T…)
// how to export from common wallets
  • Sparrow Wallet (BTC): Settings → Keystores → Master Public Key. Choose script type P2WPKH (Native SegWit) → copy the zpub….
  • Electrum (BTC): Wallet → Information → Master Public Key. Create the wallet as Native SegWit. Copy the zpub….
  • Ledger / Trezor (BTC): use Sparrow or Electrum in watch-only mode against the hardware wallet → export the zpub from there. Keeps the seed on-device.
  • EVM (Ethereum + L2s): MetaMask doesn't export xpubs directly. Use iancoleman.io/bip39 offline (download the HTML, run on an air-gapped machine), select BIP44 · ETH, set internal/external = 0, and copy Account Extended Public Key.
  • Tron: same flow as EVM via iancoleman.io — choose coin TRX in the BIP44 panel, copy the account-level xpub….

Mode B — mnemonic (dev / single-host)

Drop the full BIP39 mnemonic into one env var. Simple, but anyone who reads it can sweep your funds — only do this on hosts you fully trust (private dev, single-merchant deployments behind a hardware firewall).

CRYPTPE_HD_MNEMONIC="word1 word2 … word12"

xpub takes priority per rail: if both CRYPTPE_EVM_XPUB and CRYPTPE_HD_MNEMONIC are set, the EVM rail uses the xpub. Useful for mixed setups — e.g. xpub for BTC (hardware wallet) but mnemonic for EVM during initial rollout.

Sweep checklist

  1. Open /admin/cohorts → the hd addresses table shows the next derivation index + preview address per rail.
  2. On your air-gapped machine, derive the same index from the master seed and confirm the address matches. If it doesn't, stop — the host's xpub is wrong.
  3. Sweep the funds. Addresses below next_index are the ones that have been handed out; anything still holding a balance is fair game.