Crypto payment API.

Make a checkout with one call. Your customer pays in any coin; you get your coin at your wallet; your server hears about it in a signed webhook. No account, no email, no ID: your merchant key is the whole login.

Quickstart

  1. Set up your shop on the setup page: payout coin, payout wallet, shop name. Copy the merchant key it shows you once.
  2. Create an invoice from your server and send the customer to its checkoutUrl.
  3. Handle invoice.paid at your webhook, after checking its signature. Then ship the order.
curl https://silentmule.com/api/v1/invoices \
  -H "Authorization: Bearer $SILENTMULE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount":"49.90","reference":"A-1001","expiresInMinutes":60}'

Authentication

Every call carries your merchant key as a bearer token: Authorization: Bearer mk_.... Keep it on your server, never in a browser or an app. We store only a one-way hash of it, so we can't show it again or send it to you. A missing, wrong or deleted key gets 401 with the same message every time.

Lost your key? There's no email to reset it with, by design. Set up again with the same payout wallet and switch your server to the new key.

Create an invoice

POST /api/v1/invoices

FieldTypeWhat it is
amountstring, requiredThe price in your payout coin, like "49.90". Each payment is worth $50 to $29,999.
referencestring, optionalYour own order number, up to 64 characters. Comes back in every webhook.
expiresInMinutesinteger, optionalHow long the checkout can be started: 15 to 10,080 (7 days). Default 60.

The answer, 201 Created:

{
  "id": "inv_5ftmwfeg7th5h4f3",
  "status": "open",
  "amount": "49.9",
  "asset": "usdt-tron",
  "reference": "A-1001",
  "checkoutUrl": "https://silentmule.com/checkout/inv_5ftmwfeg7th5h4f3",
  "createdAt": "2026-11-04T12:00:00.000Z",
  "expiresAt": "2026-11-04T13:00:00.000Z",
  "paidAt": null,
  "landed": null,
  "payoutTransaction": null,
  "orders": []
}

The checkout shows your shop name and the price, never your payout address. The customer picks any coin; each attempt is its own order with a fresh deposit address, and it pays your wallet.

Read and list invoices

GET /api/v1/invoices/{id} returns one invoice, if it's yours.

GET /api/v1/invoices?status=paid&limit=50&offset=0 lists yours, newest first. Use it to reconcile if your server missed a webhook. limit goes up to 200.

A pay button for any site

Paste this where the button should be. It works on any page that allows HTML: a shop, a blog, a link-in-bio page.

<a href="https://silentmule.com/checkout/l/lnk_1dwgfh9ekaye" data-silentmule-pay data-style="amber">Pay with crypto</a>
<script src="https://silentmule.com/embed.js" async></script>

The script styles every link marked data-silentmule-pay (data-style is amber, dark or light) and opens the checkout in a small window, or in a new tab if the browser blocks windows. It only touches links to SilentMule checkouts, sets no cookies and sends nothing anywhere. Can't add scripts? The dashboard also gives you a plain HTML button with its style written inline.

The invoice and its states

StatusMeansEvent
openWaiting for the customer to pay.none
pendingA deposit is on the network and on its way to you.invoice.pending
paidThe payout to your wallet is sent. Final: it never changes after this.invoice.paid
needs_attentionA short, late or wrong-network deposit, or a failed payout. A person sorts it out; the customer can also try again.invoice.needs_attention
expiredNobody paid before expiresAt.invoice.expired

landed is what actually reached your wallet, in your coin, and payoutTransaction is its hash. Paid in the same coin as your payout coin, landed equals the price. Between two coins the checkout aims a little above your price, so landed is usually the price or slightly more; check it if a few cents matter to you.

Webhooks

Set your endpoint in the dashboard. We POST JSON to it when an invoice moves, with a SilentMule-Signature header. Answer with any 2xx within 10 seconds. Anything else, or no answer, and we retry after 1, 5, 30, 120, 360, 720 and 720 minutes, about 24 hours in all, then mark the delivery failed in your dashboard.

Deliveries can repeat and arrive out of order: deduplicate on the event id, and treat paid as final.

{
  "id": "evt_Qm8s2kT4nV7xY1aB3cD5eF6g",
  "type": "invoice.paid",
  "createdAt": "2026-11-04T12:31:07.000Z",
  "data": {
    "id": "inv_5ftmwfeg7th5h4f3",
    "status": "paid",
    "amount": "49.9",
    "asset": "usdt-tron",
    "reference": "A-1001",
    "checkoutUrl": "https://silentmule.com/checkout/inv_5ftmwfeg7th5h4f3",
    "paidAt": "2026-11-04T12:31:07.000Z",
    "landed": "50.03",
    "payoutTransaction": "0f3a9c..."
  }
}

The other events carry the same data, with the status they announce. The dashboard's test button sends {"type":"test","data":null}.

Verify a signature

The header looks like t=1793750400,v1=d9e3...c680. To check it:

  1. Take t and the raw request body, exactly as received, before any JSON parsing.
  2. Compute HMAC-SHA256 of t + "." + body with your webhook secret (whsec_...).
  3. Compare it with v1 in constant time, and reject anything where t is more than 5 minutes from now.

Node.js

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

export function verify(rawBody, header, secret, toleranceSec = 300) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "");
  if (!m) return false;
  if (Math.abs(Date.now() / 1000 - Number(m[1])) > toleranceSec) return false;
  const want = createHmac("sha256", secret).update(`${m[1]}.${rawBody}`).digest();
  const got = Buffer.from(m[2], "hex");
  return got.length === want.length && timingSafeEqual(got, want);
}

Python

import hmac, hashlib, re, time

def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", header or "")
    if not m or abs(time.time() - int(m.group(1))) > tolerance:
        return False
    want = hmac.new(secret.encode(), m.group(1).encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, m.group(2))

Test vector

Check your code against this before going live. Secret whsec_test_6YxkQ3m9Vt2LpR8sNa4Wz1, timestamp 1793750400, and this exact body:

{"id":"evt_test_0001","type":"invoice.paid","createdAt":"2026-11-04T12:00:00.000Z","data":{"id":"inv_test0000000000","status":"paid","amount":"49.9","asset":"usdt-tron","reference":"A-1001","paidAt":"2026-11-04T12:00:00.000Z","landed":"50.03","payoutTransaction":"0f3a9c"}}

Expected header: t=1793750400,v1=d9e3c64fbaabd468085854451af25fd20265b24fdca5ef984f1ca4eec232c680. Turn the 5-minute check off for this test only.

Errors and limits

Errors are JSON, { "error": "plain words", "field": "amount" }, where field is set when one input is the problem.

StatusWhen
401The merchant key is missing, wrong or deleted.
404No such invoice, or it isn't yours.
422An input doesn't work: amount outside $50 to $29,999, expiry outside 15 minutes to 7 days, a payout address that can't receive.
429More than 120 calls a minute on one merchant key. Wait a few seconds and retry.

Testing

Use the dashboard's Send a test button to check your endpoint and your signature code. To test on your own machine, point the endpoint at a tunnel to it. There are no fake payments: to see the full path end to end, make a small real checkout of $50 and pay it.

No code at all? Make a checkout link in the dashboard for a price and share it anywhere. How merchant payments work.

Changelog

4 November 2026: v1: invoices, hosted checkout, four webhook events, HMAC-SHA256 signatures. Reusable payment links and the pay button. Invoices made from a link carry link.