DOCS / API

API quickstart

From API key to a verified webhook on the TRON Nile sandbox.

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.

shell
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/settings

2. 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.

  1. Install iCryptex Keyring from Downloads in the dashboard (beta builds coming soon).
  2. Export the account xPub in Keyring.
  3. 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.

shell
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}'
FieldTypeNotes
amountnumberFixed 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_amountnumberOpen invoices only. Default 10.
descriptionstringUp to 500 characters.
customer_refstringReuses one stable address per customer.
timeout_secondsinteger60 to 86400, default 3600.
metadataobjectEchoed in reads and webhooks, up to 4096 bytes and 50 keys.
callback_urlstringhttps only. Overrides your webhook URL for this invoice.

Response (shortened):

json
{
  "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.

python
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)
javascript
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)

InputValue
secretwhsec_test_vector_0001
raw body{"event":"invoice.paid","event_id":"evt_0123456789abcdef","livemode":false}
X-Porto-Signature649a305aae7d34ff2e634061752cd50c287324021a1d12d8ffac955dafce58c5
shell
printf '%s' '{"event":"invoice.paid","event_id":"evt_0123456789abcdef","livemode":false}' \
  | openssl dgst -sha256 -hmac whsec_test_vector_0001

Events

EventWhen
invoice.paidThe invoice reached paid (once).
invoice.overpaidPaid with more than the amount, or a later transfer to a paid invoice.
invoice.underpaidA confirmed transfer leaves a fixed invoice short.
invoice.below_minimumAn open invoice is below min_amount.
invoice.late_paymentFunds arrived after expiry or cancellation.
invoice.expired, invoice.cancelledThe invoice closed without payment.
invoice.detectedA transfer is in a block but not final; off by default.
sweep.completedA sweep signed in iCryptex Keyring closed the invoice.

Payload (shortened):

json
{
  "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

StatusGroupMeaning
pendingopenAwaiting payment.
activatingopenInternal; treat as pending.
detectedopenA transfer is in a block, not yet final.
underpaid / partialopenFixed invoice: confirmed total below amount minus tolerance.
below_minimumopenOpen invoice: confirmed total below min_amount.
paidpaidFully paid (overpayment shows in overpaid_amount).
expiredclosedNot paid in time.
cancelledclosedCancelled by you.
late_paymentclosedFunds arrived after expiry or cancellation.
withdrawn / sweptclosedFunds 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.

Coming soon

This section is not available yet.