Skip to content

Developer docs

Payments

Take crypto payments on your own site. Your server creates a payment for an amount in USD, the buyer pays on a payment page that Hidden hosts, and your server gets a signed webhook when the payment is complete.

How it works

  1. 1

    Create a payment

    Your server calls POST /payments with the amount in USD and your own reference. The answer carries url.

  2. 2

    Send the buyer to the page

    Redirect the buyer to url. They pick a coin and a network and pay with the details shown there.

  3. 3

    Get the result

    Hidden sends payment.paid to your webhook endpoint when the payment is complete, and the page sends the buyer back to your returnUrl.

  4. 4

    Deliver

    Deliver what the buyer bought after payment.paid, never after the return to your site alone: anyone can open a return URL.

The buyer always pays on the Hidden page

The API never returns a deposit address, a coin amount or a QR code, and the payment page cannot be shown inside a frame. Every buyer pays on a page that shows your store name and the Hidden mark (Enterprise stores show only their own name and logo).

Turn on payments

  1. 1

    Turn on payments

    In Developers, Payments, turn on payments and add the website where you take them, for example https://shop.example.com. Return and cancel URLs must be on one of these websites. You can add up to 5.

  2. 2

    Create a key

    Create an API key with the Payments scope in Developers, API keys. Keep it on your server: calls to create a payment from a web page are refused.

  3. 3

    Add a webhook endpoint

    Add an endpoint that receives payment.* events in Developers, Webhooks.

Test payments work before you turn payments on.

Create a payment

Send the amount in USD and your own reference from your server. Only amountUsd is required.

curl -X POST https://api.sellhidden.com/v1/payments \
  -H "Authorization: Bearer hsk_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "amountUsd": 49,
    "reference": "order-1042",
    "description": "Pro plan, 1 month",
    "returnUrl": "https://shop.example.com/thanks?order=1042",
    "cancelUrl": "https://shop.example.com/cart",
    "buyerEmail": "buyer@example.com",
    "metadata": {
      "plan": "pro"
    }
  }'
Fields of POST /payments
FieldTypeDescription
amountUsdrequiredType: numberAt least 1.00 and at most your per-payment limit (never more than 10,000). Rounded to cents.
referenceType: stringYour own id, such as an order number: 1 to 64 letters, digits and _ . : @ + / = -. While a payment with it is open, the same amount returns that payment.
descriptionType: stringUp to 120 characters of plain text, shown to the buyer.
returnUrlType: stringWhere the page sends the buyer after paying: https, up to 2,048 characters, on one of your payment websites. Test payments may also return to localhost.
cancelUrlType: stringLinked on the page until the buyer pays. The same rules as returnUrl.
buyerEmailType: stringThe buyer's email, up to 254 characters. Hidden emails them a receipt once the payment is paid.
expiresInMinutesType: 15..1440The time the buyer has to start paying. Default 60.
metadataType: objectUp to 20 keys of letters, digits, _ . or - (40 characters each), with string, number or true/false values of up to 200 characters, and 2 KB of JSON in all. Returned to you, never shown to the buyer.
testType: booleanSet to true for a test payment: no money moves, and its page offers Mark as paid and Mark as expired.

The answer is 201 Created with the payment. Every payment endpoint and every payment.* event carries this same object:

201 Createdjson
{
  "ok": true,
  "payment": {
    "id": "NOVA-7K2Q-9XHD",
    "url": "https://sellhidden.com/order/NOVA-7K2Q-9XHD",
    "status": "pending",
    "test": false,
    "amountUsd": 49,
    "paidUsd": null,
    "shortUsd": 0,
    "overpaidUsd": 0,
    "creditedUsd": null,
    "feeUsd": 0,
    "networkFeeUsd": 0,
    "reference": "order-1042",
    "description": "Pro plan, 1 month",
    "metadata": {
      "plan": "pro"
    },
    "buyerEmail": "buyer@example.com",
    "returnUrl": "https://shop.example.com/thanks?order=1042",
    "cancelUrl": "https://shop.example.com/cart",
    "coin": null,
    "network": null,
    "underpaid": null,
    "availableAt": null,
    "createdAt": "2026-09-26T09:58:40.000Z",
    "expiresAt": "2026-09-26T10:58:40.000Z",
    "paidAt": null
  }
}

Then send the buyer to payment.url, for example with a 303 redirect. Keep payment.id: it is the id of the payment in every event and in GET /payments/{id}.

Create the payment when the buyer clicks Pay, not when your page loads: open payments count against your limits.

The payment page

The page shows your store name and logo, the description, the amount and the Hidden mark. On the Enterprise plan the Hidden mark is left out.

The buyer picks a coin and a network. The page shows the exact amount and a QR code, waits for the network to confirm, and then sends the buyer to your returnUrl. Until they pay, a link takes them back to your cancelUrl.

The page is in the buyer's language and works on phones.

The buyer has expiresInMinutes to start paying. Once they pick a coin, the address stays valid for the time shown on the page.

Statuses and webhooks

Payment statuses
StatusDescription
pendingWaiting for the buyer.
confirmingA transfer was seen and is waiting for confirmations.
underpaidLess than the amount arrived. The buyer can send the rest until underpaid.payableUntil.
paidComplete. Deliver.
expiredThe time ran out. paidUsd above 0 means money arrived and waits for you to accept it.
cancelledYou cancelled it.
Payment events
EventSent when
payment.paidAn API payment was paid in full, within your tolerance, or accepted by you. Carries the payment object: deliver now.
payment.underpaidLess than the amount arrived. Adds underpaid.receivedUsd, remainingUsd and payableUntil.
payment.expiredThe payment expired. paidUsd above 0 means money arrived and waits for you to accept it.
payment.cancelledThe payment was cancelled through the API or the dashboard.
payment.overpaidA transfer arrived after the payment was complete. Adds extraUsd; it is not added to your balance.

Each event carries the payment as data: the same object as GET /payments/{id}. Verify the signature like every webhook (Verify signatures). Test payments carry "test": true, and order.* events never fire for payments.

example deliveryhttp
POST https://example.com/hidden-webhook
Content-Type: application/json
X-Hidden-Event: payment.paid
X-Hidden-Delivery: dlv_01j9k2…
X-Hidden-Timestamp: 1727344800
X-Hidden-Signature: sha256=3f8a…c21e

{
  "id": "evt_9f2c…",
  "event": "payment.paid",
  "createdAt": "2026-09-26T10:01:12.000Z",
  "data": {
    "id": "NOVA-7K2Q-9XHD",
    "url": "https://sellhidden.com/order/NOVA-7K2Q-9XHD",
    "status": "paid",
    "test": false,
    "amountUsd": 49,
    "paidUsd": 49,
    "shortUsd": 0,
    "overpaidUsd": 0,
    "creditedUsd": 49,
    "feeUsd": 0,
    "networkFeeUsd": 0,
    "reference": "order-1042",
    "description": "Pro plan, 1 month",
    "metadata": {
      "plan": "pro"
    },
    "buyerEmail": "buyer@example.com",
    "returnUrl": "https://shop.example.com/thanks?order=1042",
    "cancelUrl": "https://shop.example.com/cart",
    "coin": "USDT",
    "network": "Tron (TRC20)",
    "underpaid": null,
    "availableAt": "2026-09-28T10:01:12.000Z",
    "createdAt": "2026-09-26T09:58:40.000Z",
    "expiresAt": "2026-09-26T10:58:40.000Z",
    "paidAt": "2026-09-26T10:01:12.000Z"
  }
}
// After checking X-Hidden-Signature (see Verifying signatures on the Webhooks page):
const event = JSON.parse(rawBody);
const payment = event.data;

if (event.event === "payment.paid") {
  if (payment.test) {
    console.log("Test payment marked as paid:", payment.reference); // nothing was paid
  } else {
    await deliverOnce(payment.reference, payment.id); // your order id, Hidden's payment id
  }
}
return new Response("ok");
# After checking X-Hidden-Signature (see Verifying signatures on the Webhooks page):
event = json.loads(raw_body)
payment = event["data"]

if event["event"] == "payment.paid":
    if payment["test"]:
        print("Test payment marked as paid:", payment["reference"])  # nothing was paid
    else:
        deliver_once(payment["reference"], payment["id"])  # your order id, Hidden's payment id
<?php
// After checking X-Hidden-Signature (see Verifying signatures on the Webhooks page):
$event   = json_decode($raw, true);
$payment = $event["data"];

if ($event["event"] === "payment.paid") {
    if ($payment["test"]) {
        error_log("Test payment marked as paid: " . $payment["reference"]); // nothing was paid
    } else {
        deliver_once($payment["reference"], $payment["id"]); // your order id, Hidden's payment id
    }
}
http_response_code(200);

Shown in Node

If an event did not arrive, read the payment with GET /payments/{id}.

Amounts, fees and payouts

There is no platform fee. The buyer pays the network fee on top of the amount, unless you chose in Payment methods to pay it yourself. Then it comes out of your credit and shows as networkFeeUsd.

creditedUsd is what was added to your store balance.

Money from an API payment can be withdrawn from availableAt, after a short hold.

New stores start with lower limits on single payments and on the total per 24 hours. Your limits and hold are shown in Developers, Payments. Write to support to raise them.

Short and extra payments

A payment short by no more than your underpayment tolerance (Payment methods) counts as paid. You are credited what arrived, and shortUsd shows the gap.

If more is missing, the payment is underpaid. The buyer can send the rest from the same page for 48 hours. You can accept what arrived at any time with POST /payments/{id}/accept. Otherwise the payment expires and the money waits for you to accept it.

Money above the amount is not added to your balance. overpaidUsd shows it, and payment.overpaid tells you when a transfer arrives after the payment was complete.

Refunds

Crypto payments are final, and Hidden does not send refunds for API payments. To refund a buyer, send them the money yourself.

References and retries

Send an Idempotency-Key header with every create. A retry after a timeout then returns the first answer instead of creating a second payment.

An answer that asks you to wait is not kept for its key: a 429, a 503, and 409 payment_started, payment_in_flight or payment_busy. After the wait, retry with the same key.

reference is your own id, such as an order number. While a payment with that reference is open, creating it again with the same amount returns the same payment (existing: true). With another amount the answer is 409 reference_in_use.

After a payment with that reference is paid, the reference cannot be used again (409 reference_paid).

Test payments

Create a payment with "test": true. Its page shows Test mode and two buttons: Mark as paid and Mark as expired.

Your endpoint receives the matching events with "test": true. No money moves and nothing is credited.

Keys made by the Try it button in these docs always create test payments.

Errors

Errors have the usual shape (Responses and errors), with any extra values next to code. The codes of the payments endpoints:

Payment error codes
CodeStatusDescription
validation_errorStatus: 400A field is missing or not valid. field names it.
site_not_allowedStatus: 400returnUrl or cancelUrl is not on one of your payment websites. The message lists them.
amount_over_limitStatus: 400amountUsd is above your limit for one payment (maxUsd).
payments_offStatus: 403Payments are off for your store. Turn them on in Developers, Payments.
payments_blockedStatus: 403Payments are blocked for your store. Write to support.
payments_pausedStatus: 403Your store is suspended or its withdrawals are paused.
server_onlyStatus: 403The call came from a web page. Create payments from your server.
reference_in_useStatus: 409An open payment has this reference with another amount, or one expired less than 2 hours after the buyer got an address and a late transfer can still arrive (paymentId). Cancel the open one, wait, or use a new reference.
reference_paidStatus: 409A payment with this reference was already paid (paymentId).
daily_limitStatus: 429Your payments of the last 24 hours reached your daily limit (dailyUsd). Try again after Retry-After.
open_limitStatus: 429You have too many open payments (openMax). Cancel some, or let them expire.
rate_limitedStatus: 429Too many payments in a short time. Try again after Retry-After.
payments_unavailableStatus: 503New payments cannot be created right now. Test payments still work. Try again later.
not_foundStatus: 404No payment with this id in your store.
not_cancellableStatus: 409Only a pending payment can be cancelled.
payment_startedStatus: 409The buyer started paying, or money arrived. Try again after retryAfter, or accept what arrived.
nothing_receivedStatus: 409Nothing arrived, so there is nothing to accept.
payment_in_flightStatus: 409A transfer is still confirming (until). Try again after it settles.
payment_busyStatus: 409Another action on this payment is running. Try again in a moment.
not_underpaidStatus: 409Only an underpaid or expired payment can be accepted.

Key errors such as unauthorized, insufficient_scope and store_suspended are the same as on every endpoint (Authentication).

Get help

Send us the endpoint, the request or delivery id and the response you got. Security findings go through responsible disclosure. Live platform status is on the support page.

See also