Documentation

Base URL: https://api.stablevoy.com. The full machine-readable reference is the OpenAPI 3.1 document at /v1/openapi.json. Everything the dashboard does is available through the same API.

Quick start

  1. Sign in to the dashboard with your wallet and set a payout address.
  2. Create a store, choose networks, set your webhook URL and keep the signing secret.
  3. Create an API key for your server. It is shown once.
  4. Create invoices from your server and send customers to the returned checkoutUrl.
  5. Mark orders paid when a signed invoice.paid webhook arrives.

Create an invoice

curl -X POST https://api.stablevoy.com/v1/invoices \
  -H "Authorization: Bearer sk_live_..." \
  -H "Idempotency-Key: order-1042" \
  -H "Content-Type: application/json" \
  -d '{"amountUsdCents": 2599, "externalReference": "order-1042"}'

Amounts are integer US cents. Repeating a request with the same Idempotency-Key returns the same invoice; reusing a key with different parameters is rejected with 409. The response contains one deposit address per network and the hosted checkoutUrl. Pass an optional returnUrl (your order page): after a successful payment the payment page sends the buyer back there automatically after 10 seconds, with buttons to go at once or to stay.

Invoice statuses

  • awaiting_payment, confirming: open.
  • paid, overpaid, paid_late: settled, ship the order.
  • underpaid, expired: not enough was received in time.
  • on_hold: the payer address is on the OFAC sanctions list. Funds still only go to your wallet; decide with your compliance policy.

Webhooks

Events: invoice.paid, invoice.overpaid, invoice.underpaid, invoice.expired, invoice.paid_late, invoice.on_hold, webhook.test. Delivery is at least once with exponential retries for about a day; deduplicate by the X-Stablevoy-Event-Id header. Every request carries X-Stablevoy-Signature: t=<unix>,v1=<hex>, an HMAC-SHA256 of t + "." + raw body with your signing secret.

Verify in Node.js

import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody: the exact request body bytes, before any JSON parsing.
export function verifyStablevoySignature(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const timestamp = Number(parts.t);
  if (!Number.isInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest();
  const given = Buffer.from(parts.v1 ?? '', 'hex');
  return given.length === expected.length && timingSafeEqual(given, expected);
}

Verify in PHP

function stablevoy_verify(string $rawBody, string $header, string $secret, int $tolerance = 300): bool {
    $parts = [];
    foreach (explode(',', $header) as $pair) {
        [$k, $v] = array_pad(explode('=', $pair, 2), 2, '');
        $parts[$k] = $v;
    }
    $t = (int) ($parts['t'] ?? 0);
    if ($t === 0 || abs(time() - $t) > $tolerance) {
        return false;
    }
    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
    return hash_equals($expected, $parts['v1'] ?? '');
}

Errors

Errors are RFC 7807 problem documents with a stable code, for example amount_above_platform_cap, amount_above_store_limit, payout_address_not_set, networks_unavailable, store_limit_reached, validation_failed.

Recovery guide

Stablevoy never holds your funds. Each deposit address is a contract that can only send to your payout address and the fixed fee recipient. If our service were unavailable, you can move funds yourself:

  1. Open the ForwarderFactory contract of the network on its block explorer (address listed in the API at /v1/networks).
  2. Call sweep(invoiceSalt, merchant, feeBps, token) with the invoice salt, your payout address, the fee in basis points and the token address. These values are in your invoice data and in every webhook.
  3. Anyone can submit this transaction, and the funds can only arrive at your address.