1. Get your API key
Message @portmanto in Telegram. Keys are issued on request by an admin: pk_test_ keys for the Nile sandbox, and pk_live_ keys for mainnet, on request. A test key sent to the mainnet API (or a live key to the sandbox) is rejected with 401.
Send the key on every request as Authorization: Bearer <api_key> or X-API-Key: <api_key>. The same key logs you in to the dashboard. Keep it on your server only.
Base URL: https://api.icryptex.com/v1. Every route lives under /v1; the bare /v1/ returns 404 by design. Connectivity check: GET /v1/merchant/settings returns 401 without a key and your settings with one.
curl -i https://api.icryptex.com/v1/merchant/settings # 401 without a key
curl -H "Authorization: Bearer pk_test_..." https://api.icryptex.com/v1/merchant/settings2. Connect your xPub
iCryptex is non-custodial. Invoice addresses are derived from your wallet's account xPub, so customer payments land on addresses only you can spend from. Your private keys stay in iCryptex Keyring on your computer.
- Install iCryptex Keyring from Downloads in the dashboard (beta builds coming soon).
- Export the account xPub in Keyring.
- In @portmanto, send
/register, then send the xPub.
Without an xPub, creating an invoice returns 409. The dashboard checklist shows "xPub connected" when it is attached.
3. Create an invoice
POST /v1/invoices creates an invoice and returns 201 with a unique TRC-20 address. Send an Idempotency-Key to make retries safe: the same key and body returns the stored response with Idempotent-Replayed: true.
curl https://api.icryptex.com/v1/invoices \
-H "Authorization: Bearer pk_test_..." \
-H "Idempotency-Key: order-1042" \
-H "Content-Type: application/json" \
-d '{"amount": 50, "description": "Order #1042", "metadata": {"order_id": "1042"}, "timeout_seconds": 1800}'| Field | Type | Notes |
|---|---|---|
amount | number | Fixed invoice amount in USDT, above 0 and up to 1,000,000. Omit it for an open invoice. |
type | "fixed" or "open" | Optional. An invoice with an amount is fixed. |
min_amount | number | Open invoices only. Default 10. |
description | string | Up to 500 characters. |
customer_ref | string | Reuses one stable address per customer. |
timeout_seconds | integer | 60 to 86400, default 3600. |
metadata | object | Echoed in reads and webhooks, up to 4096 bytes and 50 keys. |
callback_url | string | https only. Overrides your webhook URL for this invoice. |
Response (shortened):
{
"invoice_id": 1042,
"type": "fixed",
"status": "pending",
"amount": 50.0,
"address": "TRRPU387srsJJKiv8DfEsPnLBiWBcRMfCu",
"payment_uri": "tron:TRRPU387srsJJKiv8DfEsPnLBiWBcRMfCu?amount=50&token=...",
"expires_at": "2026-10-04T10:25:00Z",
"checkout_url": "https://pay.icryptex.com/pay/<token>",
"livemode": false,
"network": "nile"
}network is "mainnet" or "nile". checkout_url is the hosted payment page, https://pay.icryptex.com/pay/<token>; send the customer there, or show the address and amount yourself. Reusable payment links live at /pay/l/<slug>; the health check is /pay/healthz. The root of pay.icryptex.com returns 404 by design.
Read it back with GET /v1/invoices/{id}; list with GET /v1/invoices. Cancel a pending invoice with DELETE /v1/invoices/{id}.
4. Pay it on Nile
On the sandbox the customer sends Nile test USDT to the invoice address. Two ways to get it:
- If enabled on the sandbox stand, the payment page shows a "Pay for me (test USDT)" button (
POST /pay/{token}/simulate) that pays from the sandbox faucet. It never exists on mainnet. - Get Nile test USDT and TRX from the Nile faucet and send the USDT from any TRON wallet.
A transfer is counted once its block is 19 blocks deep (about one minute). Until then the invoice is detected; then it becomes paid. A short payment within the tolerance (default the larger of 0.5 USDT and 1%) still counts as paid.
5. Verify webhooks
iCryptex POSTs events to your webhook URL (set it in the dashboard, Settings > Webhooks; the signing secret is shown once). The body is canonical JSON (sorted keys, no spaces) and the header X-Porto-Signature is the lowercase hex HMAC-SHA256 of the raw body bytes, keyed with your webhook secret. Version 1 has no timestamp header: verify the signature over the raw body before parsing and deduplicate on event_id. Any 2xx response is success.
import hashlib, hmac
def verify(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody, signature, secret) {
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
return expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}Test vector (v1)
| Input | Value |
|---|---|
| secret | whsec_test_vector_0001 |
| raw body | {"event":"invoice.paid","event_id":"evt_0123456789abcdef","livemode":false} |
| X-Porto-Signature | 649a305aae7d34ff2e634061752cd50c287324021a1d12d8ffac955dafce58c5 |
printf '%s' '{"event":"invoice.paid","event_id":"evt_0123456789abcdef","livemode":false}' \
| openssl dgst -sha256 -hmac whsec_test_vector_0001Events
| Event | When |
|---|---|
invoice.paid | The invoice reached paid (once). |
invoice.overpaid | Paid with more than the amount, or a later transfer to a paid invoice. |
invoice.underpaid | A confirmed transfer leaves a fixed invoice short. |
invoice.below_minimum | An open invoice is below min_amount. |
invoice.late_payment | Funds arrived after expiry or cancellation. |
invoice.expired, invoice.cancelled | The invoice closed without payment. |
invoice.detected | A transfer is in a block but not final; off by default. |
sweep.completed | A sweep signed in iCryptex Keyring closed the invoice. |
Payload (shortened):
{
"event": "invoice.paid",
"event_id": "3f1c0a9e5b7d4e2a8c6b1d0f9e8a7b6c",
"livemode": false,
"network": "nile",
"invoice": {"invoice_id": 1042, "status": "paid", "amount": 50.0, "amount_received": 50.0, "metadata": {"order_id": "1042"}},
"transaction": {"tx_hash": "e8db4a17...", "amount": 50.0, "confirmed": true, "kind": "payment"}
}Failed deliveries are retried after 1 min, 5 min, 30 min, 2 h and 12 h (6 attempts in total). You can resend a failed delivery from the dashboard. Check livemode so a test event never fulfils a real order.
6. Statuses
| Status | Group | Meaning |
|---|---|---|
pending | open | Awaiting payment. |
activating | open | Internal; treat as pending. |
detected | open | A transfer is in a block, not yet final. |
underpaid / partial | open | Fixed invoice: confirmed total below amount minus tolerance. |
below_minimum | open | Open invoice: confirmed total below min_amount. |
paid | paid | Fully paid (overpayment shows in overpaid_amount). |
expired | closed | Not paid in time. |
cancelled | closed | Cancelled by you. |
late_payment | closed | Funds arrived after expiry or cancellation. |
withdrawn / swept | closed | Funds were swept to your wallet. |
7. Go live
The dashboard checklist ("Before you go live") shows what is missing: xPub connected, webhook URL and secret set, last delivery succeeded and a paid test invoice. Mainnet is available on request in @portmanto: an admin issues a pk_live_ key for the mainnet API and nothing else changes in your integration.
Plans are activated and cancelled on request via @portmanto. Billed monthly in USDT.