Developers

API reference

Four endpoints, one auth header, and amounts that are always strings. Everything below is generated from the routes this application actually serves — if it is not documented here, it does not exist yet.

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

Authentication

Every request carries an API key as a bearer token. A key is two parts joined by a single dot — a public key id and a secret — and it is shown once, when you create it in the dashboard. Only a SHA-256 hash of the secret is stored, so a lost key is replaced rather than recovered.

Authorization header
Authorization: Bearer <key_id>.<secret>
  • The comparison is constant time, and the hash is computed even for an unknown key id, so response timing does not tell an attacker which key ids are real.
  • Revoking a key takes effect on the next request. The record of what it did survives revocation.
  • A suspended account is rejected at the same point, even with a valid key.

Rate limits

The merchant API allows 120 requests per minute. The public payer endpoint is separately limited to 60 per minute because it is unauthenticated. Exceeding either returns 429 with a Retry-After header.

Amounts and precision

Every amount in this API is a decimal string, both in and out. A JSON number is a double, and a double cannot hold 0.1 exactly — so a number where a string was expected is rejected rather than silently rounded.

Correct

"amount": "0.0125"

Rejected

"amount": 0.0125

Arithmetic runs at 18 decimal places, wider than any chain supported. An amount must match ^\d+(\.\d+)?$ — no exponent notation, no thousands separators — and must not carry more decimal places than the asset itself: an amount the chain cannot represent can never be paid exactly, so the invoice would sit at partial forever.

Asset Chain Decimals Confirmations
BTC bitcoin 8 2
ETH ethereum 18 12
LTC litecoin 8 6
MATIC polygon 18 30

Confirmation counts are the shipped defaults and are stored per asset, so an operator can raise them. Check the dashboard for the values on your account.

POST /payment-requests

Create a payment request

Creates a fixed-amount invoice, or an open-amount donation link when type is donation. Each request is given its own freshly derived receive address — that address is what makes an inbound payment attributable to one invoice rather than another.

Body parameters

asset_id

integer required

The asset to be paid in. Ids come from your dashboard; the merchant API does not expose an asset list yet.

title

string required

What the payer sees on the hosted page. Max 191 characters.

amount

decimal string

Required for an invoice, refused for a donation link. Must be positive and within the asset's decimal places.

type

string

invoice or donation. Defaults to invoice.

description

string

Longer detail shown to the payer. Max 2000 characters.

reference

string

Your own order id. Returned to you and included in every webhook, never shown to the payer. Max 128 characters.

ttl_minutes

integer

How long an invoice stays payable, 1 to 10080 (seven days). Defaults to 60. Ignored for donation links, which do not expire.

webhook_url

https url

Where signed callbacks for this request are delivered. Must resolve to a public address — loopback, private and link-local hosts are refused.

redirect_url

https url

Where the payer is sent after paying. Same public-address rule.

Request

create.sh
curl -X POST https://cryptogate.foundrcode.com/api/merchant/v1/payment-requests \
  -H "Authorization: Bearer $CRYPTOGATE_KEY" \
  -H "Content-Type: 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"
  }'

Response 201

{
  "payment_request": {
    "id": "k3n8qzv2rt7wp1xd9fhs4bmc",
    "type": "invoice",
    "title": "Invoice 1042",
    "description": null,
    "status": "pending",
    "asset": "BTC",
    "chain": "bitcoin",
    "address": "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx",
    "amount_requested": "0.0125",
    "amount_paid": "0",
    "amount_outstanding": "0.0125",
    "expires_at": "2026-08-05T12:00:00.000000Z",
    "paid_at": null,
    "redirect_url": null,
    "reference": "ORD-1042",
    "webhook_url": "https://example.com/hooks/cryptogate",
    "created_at": "2026-08-05T11:00:00.000000Z"
  }
}

A rough edge, stated plainly

The id in the response body is the public slug — the one in the hosted page URL. The retrieve and cancel routes below take the internal numeric id, which this payload does not currently return. Until that is reconciled, poll the public endpoint with the slug you already have, or rely on webhooks.

GET /payment-requests

List payment requests

Your requests, newest first, in a standard Laravel paginator envelope. Only your own — the key identifies the account.

per_page

integer

Rows per page, 1 to 100. Defaults to 25.

page

integer

Which page to return. Defaults to 1.

curl https://cryptogate.foundrcode.com/api/merchant/v1/payment-requests?per_page=50 \
  -H "Authorization: Bearer $CRYPTOGATE_KEY"

# 200
{
  "data": [ { /* payment request objects */ } ],
  "current_page": 1,
  "per_page": 50,
  "total": 128
}
GET /payment-requests/{id}

Retrieve one

id here is the internal numeric identifier, an integer in the path. A request belonging to another account answers 404, not 403: confirming that a record exists is itself a leak.

curl https://cryptogate.foundrcode.com/api/merchant/v1/payment-requests/1042 \
  -H "Authorization: Bearer $CRYPTOGATE_KEY"

# 200
{ "payment_request": { /* as above */ } }
POST /payment-requests/{id}/cancel

Cancel one

Closes the request so the hosted page stops accepting payment and the public endpoint answers 404. A request that has already settled cannot be cancelled — the money is on chain, and pretending otherwise would be a lie about a real balance. That attempt returns 422.

curl -X POST https://cryptogate.foundrcode.com/api/merchant/v1/payment-requests/1042/cancel \
  -H "Authorization: Bearer $CRYPTOGATE_KEY"

# 200
{ "payment_request": { "status": "cancelled", ... } }

# 422 when it has already settled
{ "message": "A settled request cannot be cancelled." }
GET https://cryptogate.foundrcode.com/api/pay/{slug}

The payer view

Unauthenticated, so you can build your own checkout in front of it. The random slug is the capability, which is why slugs are not sequential. The payload deliberately omits the merchant's identity, internal ids and your reference, which may carry order information.

The hosted page at https://cryptogate.foundrcode.com/pay/{slug} is the same data rendered for a human. A cancelled or unknown slug answers 404 from both.

The payment request object

The last three fields are returned to an authenticated merchant only; the public payer endpoint stops at redirect_url.

id

string

The public slug. Used in the hosted page URL and the public endpoint.

type

string

invoice or donation.

title

string

Shown to the payer.

description

string | null

Shown to the payer.

status

string

pending, partial, paid, overpaid, expired or cancelled. A pending request past its deadline reports as expired here even before the sweep has run.

asset

string

Ticker: BTC, ETH, LTC or MATIC.

chain

string

bitcoin, ethereum, litecoin or polygon.

address

string

The receive address derived for this request alone.

amount_requested

decimal string | null

Null on a donation link, which accepts any amount.

amount_paid

decimal string

Running total of confirmed payments. "0" until one lands.

amount_outstanding

decimal string | null

What remains. Null on a donation link.

expires_at

timestamp | null

Null on a donation link, which does not expire.

paid_at

timestamp | null

Set when the request first reached paid or overpaid.

redirect_url

string | null

Where the payer is sent afterwards.

reference

string | null

Merchant only. Your order id.

webhook_url

string | null

Merchant only.

created_at

timestamp

Merchant only.

Errors

Errors are JSON with a message. Validation failures add an errors object keyed by field. Send Accept: application/json so a validation failure is never answered with a redirect.

401 Unauthorized

The bearer token is missing, malformed, revoked, or the account is suspended. Identical for every cause — a more helpful message would be a key-enumeration oracle.

{
  "message": "Invalid API credentials."
}
422 Unprocessable content

The body failed validation, or a business rule refused it: an invoice with no amount, an amount finer than the asset's decimals, or cancelling something already settled.

{
  "message": "The amount field format is invalid.",
  "errors": {
    "amount": ["The amount field format is invalid."],
    "asset_id": ["The selected asset id is invalid."]
  }
}
429 Too many requests

You crossed the rate limit for that route. Back off for the number of seconds in Retry-After, then retry.

{
  "message": "Too Many Attempts."
}
404 Not found

No such request, or it belongs to another account. Both answer the same way on purpose.

{
  "message": "Not found."
}

Next: handle the callback

Creating a request is half the integration. The other half is verifying the signature on the webhook that tells you it was paid.

Webhook reference