Developers

Take your first payment in four steps

Mint a key, create a payment request, verify the callback, switch the network. There is no SDK to install and no client library to keep up to date — it is HTTP and an HMAC.

Base URL https://cryptogate.foundrcode.com/api/merchant/v1

Quickstart

From nothing to a paid invoice

Every command below is real. Paste them in order and the last one hands you a link a customer can pay.

1

Get an API key

Sign in, open API and webhooks, and create a key. You get one string, made of a public key id and a secret joined by a dot. It is shown once — only a SHA-256 hash of the secret is stored, so we cannot re-read it to you later.

Put it in your environment, never in your repository. Revoking a key stops it on the next request; the record of what it did survives.

.env
CRYPTOGATE_KEY=cg_<24 chars>.<48 char secret>
CRYPTOGATE_WEBHOOK_SECRET=whsec_<48 chars>

# The signing secret is separate from the API key and is
# stable across key rotation, so replacing a key does not
# break a receiver that is already verifying callbacks.
2

Create a payment request

One POST. You get back a receive address derived for this invoice alone — which is what makes an inbound payment attributable — and a slug for the hosted page.

Send the amount as a string. 0.0125 as a JSON number is a double, and a double cannot hold it exactly.

create-invoice.sh
curl -X POST https://cryptogate.foundrcode.com/api/merchant/v1/payment-requests \
  -H "Authorization: Bearer $CRYPTOGATE_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "asset_id": 1,
    "title": "Invoice 1042",
    "amount": "0.0125",
    "reference": "ORD-1042",
    "ttl_minutes": 60,
    "webhook_url": "https://example.com/hooks/cryptogate"
  }'
201 Created
{
  "payment_request": {
    "id": "k3n8qzv2rt7wp1xd9fhs4bmc",
    "status": "pending",
    "asset": "BTC",
    "address": "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx",
    "amount_requested": "0.0125",
    "amount_outstanding": "0.0125",
    "expires_at": "2026-08-05T12:00:00.000000Z"
  }
}

Send the payer to https://cryptogate.foundrcode.com/pay/k3n8qzv2rt7wp1xd9fhs4bmc — a hosted page with a QR, the exact amount, a countdown and live status. Or fetch the same data as JSON from https://cryptogate.foundrcode.com/api/pay/{slug} and build your own.

3

Handle the webhook

Three events exist: payment.paid, payment.late and payment.expired. Verify the HMAC over the raw body before you act on any of them.

Deliveries retry up to six times with backoff, so your handler will occasionally see the same event twice. Key on id plus event and make the write idempotent.

Full webhook reference
webhook.php
$raw = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_CRYPTOGATE_SIGNATURE'] ?? '';

$expected = hash_hmac(
    'sha256', $raw, getenv('CRYPTOGATE_WEBHOOK_SECRET')
);

// Constant time, over the RAW bytes. Re-encoding the parsed
// JSON changes whitespace and the digest will not match.
if (! hash_equals($expected, $signature)) {
    http_response_code(400);
    exit;
}

$event = json_decode($raw, true);

if ($event['event'] === 'payment.paid') {
    Order::fulfilOnce($event['reference']);
}

http_response_code(200);
4

Go live

The checklist before you point real money at it.

Test on testnet first

A payment request made on a testnet deployment gives you a testnet address, and a faucet coin pays it end to end — including the webhook. Do that before anything else.

Verify the signature in production too

The commonest failure is a receiver that checks the signature in staging and skips it live because a tunnel was in the way.

Keep amounts as strings the whole way

Through your queue, your database column and your templates. One json_decode into a float undoes the precision the API preserved.

Handle partial and overpaid

An underpaid invoice stays partial — it is not settled, and treating it as such lets someone buy anything for one satoshi. An overpayment is credited and flagged so you can refund the difference.

Point the webhook at a public HTTPS host

Loopback, private ranges and link-local are refused, and the check runs again at send time rather than only when you save the URL.

Watch the deliveries page

It lists the recent callbacks with their status codes, which is the fastest way to find a receiver that is quietly 500ing.

This deployment is on testnet

Addresses issued here are testnet addresses and the coins are worthless. That makes it a good place to build the integration against, and a bad place to invoice a customer from. "Go live" above means switching the deployment's network — talk to us about what that involves for your account.

Conventions

Four things that will save you an afternoon

Amounts are strings, in and out

Every monetary field is a decimal string at 18 places of precision. Sending a number is rejected rather than rounded.

Your reference is the join key

Set reference to your own order id. It comes back on the object and in every webhook, and it is never shown to the payer.

Invoices expire, donation links do not

ttl_minutes runs from 1 minute to 7 days and defaults to 60. A donation link has no amount and no deadline.

Retries are normal, so be idempotent

On webhooks in, and on anything that moves money out. A retried request that pays twice is the expensive kind of bug.

Stuck on something?

The reference documents every endpoint and every error shape. If the answer is not there, ask a human — the same people who wrote the code.