Skip to content

Admin API

Admin endpoints are authenticated via an httpOnly cookie named admin_token, issued by POST /api/v1/admin/auth/login. There is no Bearer token support — all admin requests must carry the cookie.

Response casing

Admin responses are snake_case. Request bodies are parsed with dual-accept: both snake_case (canonical) and camelCase (deprecated) keys are accepted during the transition. New integrations should use snake_case.

Authentication

Login

http
POST /api/v1/admin/auth/login
Content-Type: application/json

{"password": "your-admin-password"}

Rate-limited to 5 attempts per 15 minutes. On success sets the admin_token cookie (httpOnly, SameSite=Strict, Secure in production, 24h max-age, path /).

Response 200:

json
{ "ok": true }

Error 401: { "error": "Invalid password", "code": "INVALID_PASSWORD" }

Logout

http
POST /api/v1/admin/auth/logout
Cookie: admin_token=<jwt>

Clears the admin_token cookie.

Response 200: { "ok": true }


Dashboard stats

http
GET /api/v1/admin/stats
Cookie: admin_token=<jwt>

Response 200:

json
{
  "today": {
    "order_count": 42,
    "completed_count": 38,
    "total_received": "3820.500000",
    "success_rate": "90.48"
  },
  "week": {
    "order_count": 310,
    "completed_count": 287,
    "total_received": "28450.120000",
    "success_rate": "92.58"
  },
  "total": {
    "order_count": 1420,
    "completed_count": 1305,
    "total_received": "284500.000000",
    "success_rate": "91.90",
    "merchant_count": 5
  },
  "address_pool": {
    "total": 120,
    "free": 98,
    "allocated": 22
  }
}

All monetary values are strings. success_rate is a fixed-precision percentage string.

Daily revenue (last 7 days)

http
GET /api/v1/admin/stats/daily
Cookie: admin_token=<jwt>

Fixed 7-day window. There is no days query parameter.

Response 200:

json
[
  { "date": "2026-04-03", "total_received": "1200.000000" },
  { "date": "2026-04-04", "total_received": "980.500000" }
]

Incidents (latest 10)

http
GET /api/v1/admin/incidents
Cookie: admin_token=<jwt>

Returns the 10 most recent webhook_failed and sweep_failed events across all orders.

Response 200:

json
{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "created_at": "2026-04-08T12:34:56.000Z",
      "event_type": "webhook_failed",
      "payload": { "reason": "HTTP 500", "attempt": 10 },
      "order_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "order_status": "callback_failed",
      "external_id": "order_123",
      "merchant_name": "My Shop"
    }
  ]
}

Merchants

List merchants

http
GET /api/v1/admin/merchants
Cookie: admin_token=<jwt>

Response 200:

json
{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "My Shop",
      "api_key_prefix": "pw_live_xxxx",
      "is_active": true,
      "created_at": "2026-01-01T00:00:00.000Z",
      "order_count": 142
    }
  ]
}

Create merchant

http
POST /api/v1/admin/merchants
Cookie: admin_token=<jwt>
Content-Type: application/json

{
  "name": "New Shop",
  "webhook_secret": "a-long-random-string"
}
FieldTypeRequiredDescription
namestringyes1–255 chars
webhook_secretstringyes8–255 chars. Used to HMAC-sign outgoing webhooks for this merchant

Dual-accept: webhookSecret is also accepted but deprecated.

Response 201:

json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "New Shop",
  "api_key": "pw_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "api_key_prefix": "pw_live_xxxx",
  "webhook_secret": "a-long-random-string",
  "is_active": true,
  "created_at": "2026-01-01T00:00:00.000Z"
}

One-time display

api_key is returned only on creation. It cannot be retrieved again — store it immediately. Only api_key_prefix is exposed on subsequent reads.

Error 400: { "error": "Invalid request body", "code": "INVALID_BODY" }

Get merchant detail

http
GET /api/v1/admin/merchants/:id
Cookie: admin_token=<jwt>

Returns the merchant record, aggregated stats (order_count, completed_count, callback_failed_count, active_order_count, total_received, last_order_at, success_rate, incident_count), the 10 most recent orders, and the 10 most recent incidents for this merchant.

Error 404: { "error": "Merchant not found", "code": "NOT_FOUND" }

Update merchant

http
PUT /api/v1/admin/merchants/:id
Cookie: admin_token=<jwt>
Content-Type: application/json

{
  "name": "Renamed Shop",
  "is_active": false,
  "webhook_secret": "new-secret-if-rotating"
}

All fields optional; at least one must be present. Dual-accept: isActive / webhookSecret also accepted.

Response 200:

json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Renamed Shop",
  "api_key_prefix": "pw_live_xxxx",
  "is_active": false,
  "created_at": "2026-01-01T00:00:00.000Z"
}

Errors:

  • 400 INVALID_BODY — validation failed
  • 400 EMPTY_UPDATE — no fields supplied
  • 404 NOT_FOUND — merchant id does not exist

Suspended merchants (is_active: false) cannot create new orders; existing orders continue processing.

Rotate API key

http
POST /api/v1/admin/merchants/:id/rotate-api-key
Cookie: admin_token=<jwt>

Generates a new API key; the old key is immediately invalidated.

Response 200:

json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "My Shop",
  "api_key": "pw_live_new_key_here...",
  "api_key_prefix": "pw_live_newk"
}

Orders (admin view)

List orders

http
GET /api/v1/admin/orders?page=1&limit=20&status=confirmed&merchant_id=550e8400-e29b-41d4-a716-446655440000
Cookie: admin_token=<jwt>
Query paramDefaultDescription
page1Page number
limit20Items per page (max 100)
statusFilter by order status
merchant_idFilter by merchant UUID (dual-accept merchantId)
fromISO timestamp lower bound on created_at
toISO timestamp upper bound on created_at

Response 200:

json
{
  "data": [
    {
      "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "external_id": "order_123",
      "merchant_id": "550e8400-e29b-41d4-a716-446655440000",
      "merchant_name": "My Shop",
      "address": "TXxx...",
      "amount_expected": "99.000000",
      "amount_received": "99.000000",
      "status": "completed",
      "tx_hash": "def456...",
      "created_at": "2026-04-08T00:00:00.000Z",
      "expires_at": "2026-04-08T01:00:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 142,
    "total_pages": 8
  }
}

Get order detail

http
GET /api/v1/admin/orders/:id
Cookie: admin_token=<jwt>

Returns { order, events, transaction, settlement, meta } where meta contains system info (network, USDT contract, Tronscan base URL, sweep config). transaction and settlement are null until a payment is seen and swept respectively.

Error 404: { "error": "Order not found", "code": "NOT_FOUND" }

Retry webhook

http
POST /api/v1/admin/orders/:id/retry-webhook
Cookie: admin_token=<jwt>
Content-Type: application/json

{ "callback_url": "https://sandbox.example.com/hooks" }

Re-enqueues webhook delivery for an order. The body is optional; when present, callback_url is a one-shot override — it is used for this retry only and is never persisted on the order. Dual-accept: callbackUrl also accepted.

Only orders in a webhook-retry-eligible state (e.g. callback_failed) can be retried.

Response 200: { "ok": true }

Errors:

  • 400 VALIDATION_ERRORcallback_url is not a valid http(s) URL
  • 404 NOT_FOUND — order id does not exist
  • 409 INVALID_STATE — order is not eligible for webhook retry

Retry sweep

http
POST /api/v1/admin/orders/:id/retry-sweep
Cookie: admin_token=<jwt>

Re-enqueues the fund sweep for an order whose sweep previously failed.

Response 200: { "ok": true }

Errors:

  • 404 NOT_FOUND — order id does not exist
  • 409 INVALID_STATE — order is not eligible for sweep retry
  • 409 SWEEP_DISABLED — sweep is not configured on this deployment

Wallet

http
GET /api/v1/admin/wallet
Cookie: admin_token=<jwt>

Response 200:

json
{
  "hot_wallet": {
    "address": "TXxx...",
    "trx_balance": "450.000000",
    "is_low_balance": false
  },
  "cold_wallet": {
    "address": "TYyy..."
  },
  "address_pool": {
    "total": 120,
    "free": 98,
    "allocated": 22
  },
  "sweep_queue": {
    "waiting": 0,
    "active": 0,
    "failed": 0
  },
  "system": {
    "tron_network": "nile",
    "usdt_contract": "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf",
    "trongrid_url": "https://nile.trongrid.io",
    "tronscan_base_url": "https://nile.tronscan.org/#",
    "tx_explorer_base_url": "https://nile.tronscan.org/#/transaction/",
    "address_explorer_base_url": "https://nile.tronscan.org/#/address/",
    "sweep_enabled": true,
    "sweep_mode": "burn",
    "hot_wallet_alert_trx": 100
  }
}

hot_wallet is null when HOT_WALLET_KEY is not configured; cold_wallet is null when COLD_WALLET_ADDRESS is not configured.


Error codes

CodeHTTPMeaning
INVALID_PASSWORD401Wrong admin password on login
ADMIN_UNAUTHORIZED401Missing admin_token cookie
ADMIN_TOKEN_INVALID401Cookie present but JWT invalid or expired
INVALID_BODY400Request body failed schema validation
INVALID_QUERY400Query params failed schema validation
VALIDATION_ERROR400Field-level validation failure (e.g. bad override URL)
EMPTY_UPDATE400PUT merchant called with no fields
NOT_FOUND404Resource does not exist
INVALID_STATE409Order not eligible for the requested admin action
SWEEP_DISABLED409Sweep is not enabled on this deployment

Released under the BSL 1.1 License.