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.
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: 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.
/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
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.
/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
}
/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 */ } }
/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." }
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