Complete reference for the Listener Price Oracle (LPO) and Listener Reporting Service (LRS)
All API requests require a Bearer token in the Authorization header.
Authorization: Bearer YOUR_API_KEY
Your API key is provided after subscribing. Keep it secret — it identifies your account for billing and rate limiting.
The LPO delivers real-time cryptocurrency prices with sub-2ms cached responses. Prices are sourced from 6 persistent WebSocket feeds (Coinbase, Bitstamp, Kraken, Gate.io, Crypto.com, OKX) — 357 prewarmed assets pushed in real-time with zero REST polling. REST fallback (Coinbase → Kraken → Bitstamp) activates only if all WebSocket feeds for an asset are stale.
Returns the current price for a cryptocurrency asset.
| Parameter | Type | Required | Description |
|---|---|---|---|
asset | string | Required | Asset symbol (e.g. btc, eth, sol). Case-insensitive. |
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.cpmp-site.org/price?asset=btc"
Returns current prices for multiple cryptocurrency assets in a single request. Fetch up to 30 assets at once — perfect for portfolio dashboards, watchlists, and batch operations. The entire batch counts as 1 request against your rate limits and monthly quota, rewarding efficient API usage.
/prices call with 30 assets counts as 1 request — not 30. Partners and developers who batch their requests are being good API citizens, and we reward that efficiency. Available on all tiers including Free.
| Field | Type | Required | Description |
|---|---|---|---|
assets | string[] | Required | Array of asset symbols (e.g. ["BTC", "ETH", "SOL"]). Case-insensitive. Maximum 30 assets per request. Duplicates are automatically deduplicated. |
curl -X POST -H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"assets": ["BTC", "ETH", "SOL", "DOGE", "XRP"]}' \
"https://api.cpmp-site.org/prices"
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/prices] [200]",
"status_code": 200,
"endpoint": "/prices",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "en",
"api_key_id": "ak_demo123",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbts/v1",
"timestamp": "2026-05-28T20:32:08Z",
"data": {
"prices": {
"BTC": {
"exchange": "coinbase-ws",
"cached": true,
"cache_age_seconds": 0.082,
"timestamp": 1780000328,
"price": "73551.36"
},
"ETH": {
"exchange": "coinbase-ws",
"cached": true,
"cache_age_seconds": 0.081,
"timestamp": 1780000328,
"price": "2016.48"
},
"SOL": {
"exchange": "coinbase-ws",
"cached": true,
"cache_age_seconds": 0.217,
"timestamp": 1780000328,
"price": "82.36"
},
"DOGE": {
"exchange": "coinbase-ws",
"cached": true,
"cache_age_seconds": 4.501,
"timestamp": 1780000324,
"price": "0.09983"
},
"XRP": {
"exchange": "coinbase-ws",
"cached": true,
"cache_age_seconds": 1.389,
"timestamp": 1780000327,
"price": "1.3207"
}
},
"asset_count": 5,
"resolved_count": 5,
"failed": [],
"latency_ms": 0,
"duration_ms": 12,
"monthly_usage": 4521,
"monthly_limit": 100000,
"usage_pct": 4.52,
"usage_warning": "",
"usage_status": "✅"
},
"error": ""
}
data)| Field | Type | Description |
|---|---|---|
prices | object | Map of asset symbol → price data. Each entry contains exchange, cached, cache_age_seconds, timestamp, and price. |
asset_count | int | Number of unique assets requested (after deduplication) |
resolved_count | int | Number of assets successfully resolved |
failed | string[] | Array of asset symbols that could not be resolved (empty on full success) |
latency_ms | int | Sum of exchange lookup latencies for cache misses (0 if all cached) |
duration_ms | int | Total wall-clock processing time |
monthly_usage | int | Number of requests used this month |
monthly_limit | int | Maximum requests allowed per month for your tier |
usage_pct | float | Current usage as a percentage of monthly limit |
usage_warning | string | Graduated warning: "green" (≥85%), "yellow" (≥90%), "red" (≥95%), or "" |
usage_status | string | Visual indicator: ✅, ✅🟡, ✅⚠️, or ✅🔴 |
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/prices] [200]",
...
"data": {
"prices": {
"BTC": { "exchange": "coinbase-ws", "cached": true, ... },
"ETH": { "exchange": "coinbase-ws", "cached": true, ... }
},
"asset_count": 3,
"resolved_count": 2,
"failed": ["FAKECOIN"],
...
}
}
prices, and every asset that didn't in failed. One round-trip, useful data on the first call, no client-side retry logic required. Partial failures don't break your batch.
{
"status": "🛑 [LPO] [us-east-2] [BeastMain] [/prices] [400]",
"status_code": 400,
"endpoint": "/prices",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "en",
"api_key_id": "ak_demo123",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbts/v1",
"timestamp": "2026-05-28T20:32:23Z",
"data": null,
"error": "🛑 [LPO] [us-east-2] [BeastMain] [/prices] [400] Maximum 30 assets per request"
}
["BTC", "btc", "Btc"], the response will contain a single "BTC" entry. Asset symbols are normalized to uppercase and deduplicated before processing.
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/price] [200]",
"status_code": 200,
"endpoint": "/price",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "en",
"api_key_id": "ak_demo123",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbts/v1",
"timestamp": "2026-04-12T14:23:01Z",
"data": {
"usage_status": "✅",
"api_key_id": "key_abc123",
"asset": "BTC",
"price": "83421.57",
"source": "coinbase-ws",
"timestamp": 1744466581,
"readable_timestamp": "2026-04-12T14:23:01Z",
"latency_ms": 0,
"duration_ms": 0,
"cached": true,
"cache_duration_seconds": 4.2,
"monthly_usage": 42,
"monthly_limit": 1000,
"usage_pct": 4.2,
"usage_warning": "",
"ip_address": "203.0.113.42"
},
"error": ""
}
{
"status": "✅ [LPO] [us-east-2] [BeastMirror] [/price] [200]",
"status_code": 200,
"endpoint": "/price",
"cluster_node": "BeastMirror",
"region": "us-east-2",
"language": "en",
"api_key_id": "ak_demo123",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbts/v1",
"timestamp": "2026-04-12T14:23:15Z",
"data": {
"usage_status": "✅",
"api_key_id": "key_abc123",
"asset": "ETH",
"price": "3187.44",
"source": "kraken",
"timestamp": 1744466595,
"readable_timestamp": "2026-04-12T14:23:15Z",
"latency_ms": 38,
"duration_ms": 38,
"cached": false,
"cache_duration_seconds": 0,
"monthly_usage": 43,
"monthly_limit": 1000,
"usage_pct": 4.3,
"usage_warning": "",
"ip_address": "203.0.113.42"
},
"error": ""
}
As monthly usage approaches the query limit, the data.usage_status field includes graduated warning indicators. Thresholds are runtime-tunable via application parameters.
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/price] [200]",
...
"data": {
"usage_status": "✅🟡",
...
"monthly_usage": 855,
"monthly_limit": 1000,
"usage_pct": 85.5,
"usage_warning": "green",
...
},
"error": ""
}
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/price] [200]",
...
"data": {
"usage_status": "✅⚠️",
...
"monthly_usage": 910,
"monthly_limit": 1000,
"usage_pct": 91.0,
"usage_warning": "yellow",
...
},
"error": ""
}
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/price] [200]",
...
"data": {
"usage_status": "✅🔴",
...
"monthly_usage": 960,
"monthly_limit": 1000,
"usage_pct": 96.0,
"usage_warning": "red",
...
},
"error": ""
}
data.monthly_usage approaches data.monthly_limit, the response includes graduated indicators at three configurable thresholds:
✅🟡 — Heads up: usage is climbing✅⚠️ — Caution: approaching your limit✅🔴 — Critical: near your monthly limitusage_warning field is an empty string ("") when usage is below the green threshold. The usage_pct field is always present. Both fields appear in every response — the JSON schema is always consistent. Thresholds are configurable via usage_warning_green_pct, usage_warning_yellow_pct, and usage_warning_red_pct application parameters.
The /price endpoint returns the standard 12-field UME envelope. Price data is nested inside the data field:
| Field | Type | Description |
|---|---|---|
status | string | Color-coded bracket line: ✅ [LPO] [us-east-2] [BeastMain] [/price] [200] |
status_code | int | HTTP status code as integer |
endpoint | string | The URL path that handled the request |
cluster_node | string | Container that served the request (BeastMain, BeastMirror, BeastLRS) |
region | string | AWS region — us-east-2 |
language | string | ISO 639-1 language code for human-readable content |
timestamp | string | RFC 3339 UTC timestamp of response generation |
data | object | Price data payload (see below). null on error. |
error | string | Empty string on success. Full bracketed error message on failure. |
data)| Field | Type | Description |
|---|---|---|
usage_status | string | ✅ on success, ✅🟡 at green warning, ✅⚠️ at yellow warning, ✅🔴 at red warning |
api_key_id | string | Your API key |
asset | string | Uppercase asset symbol |
price | string | Current price in USD (formatted to 9 decimal places, trailing zeros trimmed) |
source | string | Price source tag. WebSocket (real-time): coinbase-ws, bitstamp-ws, kraken-ws, gateio-ws, cryptocom-ws, okx-ws. REST fallback: coinbase, bitstamp, kraken, gateio, cryptocom, okx. |
timestamp | int64 | Unix timestamp of the price |
readable_timestamp | string | RFC3339 formatted timestamp |
latency_ms | int64 | Time to fetch price from exchange (0 on cache hit) |
duration_ms | int64 | Total request processing time including validation |
cached | bool | Whether the response was served from cache |
cache_duration_seconds | float64 | Age of cached value in seconds (0 if live fetch) |
monthly_usage | int | Number of queries used this month |
monthly_limit | int | Maximum queries allowed per month for your tier |
usage_pct | float64 | Current usage as a percentage of monthly limit (always present) |
usage_warning | string | Graduated warning level: "green" (≥85%), "yellow" (≥90%), "red" (≥95%), or "" (empty string when below green threshold). Always present. |
ip_address | string | Client IP address |
The LRS provides access to your API usage history and aggregated statistics. Included with every subscription tier.
Returns paginated usage log entries for your API key.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
api_key_id | string | Optional | — | Filter by API key ID |
asset | string | Optional | — | Filter by asset symbol (e.g. BTC) |
start_date | string | Optional | 93 days ago | Start date in YYYY-MM-DD format |
end_date | string | Optional | Today | End date in YYYY-MM-DD format |
cached | bool | Optional | — | Filter by cache status: true or false |
page | int | Optional | 1 | Page number |
page_size | int | Optional | 100 | Records per page (max 1000) |
format | string | Optional | json | Output format: json, csv, tsv, text |
# JSON (default)
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://lrs.cpmp-site.org/reports/usage?asset=BTC&page=1&page_size=50"
# CSV download
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://lrs.cpmp-site.org/reports/usage?format=csv" -o usage.csv
# Tab-delimited
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://lrs.cpmp-site.org/reports/usage?format=tsv" -o usage.tsv
# Plain text
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://lrs.cpmp-site.org/reports/usage?format=text"
Returns aggregated statistics for a date range.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
api_key_id | string | Optional | — | Filter by API key ID |
start_date | string | Optional | 30 days ago | Start date in YYYY-MM-DD format |
end_date | string | Optional | Today | End date in YYYY-MM-DD format |
format | string | Optional | json | Output format: json, csv, tsv, text |
| Format | Content-Type | Use Case |
|---|---|---|
json | application/json | Default. Best for programmatic consumption and APIs. |
csv | text/csv | Comma-separated. Import into Excel, Google Sheets, or any CSV-compatible tool. |
tsv | text/tab-separated-values | Tab-delimited. Ideal for databases, ETL pipelines, and tools that handle commas in data. |
text | text/plain | Human-readable plain text. Good for quick terminal review or log files. |
{
"logs": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"api_key_id": "key_abc123",
"asset": "BTC",
"price": 83421.57,
"source": "coinbase",
"cached": true,
"latency_ms": 7,
"duration_ms": 7,
"ip_address": "203.0.113.42",
"timestamp": 1744466581,
"readable_timestamp": "2026-04-12T14:23:01Z",
"cache_age_seconds": 4.2
}
],
"total_count": 1482,
"page": 1,
"page_size": 100,
"filters": {
"asset": "BTC"
}
}
id,api_key_id,asset,price,source,cached,latency_ms,duration_ms,ip_address,timestamp,readable_timestamp,cache_age_seconds
a1b2c3d4-e5f6-7890-abcd-ef1234567890,key_abc123,BTC,83421.57000000,coinbase,true,7,7,203.0.113.42,1744466581,2026-04-12T14:23:01Z,4.200
LRS Usage Report — Page 1 | Page Size 100 | Total Records 1482
================================================================================
[2026-04-12T14:23:01Z] a1b2c3d4... | Asset: BTC | Price: 83421.57000000 | Source: coinbase | Cached: true | Latency: 7ms | IP: 203.0.113.42
{
"total_requests": 14820,
"cache_hit_rate": 91.4,
"avg_latency_ms": 9.3,
"by_asset": {
"BTC": 6210,
"ETH": 4105,
"SOL": 2380,
"DOGE": 1240,
"XRP": 885
},
"by_source": {
"coinbase": 13540,
"kraken": 1180,
"bitstamp": 100
},
"date_range": {
"start": "2026-03-13",
"end": "2026-04-12"
}
}
LRS Summary Report — 2026-03-13 to 2026-04-12
============================================================
Total Requests : 14820
Cache Hit Rate : 91.40%
Avg Latency : 9.30ms
By Asset:
BTC 6210
ETH 4105
SOL 2380
By Source:
coinbase 13540
kraken 1180
bitstamp 100
The Trinity Beast LPO serves 357 prewarmed assets across 6 exchanges via persistent WebSocket connections. Per-exchange split reflects real liquidity ceilings: Bitstamp 48, Coinbase 66, Crypto.com 46, Gate.io 65, Kraken 65, OKX 67. Prices arrive in real-time (push-based) before any request is made.
| Exchange | Source Tag | WebSocket Endpoint | Pair Suffix | Assets |
|---|---|---|---|---|
| Coinbase | coinbase-ws | wss://advanced-trade-ws.coinbase.com | USD | 66 |
| Bitstamp | bitstamp-ws | wss://ws.bitstamp.net | USD | 48 |
| Kraken | kraken-ws | wss://ws.kraken.com/v2 | USD | 65 |
| Gate.io | gateio-ws | wss://api.gateio.ws/ws/v4/ | USDT | 65 |
| Crypto.com | cryptocom-ws | wss://stream.crypto.com/exchange/v1/market | USD | 46 |
| OKX | okx-ws | wss://ws.okx.com:8443/ws/v5/public | USDT | 67 |
BTC · ETH · SOL · DOGE · XRP · LINK · DOT · LTC · AVAX · UNI · PEPE · XLM · ETC · ICP · RENDER · BONK · BCH · JASMY · AXS · CHZ · STORJ · ZEC · DASH · CGLD · NEAR
AAVE · ATOM · ARB · CRV · GRT · FIL · SHIB · BAT · LRC · MASK · ANKR · SKL · API3 · QNT · HYPE · LPT · HNT · UMA · JTO · CTX · POL · PNUT · POPCAT · DRIFT · CUBE
NANO · SC · LSK · KAVA · BICO · RARI · OCEAN · CFG · CQT · ALGO · FET · FLOW · XTZ · QTUM · ICX · ASTR · ENJ · EGLD · COTI · MINA · GLMR · MOVR · KSM · TEER · CRO
BNB · TRX · APT · SEI · INJ · OP · SUI · VET · HBAR · SAND · MANA · S · CHR · FLR · THETA · CELR · REEF · DENT · HOT · POND · IOTX · ALICE · SUPER · FLUX · ONE
TON · WLD · APE · BLUR · IMX · ENS · LDO · SNX · COMP · 1INCH · SUSHI · GALA · MAGIC · ORDI · PIXEL · RUNE · ARKM · DYM · GMX · CRO · TAO · AGLD · AR · AUDIO · POL
TIA · JUP · STRK · PYTH · W · ZRO · PENDLE · ONDO · WIF · FLOKI · NOT · AEVO · ENA · ETHFI · TURBO · NEIRO · DOGS · HMSTR · EIGEN · BOME · KAITO · GRASS · BANANA · ACE · TNSR
| Priority | Source | Source Tag | Timeout | Weight |
|---|---|---|---|---|
| 1 | Coinbase | coinbase | 100ms | 0.50 |
| 2 | Kraken | kraken | 80ms | 0.35 |
| 3 | Bitstamp | bitstamp | 200ms | 0.15 |
REST fallback is rarely triggered — with 6 WebSocket feeds pushing prices in real-time, the cache hit rate is 98.5%+. REST sources are only consulted when an asset's WebSocket price is older than the cache_ttl_seconds threshold (currently 300 seconds).
GET /asset-categories endpoint for a category-grouped listing of all available assets. The GET /exchanges endpoint returns the full exchange configuration including WebSocket endpoints and asset mappings.
| Indicator | Meaning | When It Appears |
|---|---|---|
| ✅ | Success | Every successful price response (usage below green threshold) |
| ✅🟡 | Success with green warning | Monthly usage is at or above 85% of your limit (configurable via usage_warning_green_pct) |
| ✅⚠️ | Success with yellow warning | Monthly usage is at or above 90% of your limit (configurable via usage_warning_yellow_pct) |
| ✅🔴 | Success with red warning | Monthly usage is at or above 95% of your limit (configurable via usage_warning_red_pct) |
| ⚠️ | Rate limit warning | Monthly query limit reached or QPS rate limit exceeded |
| 🛑 | Error | Invalid request, authentication failure, or server error |
All error responses use the unified response envelope with color-coded bracket convention. The status field shows the indicator and origin metadata. The error field contains the full bracketed message.
{
"status": "🛑 [LPO] [us-east-2] [BeastMain] [/price] [401]",
"language": "en",
"api_key_id": "ak_demo123",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbts/v1",
"status_code": 401,
"endpoint": "/price",
"cluster_node": "BeastMain",
"region": "us-east-2",
"timestamp": "2026-04-26T16:20:03Z",
"error": "🛑 [LPO] [us-east-2] [BeastMain] [/price] [401] Missing required API key parameter"
}
{
"status": "🛑 [LPO] [us-east-2] [BeastMirror] [/price] [429]",
"language": "en",
"api_key_id": "ak_demo123",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbts/v1",
"error": "🛑 [LPO] [us-east-2] [BeastMirror] [429] ⚠️ Monthly query limit reached. You have used 1000 of 1000 allowed queries this month. Upgrade your plan for higher limits.",
"code": 429,
"region": "us-east-2",
"cluster_node": "BeastMirror"
}
| HTTP Status | Meaning | Common Cause |
|---|---|---|
400 | Bad Request | Missing or invalid asset parameter. |
401 | Unauthorized | Missing, invalid, or revoked API key. |
429 | Too Many Requests | Monthly query limit reached or QPS rate limit exceeded. Message includes your current usage and limit. |
500 | Internal Server Error | All price sources (WebSocket + REST) failed or database connectivity issue. |
503 | Service Unavailable | ElastiCache health check failed. |
| Tier | Requests/Month | LRS Included | Price |
|---|---|---|---|
| Free | 1,000 | 10 reports/month | $0/mo |
| Pro | 50,000 | 10 reports/month | $30/mo |
| Enterprise | 500,000 | 10 reports/month | $100/mo |
| Unlimited | Unlimited | Unlimited | $300/mo |
| Lifetime | Unlimited forever | Unlimited forever | $3,000 one-time |
| AWS Partner | Unlimited — no rate limiting | Unlimited | $0 — always free |
| Service | Protocol | Endpoint | Port |
|---|---|---|---|
| LPO (Price API) | HTTPS | https://api.cpmp-site.org | 443 |
| LRS (Reports) | HTTPS | https://lrs.cpmp-site.org | 443 |
| LPO (UDP) | UDP | udp.cpmp-site.org | 2679 |
| LRS (UDP) | UDP | udp.cpmp-site.org | 2680 |
| PrivateLink (LPO) | TCP | VPC Endpoint → NLB | 8080 |
| PrivateLink (LRS) | TCP | VPC Endpoint → NLB | 9090 |
| Method | Path | Description |
|---|---|---|
GET | /exchanges | Public exchange asset list for demo dropdown |
GET | /asset-categories | Asset categories for demo dropdown NEW |
GET | /map/pins | Mission impact map pins |
POST | /translate | Real-time text translation (AWS Translate, cached in ElastiCache) |
GET | /search | Full-text document search (Valkey-indexed, multi-lingual) NEW |
POST | /support/submit | Submit support ticket |
GET | /support/tickets | List tickets by email |
GET | /support/ticket/{ticket_number} | View ticket detail |
GET | /support/resolve | One-click ticket resolve via email link. Query: ?token=.... Single-use, 30-day TTL. NEW |
POST | /demo/register | Demo lead registration NEW |
POST | /partner/apply | Partner application submission |
GET | /partner/status | Partner application status check |
POST | /newsletter/subscribe | Newsletter subscribe |
GET | /newsletter/unsubscribe | Newsletter unsubscribe |
GET | /newsletter/resubscribe | Newsletter resubscribe |
GET | /checkout | Public checkout redirect (Stripe Payment Link) NEW |
GET | /public/status | Public system status — nodes, feeds, uptime (no auth) |
GET | /public/cluster | Public ECS cluster stats — per-node metrics, cache hit rates (no auth) NEW |
| Method | Path | Description |
|---|---|---|
GET | /admin/config | View application parameters |
GET | /admin/setup-demo | Setup demo API key |
GET | /admin/invalidate-key | Invalidate API key cache |
GET | /admin/demo-mode | Toggle demo/performance mode |
GET | /admin/system-mode | Switch system profile |
GET | /admin/reload-params | Force parameter reload |
GET | /admin/profiles | List system profiles NEW |
GET | /admin/stress-stats | Real-time 24-counter metrics |
GET | /admin/stress-reset | Reset metrics counters |
GET | /admin/cluster-stats | Cluster-wide aggregated metrics NEW |
POST | /admin/build-search-index | Rebuild full-text search index from all 32 docs NEW |
GET | /admin/feed-status | Price feed status (all 6 exchanges) |
GET | /admin/partner-ws-stats | Partner WebSocket connections — masked keys, subscribed assets, uptime NEW |
GET | /admin/lang/list | List i18n language keys cached in Valkey NEW |
POST | /admin/lang/set | Push an i18n language JSON blob to Valkey NEW |
GET | /admin/usage-analytics | Usage log analytics |
GET | /admin/adaptive-stats | Adaptive governor stats |
GET | /admin/db-insights | PostgreSQL self-observability (cache hit, top queries, scan health) NEW |
GET | /admin/exchange-feeds | List exchange configurations |
POST | /admin/exchange-feeds/save | Create/update exchange |
POST | /admin/exchange-feeds/toggle | Enable/disable exchange |
DELETE | /admin/exchange-feeds | Remove exchange |
GET | /admin/exchange-assets | List asset mappings |
POST | /admin/exchange-assets/save | Create/update asset mappings |
POST | /admin/exchange-assets/toggle | Flip enabled/prewarm on single asset NEW |
DELETE | /admin/exchange-assets | Remove asset mappings |
POST | /admin/eam/sync | Sync EAM → application_parameters NEW |
GET | /admin/eam/audit | Start 4-layer prewarm audit (async) NEW |
GET | /admin/eam/audit?poll=1 | Poll for audit results NEW |
POST | /admin/eam/discover | Start asset discovery for exchange (async) NEW |
GET | /admin/eam/discover?exchange=okx&poll=1 | Poll for discovery results NEW |
GET | /admin/partner-applications | List partner applications |
POST | /admin/partner-applications/{id}/status | Update application status |
POST | /admin/sql | Execute SQL (read/write) NEW |
POST | /admin/sql-batch | Execute SQL batch (DDL allowed) NEW |
POST | /admin/valkey | Execute ElastiCache commands NEW |
GET | /admin/payment-links | List payment links NEW |
POST | /admin/payment-links/save | Create/update payment link NEW |
POST | /admin/payment-links/toggle | Enable/disable payment link NEW |
POST | /admin/test-refund-email | Send test refund email (dummy data, any language) NEW |
POST | /admin/test-finalize-email | Send test translation completion email with per-language cost breakdown NEW |
POST | /admin/test-customer-email | Send test customer-facing translation email (any template, any language) NEW |
Test email endpoints read live templates. All three test-*-email endpoints resolve their strings through the shared EmailLoader, which means they render exactly what a real send would produce — including any copy edited in Valkey since the last deploy. Use them to verify a template change before it reaches a customer.
| Method | Path | Description |
|---|---|---|
GET | /admin/newsletter/templates | List templates |
GET | /admin/newsletter/templates/{id} | Get template |
POST | /admin/newsletter/templates | Save template |
DELETE | /admin/newsletter/templates/{id} | Delete template |
GET | /admin/newsletter/newsletters | List newsletters |
GET | /admin/newsletter/newsletters/{id} | Get newsletter |
POST | /admin/newsletter/newsletters | Save newsletter |
POST | /admin/newsletter/newsletters/{id}/status | Toggle status |
POST | /admin/newsletter/newsletters/{id}/send | Send newsletter |
GET | /admin/newsletter/subscribers | List subscribers |
| Method | Path | Description |
|---|---|---|
GET | /admin/email/templates | List email templates |
GET | /admin/email/templates/{id} | Get template |
POST | /admin/email/templates | Save template |
DELETE | /admin/email/templates/{id} | Delete template |
GET | /admin/email/senders | List senders |
POST | /admin/email/senders | Save sender |
DELETE | /admin/email/senders/{id} | Delete sender |
GET | /admin/email/drafts | List drafts |
GET | /admin/email/drafts/{id} | Get draft |
POST | /admin/email/drafts | Save draft |
DELETE | /admin/email/drafts/{id} | Delete draft |
POST | /admin/email/drafts/{id}/send | Send email |
| Method | Path | Description |
|---|---|---|
GET | /support/admin/stats | Ticket statistics — counts by status (total, new, open, in_progress, awaiting_customer, resolved, closed) |
GET | /support/admin/tickets | List all tickets. Filters: ?status=, ?category=, ?email=. Ordered by updated_at DESC |
GET | /support/admin/tickets/{id} | Full ticket detail by UUID — includes all replies (internal notes visible), translations, preferred_lang |
POST | /support/admin/tickets/{id}/update | Update ticket fields. Body: {"status":"...", "category":"..."} (at least one required) |
POST | /support/admin/tickets/{id}/reply | Add reply. Body: {"author":"...", "message":"...", "is_internal": false}. Auto-translates + emails customer |
GET | /support/admin/ticket/{ticket_number} | Full ticket by ticket number — includes AutoOps Bedrock analysis from Valkey |
| Method | Path | Description |
|---|---|---|
GET | /dashboard/api/support/tickets | List authenticated user's tickets. Scoped to session email. Ordered by most recent. |
GET | /dashboard/api/support/tickets/{ticket_number} | Ticket detail with reply thread. Excludes internal admin notes. Shows translated replies in customer's language. |
POST | /dashboard/api/support/tickets/{ticket_number}/reply | Post customer reply. Body: {"message":"..."}. Max 10,000 chars. Auto-translates to English. Notifies admin. |
POST | /dashboard/api/support/tickets/{ticket_number}/resolve | Mark own ticket as resolved. Notifies admin. Closed tickets cannot be resolved. |
| Method | Path | Description |
|---|---|---|
GET | /dashboard/api/account | Retrieve full account profile: email, display name, roles, facets, preferred_lang, api_lang, response_format, member since, tier info. |
POST | /dashboard/api/profile | Update account profile. Body fields (all optional): display_name, preferred_lang, api_lang, response_format. Updates ALL non-revoked api_keys rows for the user. |
POST | /dashboard/api/change-email | Initiate email change. Body: {"new_email":"..."}. Sends verification link to new email. 1 pending change at a time. |
Added 2026-08-07 through 2026-08-11 alongside the webhook Delivery Log, Delivery Reports, and dashboard-native activation features (see Trinity-Beast-Account-Dashboards.html §5.5).
| Method | Path | Description |
|---|---|---|
POST | /dashboard/api/webhook/activate | Activates a pending_verification subscription. Body: {"delivery_method":"...", "udp_host":"...", "udp_port":..., "https_url":"..."}. Requires assets already configured. Skips the public API's token-echo verification since a dashboard session already proves control. |
GET | /dashboard/api/webhook/log | Up to 50 recent push events, grouped server-side by (sequence_number, delivery_method), each with a nested per-asset price array. Response shape changed 2026-08-09 — previously one flat row per asset per push; now one grouped row per push with a nested assets[] array. Any consumer reading a flat per-asset shape will break. |
GET | /dashboard/api/webhook/reports/daily | Daily delivery-rollup rows (from webhook_delivery_daily) for the caller's subscription. Date-range filterable, 4-format export (JSON/CSV/TSV/Text). |
GET | /dashboard/api/webhook/reports/summary | Aggregate delivery summary (delivery rate, asset coverage, average latency) over the selected date range. |
POST | /dashboard/api/webhook/reports/refresh | Starts an on-demand rollup refresh outside the 15-minute cron cadence — asynchronous, returns 202 immediately. 3-day scan window, 60-second per-subscription cooldown. |
GET | /dashboard/api/webhook/reports/refresh/status | Poll for the outcome of a refresh started above (running, done, error, or not_found). |
POST | /dashboard/api/add-lrs | Purchases the $20/mo LRS Reports add-on for an eligible account (Free/Pro/Enterprise, or a Webhook/Partner account without a tier-included grant). Creates the Stripe subscription, links to the account's existing Stripe customer via stripe_customer_xref. Replaced the old standalone Stripe Payment Link, now disabled — this is the sole purchase path. |
The public-API mirror of the three webhook-reports endpoints lives at GET /webhook/reports/daily, /webhook/reports/summary, and POST /webhook/reports/refresh — same behavior, authenticated by API key rather than dashboard session, and (deliberately) not gated by LRS entitlement.
Partner connection management. Available to accounts with a Partner-tier key. No asset cap — partner access is unlimited by policy; the watchlist is purely organizational.
| Method | Path | Description |
|---|---|---|
GET | /dashboard/api/partner/config | Current partner configuration: api_key_id, tier, asset_count, assets (array). Returns 404 if the session email has no non-revoked partner key. |
POST | /dashboard/api/partner/assets | Update the partner's asset watchlist. Body: {"assets":["BTC","ETH","SOL",...]}. Deduplicates, normalizes to uppercase. Empty array is valid (clears watchlist — access unaffected). No maximum. Audit-logged. |
GET | /dashboard/api/partner/available-assets | Full pickable catalog (same as GET /asset-categories?full=1). Powers the Partner asset watchlist chip-picker. |
| Method | Path | Description |
|---|---|---|
GET | /dashboard/api/admin/support/tickets | List all tickets system-wide. Filters: ?status=, ?category=. Limit 200. Ordered by created_at DESC. |
GET | /dashboard/api/admin/support/tickets/{id} | Full ticket detail by UUID — includes all replies (internal notes visible), customer IP, email. |
POST | /dashboard/api/admin/support/tickets/{id}/reply | Post admin reply. Body: {"message":"...", "is_internal": false}. Translates + emails customer. Includes one-click resolve link. |
POST | /dashboard/api/admin/support/tickets/{id}/status | Update ticket status. Body: {"status":"open"}. Valid: new, open, in_progress, awaiting_customer, resolved, closed. Audit-logged. |
| Method | Path | Description |
|---|---|---|
GET | /dashboard/api/admin/analytics | Usage analytics from Aurora. ?mode=detail (default, paginated rows) or ?mode=summary (aggregates via a single GROUPING SETS query with p50/p95/p99 latency percentiles). Date filters: ?start_date=, ?end_date=. CSV export via ?format=csv. |
GET | /dashboard/api/admin/daily-rollup | Per-day usage rollup served from the mv_usage_daily materialized view (fast, never scans usage_logs). ?days= (default 30, max 93). Returns totals, latency percentiles, cache-hit rate, cardinality, and a last_refreshed timestamp. NEW |
Both LPO and LRS support UDP for low-latency, connectionless access. UDP is ideal for high-frequency price polling where occasional packet loss is acceptable.
Send a UDP datagram to udp.cpmp-site.org:2679 with your API key and asset.
# Python UDP example
import socket, json
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
request = json.dumps({"api_key": "YOUR_API_KEY", "asset": "btc"}).encode()
sock.sendto(request, ("udp.cpmp-site.org", 2679))
data, addr = sock.recvfrom(4096)
print(json.loads(data))
The reply is JSON carrying the same 12-field envelope as the HTTP API:
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/price] [200]",
"status_code": 200,
"endpoint": "/price",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "en",
"api_key_id": "YOUR_API_KEY",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbi/v1",
"timestamp": "2026-07-30T19:22:14Z",
"data": {
"asset": "BTC",
"price": 64762.93,
"source": "coinbase-ws",
"cached": true,
"monthly_usage": 37862,
"monthly_limit": 999999999,
"usage_pct": 0.0,
"usage_warning": ""
},
"error": ""
}
Note that usage_warning is an empty string rather than absent,
and error is an empty string rather than omitted — the
always-present rule applies over UDP exactly as it does over HTTP.
Send a UDP datagram to udp.cpmp-site.org:2680 for usage reports and summaries.
# Python UDP example
import socket, json
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
request = json.dumps({"api_key": "YOUR_API_KEY", "type": "summary"}).encode()
sock.sendto(request, ("udp.cpmp-site.org", 2680))
data, addr = sock.recvfrom(65535)
print(json.loads(data))
| Method | Use Case | Latency |
|---|---|---|
| HTTPS (ALB) | Standard API access from any client | ~50ms |
| UDP (NLB) | High-frequency polling, low-latency price feeds | ~10ms |
| PrivateLink (NLB) | Private VPC-to-VPC access, no public internet | ~5ms |
Enterprise subscribers can connect via AWS PrivateLink. Both LPO (TCP 8080) and LRS (TCP 9090) are available through the same PrivateLink endpoint. Create a VPC endpoint in your AWS account pointing to our PrivateLink service.
Usage logs are written to Aurora Serverless v2 by the LPO in real-time. A nightly sync job (1 AM EST) copies usage data from Aurora to ElastiCache for fast LRS report generation. Reports reflect data up to the previous day's sync.
Every HTTP endpoint in The Trinity Beast returns a consistent JSON envelope. This makes responses self-reporting — each response tells you which endpoint handled the request, which ECS container answered, and which AWS region served it.
Coverage is now universal. Three endpoints previously returned bare payloads outside the envelope: /search, /ws/assets, and /admin/partner-ws-stats. All three now return the standard 12-field envelope with their payload nested under data. There are zero remaining code paths that bypass the envelope.
Consumer impact: if you were reading response.results from /search, read response.data.results instead. The unwrap pattern is the same for every TBI endpoint — check status_code, throw on error, read the payload from data.
// BEFORE — bare payload (no longer returned)
{ "query": "webhook", "results": [ ... ], "count": 12, "elapsed": "3.1ms" }
// AFTER — standard envelope, payload under data
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/search] [200]",
"status_code": 200,
"endpoint": "/search",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "en",
"api_key_id": "",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbi/v1",
"timestamp": "2026-07-29T20:14:00Z",
"data": {
"query": "webhook",
"results": [
{ "title": "API Reference", "section": "Webhook Push", "url": "/docs/...", "snippet": "...", "score": 27 }
],
"count": 12,
"elapsed": "3.1ms"
},
"error": ""
}
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/health] [200]",
"language": "en",
"api_key_id": "ak_demo123",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbts/v1",
"status_code": 200,
"endpoint": "/health",
"cluster_node": "BeastMain",
"region": "us-east-2",
"timestamp": "2026-04-26T16:20:03Z",
"data": { "message": "Health Check - OK" }
}
{
"status": "🛑 [LPO] [us-east-2] [BeastMain] [/price] [401]",
"language": "en",
"api_key_id": "ak_demo123",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbts/v1",
"status_code": 401,
"endpoint": "/price",
"cluster_node": "BeastMain",
"region": "us-east-2",
"timestamp": "2026-04-26T16:20:03Z",
"error": "🛑 [LPO] [us-east-2] [BeastMain] [/price] [401] Missing required API key parameter"
}
Every response uses a color-coded emoji indicator followed by bracketed origin metadata. This convention matches the usage logs, application logs, and subscriber-facing error messages — consistent across all messaging.
| Indicator | Color | Meaning | HTTP Codes |
|---|---|---|---|
| ✅ | Green | Success | 200, 201 |
| 🛑 | Red | Error | 400, 401, 403, 404, 429, 500, 503 |
| ⚠️ | Yellow | Warning / Redirect | 3xx |
| ✅🔴 | Green + Red | Success with critical warning (usage ≥ 95%) | 200 (with usage_warning: "red") |
| ✅⚠️ | Green + Yellow | Success with caution warning (usage ≥ 90%) | 200 (with usage_warning: "yellow") |
| ✅🟡 | Green + Gold | Success with green warning (usage ≥ 85%) | 200 (with usage_warning: "green") |
| ℹ️ | Blue | Informational | Used in logs |
| 🟠 | Amber | Debug | Used in logs |
OK, OK - NOTE: usage above 85%, OK - WARNING: approaching monthly query limit, OK - CRITICAL: near monthly query limit, and ERROR. This setting applies to both LPO and LRS responses for your API key.
The bracket format self-reports the origin of every message at a glance:
INDICATOR [SERVICE] [REGION] [NODE] [ENDPOINT] [CODE] message
Example: 🛑 [LPO] [us-east-2] [BeastMirror] [/admin/config] [403] Invalid admin key
This convention is used consistently across:
status field (unified envelope)error field (error messages)StandardErrorResponse)usage_logs table)| Field | Type | Description |
|---|---|---|
status | string | Color-coded bracket line: ✅ [LPO] [us-east-2] [BeastMain] [/health] [200]. The leading indicator (✅/🛑/⚠️) switches to text (OK/ERROR/WARN) when the API key's response_format is set to "plain". |
language | string | ISO 639-1 language code declaring the language of all human-readable content in this response (e.g., "en", "ja", "es"). Determined by the user's api_lang setting. Default: "en". |
status_code | int | HTTP status code as an integer for programmatic parsing |
endpoint | string | The URL path that handled the request (self-reporting) |
cluster_node | string | Which ECS container answered: BeastMain, BeastMirror, or BeastLRS |
region | string | AWS region — us-east-2 |
api_key_id | string | Which API key made the call — who asked. Empty string on unauthenticated requests, never absent. |
ip_address | string | The client IP the request arrived from — from where. Empty string when the request was rejected before IP resolution (for example a UDP parse-level error), never absent. |
agent_profile_arn | string | The addressable identity of the actor that produced this response — who answered. Format: arn:tbi:{region}:{account}:agent-profile/{name}/{version}. Never empty. |
timestamp | string | RFC 3339 UTC timestamp of when the response was generated |
data | object | The response payload. Always present. On errors it is null — never omitted. |
error | string | Full bracketed error message with indicator. Always present. On success it is an empty string — never omitted. |
"", 0, false,
[], {}, null)
are valid and are still emitted. A success response and an error response have exactly the same
shape and can be parsed with the same struct or type definition.
data.data || data. Check status_code,
throw on a non-empty error, and read your payload from
data. The shape is the contract.
/price endpoint uses the standard 12-field UME envelope. Price data is nested inside the data field. All endpoints — including /price — return the same envelope shape.
2679 for prices, 2680 for reports) are
UME-compliant, including the bracketed status line.
flowchart LR
A["Client Request"] --> B["⚖️ ALB / NLB"]
B --> C{"ECS Container"}
C --> D["BeastMain"]
C --> E["BeastMirror"]
C --> F["BeastLRS"]
D --> G["WriteJSON / WriteError"]
E --> G
F --> G
G --> H["Unified Envelope"]
H --> I["status: ok | error"]
H --> J["endpoint: /path"]
H --> K["cluster_node: BeastX"]
H --> L["region: us-east-2"]
H --> M["timestamp: RFC3339"]
H --> N["data / error payload"]
style A fill:#334155,stroke:#475569,color:#cbd5e1
style B fill:#334155,stroke:#475569,color:#cbd5e1
style C fill:#2d4a5e,stroke:#475569,color:#cbd5e1
style D fill:#2d4a5e,stroke:#475569,color:#cbd5e1
style E fill:#2d4a5e,stroke:#475569,color:#cbd5e1
style F fill:#2d4a5e,stroke:#475569,color:#cbd5e1
style G fill:#3d3455,stroke:#475569,color:#cbd5e1
style H fill:#FF9900,stroke:#cc7a00,color:#0f172a
style I fill:#1e293b,stroke:#475569,color:#94a3b8
style J fill:#1e293b,stroke:#475569,color:#94a3b8
style K fill:#1e293b,stroke:#475569,color:#94a3b8
style L fill:#1e293b,stroke:#475569,color:#94a3b8
style M fill:#1e293b,stroke:#475569,color:#94a3b8
style N fill:#1e293b,stroke:#475569,color:#94a3b8
Comprehensive inventory of every registered route across The Trinity Beast services.
| Method | Path | Auth | Service | Category | Description |
|---|---|---|---|---|---|
| 🏥 Health & Status | |||||
GET | /health | None | LPO | Health | Health check |
GET | /health | None | LRS | Health | Health check |
| Price | |||||
GET | /price | Bearer | LPO | Price | Fetch real-time crypto price |
| Reports | |||||
GET | /reports/usage | Bearer | LRS | Reports | Usage detail report |
GET | /reports/summary | Bearer | LRS | Reports | Usage summary report |
GET | /reports/report-usage | Bearer | LRS | Reports | Report access detail |
GET | /reports/report-summary | Bearer | LRS | Reports | Report access summary |
| Public | |||||
GET | /exchanges | None | LPO | Exchange Mgmt | Exchange asset list |
GET | /asset-categories | None | LPO | Exchange Mgmt | Asset categories for demo dropdown |
GET | /map/pins | None | LPO | System | Mission impact map pins |
POST | /translate | None | LPO | Translation | Real-time text translation via AWS Translate (ElastiCache cached) |
GET | /search | None | LPO | Search | Full-text document search — query + lang params, returns ranked results with snippets |
GET | /ws/assets | None | LPO | Partners | Partner WebSocket discovery — available assets, connection URL, subscribe protocol |
POST | /translate/signup | None | LPO | Translation | TBTS self-service signup — creates translation key + Stripe customer, sends welcome email |
GET | /translate/languages | None | LPO | Translation | Live 127-language catalog, quote limits and alternate codes (see 15.12) |
GET | /translate/models | None | LPO | Translation | The six translation agents with our AI cost per agent (see 15.1) |
POST | /support/submit | None | LPO | Support | Submit support ticket |
GET | /support/tickets | None | LPO | Support | List tickets by email |
GET | /support/ticket/{ticket_number} | None | LPO | Support | View ticket detail |
POST | /demo/register | None | LPO | System | Demo lead registration |
POST | /partner/apply | None | LPO | Partners | Partner application |
GET | /partner/status | None | LPO | Partners | Partner application status |
POST | /newsletter/subscribe | None | LPO | Newsletter | Newsletter subscribe |
GET | /newsletter/unsubscribe | None | LPO | Newsletter | Newsletter unsubscribe |
GET | /newsletter/resubscribe | None | LPO | Newsletter | Newsletter resubscribe |
| Field | Type | Description | |||
|---|---|---|---|---|---|
| Admin — System | |||||
GET | /admin/config | Admin Key | LPO | System | View application parameters |
GET | /admin/setup-demo | Admin Key | LPO | System | Setup demo API key |
GET | /admin/invalidate-key | Admin Key | Both | System | Invalidate API key cache |
GET | /admin/demo-mode | Admin Key | LPO | System | Toggle demo/performance mode |
GET | /admin/system-mode | Admin Key | LPO | System | Switch system profile |
GET | /admin/reload-params | Admin Key | LPO | System | Force parameter reload |
GET | /admin/profiles | Admin Key | LPO | System | List system profiles |
| Method | Path | Auth | Service | Category | Description |
|---|---|---|---|---|---|
| 📈 Admin — Metrics & Monitoring | |||||
GET | /admin/stress-stats | Admin Key | LPO | Metrics | Real-time 24-counter metrics |
GET | /admin/stress-reset | Admin Key | LPO | Metrics | Reset metrics counters |
GET | /admin/cluster-stats | Admin Key | LPO | Metrics | Cluster-wide aggregated metrics |
GET | /admin/feed-status | Admin Key | LPO | Metrics | Price feed status (all 6 exchanges) |
GET | /admin/usage-analytics | Admin Key | LPO | Metrics | Usage log analytics |
GET | /admin/adaptive-stats | Admin Key | LPO | Metrics | Adaptive governor stats |
GET | /admin/db-insights | Admin Key | LPO | Metrics | PostgreSQL self-observability — cache hit ratio, top queries, scan health |
POST | /admin/db-insights/reset | Admin Key | LPO | Metrics | Reset pg_stat_statements baseline for a clean measurement window |
| Method | Path | Auth | Service | Category | Description |
|---|---|---|---|---|---|
| 🔄 Admin — Exchange Management | |||||
GET | /admin/exchange-feeds | Admin Key | LPO | Exchange Mgmt | List exchange configurations |
POST | /admin/exchange-feeds/save | Admin Key | LPO | Exchange Mgmt | Create/update exchange |
POST | /admin/exchange-feeds/toggle | Admin Key | LPO | Exchange Mgmt | Enable/disable exchange |
DELETE | /admin/exchange-feeds | Admin Key | LPO | Exchange Mgmt | Remove exchange |
GET | /admin/exchange-assets | Admin Key | LPO | Exchange Mgmt | List asset mappings |
POST | /admin/exchange-assets/save | Admin Key | LPO | Exchange Mgmt | Create/update asset mappings |
DELETE | /admin/exchange-assets | Admin Key | LPO | Exchange Mgmt | Remove asset mappings |
| Method | Path | Auth | Service | Category | Description |
|---|---|---|---|---|---|
| 🎫 Admin — Support | |||||
GET | /support/admin/stats | Admin Key | LPO | Support | Ticket statistics |
GET | /support/admin/tickets | Admin Key | LPO | Support | List all tickets |
GET | /support/admin/tickets/{id} | Admin Key | LPO | Support | View ticket detail |
POST | /support/admin/tickets/{id} | Admin Key | LPO | Support | Update ticket |
POST | /support/admin/tickets/{id}/reply | Admin Key | LPO | Support | Add reply |
| 🤝 Admin — Partners | |||||
GET | /admin/partner-applications | Admin Key | LPO | Partners | List applications |
POST | /admin/partner-applications/{id}/status | Admin Key | LPO | Partners | Update status |
| 📰 Admin — Newsletter | |||||
GET | /admin/newsletter/templates | Admin Key | LPO | Newsletter | List templates |
GET | /admin/newsletter/templates/{id} | Admin Key | LPO | Newsletter | Get template |
POST | /admin/newsletter/templates | Admin Key | LPO | Newsletter | Save template |
DELETE | /admin/newsletter/templates/{id} | Admin Key | LPO | Newsletter | Delete template |
GET | /admin/newsletter/newsletters | Admin Key | LPO | Newsletter | List newsletters |
GET | /admin/newsletter/newsletters/{id} | Admin Key | LPO | Newsletter | Get newsletter |
POST | /admin/newsletter/newsletters | Admin Key | LPO | Newsletter | Save newsletter |
POST | /admin/newsletter/newsletters/{id}/status | Admin Key | LPO | Newsletter | Toggle status |
POST | /admin/newsletter/newsletters/{id}/send | Admin Key | LPO | Newsletter | Send newsletter |
GET | /admin/newsletter/subscribers | Admin Key | LPO | Newsletter | List subscribers |
| ✉️ Admin — Email | |||||
GET | /admin/email/templates | Admin Key | LPO | List email templates | |
GET | /admin/email/templates/{id} | Admin Key | LPO | Get template | |
POST | /admin/email/templates | Admin Key | LPO | Save template | |
DELETE | /admin/email/templates/{id} | Admin Key | LPO | Delete template | |
GET | /admin/email/senders | Admin Key | LPO | List senders | |
POST | /admin/email/senders | Admin Key | LPO | Save sender | |
DELETE | /admin/email/senders/{id} | Admin Key | LPO | Delete sender | |
GET | /admin/email/drafts | Admin Key | LPO | List drafts | |
GET | /admin/email/drafts/{id} | Admin Key | LPO | Get draft | |
POST | /admin/email/drafts | Admin Key | LPO | Save draft | |
DELETE | /admin/email/drafts/{id} | Admin Key | LPO | Delete draft | |
POST | /admin/email/drafts/{id}/send | Admin Key | LPO | Send email | |
| 💳 Checkout & Payments | |||||
GET | /checkout | None | LPO | Checkout | Redirect to Stripe Payment Link |
GET | /admin/payment-links | Admin Key | LPO | Checkout | List payment links |
POST | /admin/payment-links/save | Admin Key | LPO | Checkout | Create/update payment link |
POST | /admin/payment-links/toggle | Admin Key | LPO | Checkout | Enable/disable payment link |
| 🗄️ Admin — Database & Cache | |||||
POST | /admin/sql | Admin Key | LPO | Database | Execute SQL (read/write) |
POST | /admin/sql-batch | Admin Key | LPO | Database | Execute SQL batch (DDL allowed) |
POST | /admin/valkey | Admin Key | LPO | Cache | Execute ElastiCache commands |
| UDP Protocol | |||||
| UDP | :2679 | Bearer (in JSON body) | LPO | Price | Price queries |
| UDP | :2680 | Bearer (in JSON body) | LRS | Reports | Report queries |
These endpoints expose operational metrics for the public Infrastructure Live page. No authentication required. Safe for public consumption — sensitive internals (DB pools, internal IPs, config) are stripped.
Returns a curated system status snapshot — node health, feed connections, uptime, and cache hit rate. Designed for the public dashboard. Reads from ElastiCache for sub-millisecond response.
Cache: public, max-age=3
| Field | Type | Description |
|---|---|---|
status | string | "operational" when all systems healthy |
system.nodes | int | Total expected nodes (4) |
system.nodes_healthy | int | Nodes currently reporting |
system.node_names | []string | Names of healthy nodes (BeastMain, BeastMirror, BeastLRS, BeastWebhook) |
system.uptime | string | Human-readable uptime (e.g., "4d 12h") |
system.cache_hit_pct | float | Cluster-wide cache hit percentage |
feeds[] | array | Per-exchange WebSocket feed status |
feeds[].exchange | string | Exchange name (e.g., "Coinbase") |
feeds[].connected | bool | WebSocket connection active |
feeds[].assets | int | Number of assets cached from this feed |
curl https://api.cpmp-site.org/public/status
Returns per-node ECS cluster metrics — the same operational data as /admin/cluster-stats but stripped of sensitive internals. Reads from ElastiCache (each container publishes its stats every 3 seconds).
Cache: public, max-age=5
| Field | Type | Description |
|---|---|---|
status | string | "ok" |
nodes_found | int | Nodes currently reporting |
nodes_expected | int | Expected node count (4) |
region | string | AWS region (us-east-2) |
nodes[] | array | Per-node metrics |
nodes[].name | string | Node name (BeastMain, BeastMirror, BeastLRS, BeastWebhook) |
nodes[].role | string | "LPO + LRS" or "Push Delivery" |
nodes[].uptime_seconds | float | Seconds since container start |
nodes[].tcp_requests | int | Total TCP requests served |
nodes[].udp_requests | int | Total UDP requests served |
nodes[].lrs_requests | int | Total LRS report requests |
nodes[].syncmap_hit_pct | float | Local sync.Map cache hit rate (%) |
nodes[].cache_hit_pct | float | Combined cache hit rate (sync.Map + ElastiCache) (%) |
nodes[].errors_5xx | int | Server errors since start |
nodes[].errors_4xx | int | Client errors since start |
nodes[].rate_limit_hits | int | Rate limit rejections since start |
aggregated | object | Cluster-wide totals (same fields summed across all nodes) |
curl https://api.cpmp-site.org/public/cluster
{
"status": "ok",
"nodes_found": 4,
"nodes_expected": 4,
"region": "us-east-2",
"nodes": [
{
"name": "BeastMain",
"role": "LPO + LRS",
"uptime_seconds": 86412.3,
"tcp_requests": 1247,
"udp_requests": 0,
"lrs_requests": 83,
"syncmap_hit_pct": 99.2,
"cache_hit_pct": 99.8,
"errors_5xx": 0,
"errors_4xx": 12,
"rate_limit_hits": 0
}
],
"aggregated": {
"tcp_requests": 3891,
"udp_requests": 0,
"lrs_requests": 249,
"syncmap_hit_pct": 99.1,
"cache_hit_pct": 99.7,
"rate_limit_hits": 0,
"errors_5xx": 0,
"errors_4xx": 34,
"bg_work_dropped": 0
}
}
Returns infrastructure-wide operational metrics aggregated from CloudWatch, Valkey, AWS ECS, and other AWS service APIs. Powers the Infrastructure Live page. The aggregated payload is cached in Valkey for 60 seconds; the standard response envelope is rebuilt on every request, so cache hits and misses are identical in shape.
Cache: public, max-age=60 (Valkey payload: 60s)
Envelope: Like every TBI response, this endpoint returns the standard 12-field Unified Messaging Envelope (UME). The fields documented below are the contents of the data object. Because this is a public, no-auth endpoint, api_key_id is "" (empty string, always present — never omitted).
data object)| Field | Type | Description |
|---|---|---|
cloudfront.requests_24h | int | CloudFront requests in last 24h |
cloudfront.bytes_24h | int | Bytes downloaded in last 24h |
cloudfront.bytes_human | string | Human-readable bytes (e.g., "1.4 GB") |
waf.alb_blocked_24h | int | ALB WAF blocked requests (24h) |
waf.alb_allowed_24h | int | ALB WAF allowed requests (24h) |
waf.cf_blocked_24h | int | CloudFront WAF blocked requests (24h) |
sqs.visible | int | Messages visible in usage log queue |
sqs.in_flight | int | Messages currently being processed |
lambda.receipt | object | Receipt Lambda stats (invocations_24h, errors_24h) |
lambda.writer | object | Queued Writer Lambda stats |
sync.last_run | string | Last nightly sync timestamp |
sync.status | string | "success" or "failed" |
sync.duration | string | Sync execution time |
sync.search_index | object | Search index stats (languages, sections) |
guardduty.active_findings | int | Active GuardDuty findings |
alarms.total | int | Total CloudWatch alarms configured |
alarms.alarm | int | Alarms currently in ALARM state |
honeypot.total_hits | int | Total honeypot trap hits |
honeypot.auto_blocked | int | IPs auto-blocked by honeypot |
cache.engine | string | "valkey" |
cache.version | string | Valkey version (e.g., "7.2.4") |
cache.items | int | Total keys in Valkey (DBSIZE) |
cache.hit_rate | float | All-time cache hit rate (%) |
cache.memory_used | string | Human-readable memory usage (e.g., "3.03G") |
autoops.lambdas | object | Per-function invocation counts (24h) |
autoops.step_functions | object | Step Function execution counts (24h) |
autoops.self_heal_count | int | Self-heal actions this month |
autoops.last_threat | string | Current threat level (NONE, LOW, MEDIUM, HIGH, CRITICAL) |
services.cluster | string | ECS cluster name |
services.total_desired | int | Total desired task count across all 4 services |
services.total_running | int | Total running task count across all 4 services |
services.total_pending | int | Total pending task count (non-zero during deployments) |
services.all_healthy | bool | true when all services are ACTIVE with running == desired and pending == 0 |
services.services[] | array | Per-service breakdown — name, status, desired, running, pending, healthy, task_family |
translation.mtd_spend_usd | float | Month-to-date Bedrock spend (NEW — added alongside the daily figures, since the daily counters reset every 24h and previously gave no month-level view) |
translation.mtd_infra_usd | float | Month-to-date infra cost attributed to translation NEW |
translation.mtd_cost_usd | float | Month-to-date total cost (spend + infra) NEW |
translation.mtd_input_tokens | int | Month-to-date input tokens consumed NEW |
translation.mtd_output_tokens | int | Month-to-date output tokens produced NEW |
translation.daily_spend_usd / daily_infra_usd / daily_cost_usd | float | Today's equivalents, reset every 24h against the $600/day cap |
translation.daily_input_tokens / daily_output_tokens | int | Today's token counts, reset every 24h against the 50M/day cap |
translation.active_jobs / queue_depth | int | Currently running jobs and SQS queue depth |
translation.cost_per_pair_usd | float | Live average cost per language pair |
webhook_engine.delivery_active | bool | Whether the outbound Webhook Push delivery engine is running NEW |
webhook_engine.test_receiver | object | Health of the inbound test receiver (added 2026-08-07) — enabled, plus per-transport https/udp objects each with host, port, target_group, healthy_hosts, healthy NEW |
curl https://api.cpmp-site.org/public/infrastructure
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/public/infrastructure] [200]",
"status_code": 200,
"endpoint": "/public/infrastructure",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "en",
"api_key_id": "",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbi/v1",
"timestamp": "2026-05-30T17:00:00Z",
"data": {
"cloudfront": { "requests_24h": 1095, "bytes_24h": 17114513, "bytes_human": "16 MB" },
"waf": { "alb_blocked_24h": 1186, "alb_allowed_24h": 3302, "cf_blocked_24h": 0 },
"sqs": { "visible": 0, "in_flight": 0 },
"lambda": {
"receipt": { "state": "Active", "last_deployed": "", "invocations_24h": 0, "errors_24h": 0 },
"writer": { "state": "Active", "last_deployed": "", "invocations_24h": 141, "errors_24h": 0 }
},
"sync": {
"last_run": "May 30 01:00AM",
"status": "success",
"duration": "783.351423ms",
"new_logs": "154 new logs loaded",
"search_index": { "languages": 12, "sections": 3150 }
},
"guardduty": { "active_findings": 0, "status": "active" },
"alarms": { "total": 17, "ok": 16, "alarm": 1, "insufficient_data": 0 },
"honeypot": { "active": true, "endpoints": 12, "total_hits": 22, "auto_blocked": 77, "pending_queue": 0 },
"autoops": {
"active": true,
"lambdas": {
"notify_invocations_24h": 14,
"self_heal_invocations_24h": 61,
"waf_action_invocations_24h": 3,
"honeypot_processor_invocations_24h": 288,
"bedrock_analyze_invocations_24h": 288,
"rhema_support_invocations_24h": 11,
"digest_invocations_24h": 1,
"total_invocations_24h": 666,
"total_errors_24h": 0
},
"step_functions": { "executions_24h": 62, "succeeded_24h": 62, "failed_24h": 0 },
"eventbridge": {
"rules": [
{ "name": "tbi-ops-alarm-trigger", "schedule": "", "pattern": "CloudWatch alarm → ALARM", "target": "Step Function: health-check-heal" },
{ "name": "tbi-ops-honeypot-queue-processor", "schedule": "rate(5 minutes)", "pattern": "", "target": "Lambda: honeypot-processor" }
],
"total": 6
},
"waf_blocked_by_ops": 0,
"self_heal_count": 0,
"last_threat": "LOW"
},
"cache": {
"engine": "valkey",
"version": "7.2.6",
"items": 38675,
"hit_rate": 65.7,
"memory_used": "1.42G",
"memory_percent": 0.0,
"cpu_percent": 3.0
},
"services": {
"cluster": "trinity-beast-fargate-cluster",
"total_desired": 4,
"total_running": 4,
"total_pending": 0,
"all_healthy": true,
"services": [
{ "name": "trinity-beast-main-service", "status": "ACTIVE", "desired": 1, "running": 1, "pending": 0, "healthy": true, "task_family": "trinity-beast-main-task" }
]
},
"translation": {
"active": true,
"daily_spend_usd": 0,
"daily_limit_usd": 600,
"active_jobs": 0,
"queue_depth": 0,
"cost_per_pair_usd": 1.65,
"daily_input_tokens": 0,
"daily_output_tokens": 0,
"daily_token_limit": 50000000
}
},
"error": ""
}
The services.services[] array contains one object per Fargate service (4 total — abbreviated to one above). Each eventbridge.rules[] entry always carries both schedule and pattern; the field that does not apply to a given rule is an empty string, never omitted — consistent with the UME "no omitempty" rule.
All admin endpoints require the X-Admin-Key header. The admin key is provisioned in AWS Secrets Manager (trinity-beast-admin-secrets) and nowhere else. If no key is loaded, every admin endpoint refuses the request.
X-Admin-Key: YOUR_ADMIN_KEY
Endpoints for managing application configuration, demo mode, system profiles, and parameter reloading.
| Method | Path | Description |
|---|---|---|
GET | /admin/config | View all application parameters and their current values |
GET | /admin/setup-demo | Create or reset the demo API key for the live demo page |
GET | /admin/invalidate-key | Invalidate a cached API key in ElastiCache (forces re-fetch from Aurora) |
GET | /admin/demo-mode | Toggle between demo mode (synthetic prices) and performance mode (live feeds) |
GET | /admin/system-mode | Switch the active system profile (e.g., production, maintenance, demo) |
GET | /admin/reload-params | Force an immediate reload of application parameters from the database |
GET | /admin/profiles | List all available system profiles and their configurations |
Real-time observability into The Trinity Beast runtime performance, price feed health, and adaptive governor behavior.
| Method | Path | Description |
|---|---|---|
GET | /admin/stress-stats | 24-counter real-time metrics: requests, errors, cache hits, latency percentiles, goroutines, memory |
GET | /admin/stress-reset | Reset all runtime metrics counters to zero |
GET | /admin/cluster-stats | Aggregated metrics across all ECS containers in the cluster |
GET | /admin/feed-status | WebSocket and REST feed status for all 6 exchanges (Coinbase, Bitstamp, Kraken, Gate.io, Crypto.com, OKX) |
GET | /admin/usage-analytics | Aggregated usage log analytics with breakdowns by asset, source, and time period |
GET | /admin/adaptive-stats | Adaptive governor statistics — current thresholds, adjustment history, and feed health scores |
GET | /admin/db-insights | PostgreSQL self-observability — buffer-cache hit ratio, top queries by total time and call count (from pg_stat_statements), and per-table sequential-vs-index scan health |
POST | /admin/db-insights/reset | Reset the pg_stat_statements baseline so a clean measurement window can begin (e.g., before/after an optimization A/B) |
Full CRUD for exchange feed configurations and asset symbol mappings. Changes take effect immediately — no restart required.
| Method | Path | Description |
|---|---|---|
GET | /admin/exchange-feeds | List all exchange configurations with connection status |
POST | /admin/exchange-feeds/save | Create or update an exchange configuration |
POST | /admin/exchange-feeds/toggle | Enable or disable an exchange without removing its configuration |
DELETE | /admin/exchange-feeds | Permanently remove an exchange configuration |
GET | /admin/exchange-assets | List all asset symbol mappings across exchanges. Returns id, asset, exchange_name, exchange_symbol, enabled, prewarm, category, icon_url. Optional ?exchange=coinbase filter. |
POST | /admin/exchange-assets/save | Create or update asset symbol mappings for an exchange. Accepts enabled, prewarm, and category per asset. |
POST | /admin/exchange-assets/toggle | Flip enabled and/or prewarm on a single asset row. Only the fields provided are updated. Body: {"exchange_name":"okx","asset":"BANANA","prewarm":false} |
DELETE | /admin/exchange-assets | Remove asset symbol mappings. ?exchange=bitstamp&asset=APE removes one; ?exchange=bitstamp removes all for that exchange. |
POST | /admin/eam/sync | Sync EAM → application_parameters. Writes per-exchange CSV params and master prewarm_assets_list from all enabled=true, prewarm=true rows. Follow with /admin/force-deploy-params to activate. |
GET | /admin/eam/audit | Start a 4-layer prewarm audit in the background (async). Returns 202 immediately. Layers: live exchange API prices, EAM ↔ param drift, master list parity, LPO smoke test. Results cached in Valkey 24h. |
GET | /admin/eam/audit?poll=1 | Poll for audit results. Returns {"ready":false} while running, {"ready":true,"result":{...}} when complete. |
POST | /admin/eam/discover | Start async discovery of uncovered assets for an exchange, ranked by 24h volume. Body: {"exchange":"okx","limit":18}. Returns 202 immediately. Results cached in Valkey 10 min. |
GET | /admin/eam/discover?exchange=okx&poll=1 | Poll for discovery results. Each candidate includes asset, exchange_symbol, price, volume_24h, category (auto-assigned via CoinGecko), and icon_url. Result also contains a grouped object (candidates keyed by category), covered_total, exchange_total, uncovered, and scored. |
GET /asset-categories — Response ShapePublic endpoint, no auth. Returns all enabled assets grouped by category with icon URLs. Cache: 5 min.
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/asset-categories] [200]",
"status_code": 200,
"data": {
"categories": [
{
"name": "Major Currencies",
"description": "Top market cap assets",
"display_order": 1,
"assets": ["BTC", "ETH", "SOL", "XRP", "ADA"],
"icons": {
"BTC": "https://coin-images.coingecko.com/coins/images/1/large/bitcoin.png",
"ETH": "https://coin-images.coingecko.com/coins/images/279/large/ethereum.png",
"SOL": "https://coin-images.coingecko.com/coins/images/4128/large/solana.png"
}
},
{
"name": "DeFi",
"description": "Decentralized finance protocols",
"display_order": 2,
"assets": ["UNI", "AAVE", "MKR", "CRV"],
"icons": {
"UNI": "https://coin-images.coingecko.com/coins/images/12504/large/uni.png",
"AAVE": "https://coin-images.coingecko.com/coins/images/12645/large/aave.png"
}
}
],
"total_assets": 357
},
"error": ""
}
icons map only contains entries for assets that have an icon_url in EAM (~78% coverage from CoinGecko top 750). Assets without an icon are still listed in the assets array — the frontend falls back to rotating geometric SVGs (gold/blue/purple).
GET /asset-categories?full=1 — Uncapped ModeBy default each category is trimmed to 9 assets (ranked by 30-day request volume from usage_logs, alphabetical tiebreak) to keep the demo dropdown compact. Passing full=1 skips the trim entirely and returns every enabled asset in every category — the mode the Webhook and Partner dashboard asset pickers use, since a Professional or Enterprise subscriber needs to see the whole pool, not just the top 9 per category. Cache is 15 minutes in this mode (vs 5 minutes capped) since the full response is larger and changes less often.
GET /asset-names — Response ShapePublic endpoint, no auth. Returns the human-readable display name for every asset symbol, sourced from exchange_asset_map.display_name (backfilled from CoinGecko). Used by the dashboard asset pickers to show a name on hover (e.g. hovering BTC shows "Bitcoin").
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/asset-names] [200]",
"status_code": 200,
"data": {
"names": {
"BTC": "Bitcoin",
"ETH": "Ethereum",
"SOL": "Solana"
}
},
"error": ""
}
POST /admin/eam/discover — Response Shape (poll result){
"ready": true,
"result": {
"exchange": "okx",
"candidates": [
{
"asset": "MON",
"exchange_symbol": "MON-USDT",
"price": 0.0237,
"volume_24h": 21862.50,
"exchange": "okx",
"category": "Meme Coins",
"icon_url": "https://coin-images.coingecko.com/coins/images/..."
}
],
"grouped": {
"Meme Coins": [{"asset": "MON", "...": "..."}],
"DeFi": [{"asset": "SPELL", "...": "..."}],
"Infrastructure": [{"asset": "FLUX", "...": "..."}]
},
"covered_total": 171,
"exchange_total": 400,
"uncovered": 229,
"scored": 40
}
}
Direct database and cache access for administrative operations. These endpoints execute commands against Aurora Serverless v2 and ElastiCache.
| Method | Path | Description |
|---|---|---|
POST | /admin/sql | Execute a single SQL statement (SELECT, INSERT, UPDATE, DELETE) |
POST | /admin/sql-batch | Execute multiple SQL statements in sequence (DDL allowed — CREATE TABLE, ALTER, etc.) |
POST | /admin/valkey | Execute ElastiCache (Valkey) commands — GET, SET, DEL, KEYS, HGETALL, etc. |
/admin/sql, /admin/sql-batch, and /admin/valkey endpoints execute commands directly against production databases. Use with care.
/admin/sql: The endpoint accepts an optional args array for safe parameter binding. When present, the query uses PostgreSQL placeholders ($1, $2, …) bound to the array values instead of string interpolation. The parameter is backward-compatible — omitting args preserves the exact prior behavior.
{
"query": "SELECT COUNT(*) AS count FROM usage_logs WHERE asset = $1",
"args": ["BTC"],
"mode": "read"
}
| Method | Path | Description |
|---|---|---|
GET | /support/admin/stats | Ticket statistics — open, closed, average response time |
GET | /support/admin/tickets | List all support tickets with filtering and pagination |
GET | /support/admin/tickets/{id} | View full ticket detail including all replies |
POST | /support/admin/tickets/{id} | Update ticket status, priority, or assignment |
POST | /support/admin/tickets/{id}/reply | Add an admin reply to a ticket (triggers email notification via SES) |
| Method | Path | Description |
|---|---|---|
GET | /admin/partner-applications | List all partner applications with status filtering |
POST | /admin/partner-applications/{id}/status | Approve, reject, or request more info on a partner application |
Full newsletter lifecycle management — templates, composition, subscriber management, and sending via SES.
| Method | Path | Description |
|---|---|---|
GET | /admin/newsletter/templates | List all newsletter templates |
GET | /admin/newsletter/templates/{id} | Get a specific template by ID |
POST | /admin/newsletter/templates | Create or update a newsletter template |
DELETE | /admin/newsletter/templates/{id} | Delete a newsletter template |
GET | /admin/newsletter/newsletters | List all newsletters with status |
GET | /admin/newsletter/newsletters/{id} | Get a specific newsletter by ID |
POST | /admin/newsletter/newsletters | Create or update a newsletter |
POST | /admin/newsletter/newsletters/{id}/status | Toggle newsletter active/inactive status |
POST | /admin/newsletter/newsletters/{id}/send | Send newsletter to all active subscribers via SES |
GET | /admin/newsletter/subscribers | List all newsletter subscribers with status |
Direct email composition and sending — templates, verified senders, draft management, and delivery via SES.
| Method | Path | Description |
|---|---|---|
GET | /admin/email/templates | List all email templates |
GET | /admin/email/templates/{id} | Get a specific email template |
POST | /admin/email/templates | Create or update an email template |
DELETE | /admin/email/templates/{id} | Delete an email template |
GET | /admin/email/senders | List verified sender identities |
POST | /admin/email/senders | Add or update a sender identity |
DELETE | /admin/email/senders/{id} | Remove a sender identity |
GET | /admin/email/drafts | List all email drafts |
GET | /admin/email/drafts/{id} | Get a specific draft |
POST | /admin/email/drafts | Create or update an email draft |
DELETE | /admin/email/drafts/{id} | Delete an email draft |
POST | /admin/email/drafts/{id}/send | Send an email draft via SES |
Diagnostic endpoints for verifying email rendering without triggering real payment processing or translation jobs.
| Method | Path | Description |
|---|---|---|
POST | /admin/test-refund-email | Send a test refund confirmation email with dummy data. Supports type: translation or subscription. Supports lang: en, es, pt, fr, de, ru, hi, ur, ar, ja, zh, it. |
POST | /admin/test-finalize-email | Send a test translation completion notification email with mock data. Verifies the per-language cost breakdown table (sqsLangDetails) renders correctly. Three scenarios: succeeded, partial, failed. No Bedrock calls, no translation jobs — SES only. |
{
"email": "recipient@example.com",
"lang": "ur",
"type": "translation"
}
{
"status": "sent",
"email": "recipient@example.com",
"lang": "ur",
"type": "translation",
"subject": "رقم کی واپسی کی تصدیق — Trinity-Beast-API-Reference.html"
}
Sends a mock translation completion email with realistic dummy data. The email includes the full stats bar (documents, languages, succeeded/failed counts, duration, cost/fee/total, state badge) and the Cost by Language table showing per-language Bedrock token usage and cost. Subject line is prefixed with [TEST] and a purple banner at the top makes it immediately clear this is not a real alert.
{
"email": "recipient@example.com",
"scenario": "succeeded"
}
// scenario values:
// "succeeded" — 2 docs × 11 langs, all 22 pairs succeeded, full cost breakdown table
// "partial" — 2 docs × 11 langs, 16/22 pairs succeeded, 3 failed pairs listed, partial cost table
// "failed" — 1 doc × 3 langs, all failed, no cost table, CRITICAL badge
{
"status": "sent",
"email": "recipient@example.com",
"scenario": "partial",
"subject": "[TEST] Translation Complete: 2 docs × 11 langs — 16/22 pairs PARTIAL"
}
Internal admin endpoints for the document translation pipeline. All require the X-Admin-Key header. Full documentation: TBI Translation Engine.
| Method | Path | Description |
|---|---|---|
POST | /admin/translate | Submit a translation job. Accepts docs (array, max 42; more answers 400 translate_max_docs), langs ("all" for the internal 11, or an array of up to 12 codes, each resolved through the catalog and its alternate codes and de-duplicated; an unknown code answers 400 translate_langs_invalid), model, options (force, delta, skip_search_rebuild, skip_validation), protected_terms. Enforces $600/day dollar cap and 50M token/day cap. Returns job_id. |
| Method | Path | Description |
|---|---|---|
GET | /admin/translate/status/{job_id} | Full job state — Valkey primary (live progress), Aurora fallback (completed jobs) |
GET | /admin/translate/queue | All active and queued jobs |
GET | /admin/translate/history | Last 50 completed jobs from Aurora |
GET | /admin/translate/health | System health — queue depth, active jobs, daily spend, daily token counts (input/output/limit), last completed job |
| Method | Path | Description |
|---|---|---|
POST | /admin/translate/cancel/{job_id} | Stop any running job. Each language finishes the section it is on and stops there, so every page already delivered stays live and nothing is left half-written. The same cancel path the customer uses (see 15.7) |
POST | /admin/translate/refund-approve/{quote_id} | Issue the refund calculated for a customer-cancelled quote. Moves refund_status from pending_approval to approved, and claims the quote first so a second click can never refund twice (see 15.7) |
POST | /admin/translate/retry-failed/{job_id} | Re-submit only the failed pairs from a previous job, on the same model as the original job. On a customer job it goes through the same one-free-retry claim as the customer (15.10), so it can never reopen a pair that was already refunded |
POST | /admin/translate/refund-sweep | Run the failed-pair refund sweeper now instead of waiting for its 15-minute tick. Returns {opened, closed_none, due, issued, failed, errors}. 409 translate_refund_sweep_busy when another container is sweeping; 503 translate_refund_sweep_failed when the sweep couldn't run |
The submit handler enforces two independent daily caps. Either can trigger a 429 on /admin/translate. For customer accepts (15.5, 15.6), the kill switch and both caps are also checked before the charge, and a tripped guard answers 503 tbts_service_paused with nothing charged, so no customer ever pays for a job a guard would stop:
| Cap | Valkey Key | Limit | Reset |
|---|---|---|---|
| Dollar cap | autoops:bedrock:spend:daily | $600/day | 24h TTL auto-reset |
| Token cap | autoops:bedrock:tokens:input:daily + autoops:bedrock:tokens:output:daily | 50M combined tokens/day | 24h TTL auto-reset |
Emergency kill switch: SET autoops:bedrock:kill 1 in Valkey — halts all translation operations immediately.
Gates /translate/quote, accept and admin submit. The catalog holds 127 languages: the 75 rows of Amazon Translate's published list joined with the 119 languages Qwen 3 publishes. Input codes are 2–12 characters (a primary code, an optional script, an optional region), and 36 alternate codes such as nb, zh-Hant and ar-EG are converted silently. A code that has a valid shape but is not in the catalog is rejected before any document analysis, pricing or charge. See architecture.md for the full incident this closes.
| Method | Path | Description |
|---|---|---|
GET | /admin/translate/languages | List all supported languages. Optional ?status=supported|blocked|unverified filter. Returns Aurora rows plus live Valkey values for drift visibility. |
POST | /admin/translate/languages | Upsert a language. Body: {"lang_code":"xx","status":"blocked","notes":"reason","updated_by":"cory"}; status is always required. Also accepts native_name, script, rtl, word_spaced, base_lang, tuning_lang, target_allowed and default_model. A new code requires english_name, native_name, script and tuning_lang (400 translate_language_fields_required; recorded with source='manual'). An invalid default_model answers 400 translate_language_model_invalid. The response includes rtl and word_spaced. |
POST | /admin/translate/languages/reload | Flush the catalog keys in Valkey, reload from Aurora in place right away, and return source (where the reload read from). |
Valkey cache, two keys: tx:supported_langs (hash, lang_code → status, synced nightly by BeastReconciler) and tx:lang_catalog (the full catalog as JSON, TTL 27 hours). Every LPO container also refreshes its catalog in place every 15 minutes (timer txlangs_refresh), so a change reaches every container without a deploy. If Aurora and both Valkey keys are unreachable the catalog is empty and a quote answers 503 tbts_langs_unavailable — it fails closed, and nothing is ever charged for a language we can't confirm.
The dashboard exposes 8 support endpoints split into two groups: Customer (authenticated via Bearer token, scoped to the user's own tickets) and Admin (Bearer token + admin role, system-wide access). All responses use the standard 12-field UME envelope.
These endpoints are scoped to the authenticated user's email. Customers can only see and interact with their own tickets. Internal admin notes are never exposed.
GET /dashboard/api/support/ticketsList the authenticated user's support tickets, ordered by most recent. Limited to 50.
Authentication: Bearer token (session cookie)
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/dashboard/api/support/tickets] [200]",
"status_code": 200,
"endpoint": "/dashboard/api/support/tickets",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "en",
"api_key_id": "ak_demo123",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbi-dashboard/v1",
"timestamp": "2026-05-17T19:00:00Z",
"data": {
"tickets": [
{
"ticket_number": "TKT-20260517-A3F2",
"subject": "API key not working after renewal",
"status": "open",
"created_at": "2026-05-17T14:30:00Z"
}
]
},
"error": ""
}
GET /dashboard/api/support/tickets/{ticket_number}Full ticket detail with reply thread. Excludes internal admin notes. Shows translated replies in the customer's language.
Authentication: Bearer token (session cookie). Must own the ticket.
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/dashboard/api/support/tickets/TKT-20260517-A3F2] [200]",
"status_code": 200,
"endpoint": "/dashboard/api/support/tickets/TKT-20260517-A3F2",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "en",
"api_key_id": "ak_demo123",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbi-dashboard/v1",
"timestamp": "2026-05-17T19:00:00Z",
"data": {
"ticket": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"ticket_number": "TKT-20260517-A3F2",
"name": "Jane Developer",
"category": "technical",
"subject": "API key not working after renewal",
"message": "I renewed my subscription yesterday but my API key returns 403...",
"status": "open",
"preferred_lang": "en",
"created_at": "2026-05-17T14:30:00Z",
"updated_at": "2026-05-17T15:45:00Z"
},
"replies": [
{
"id": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
"author_type": "admin",
"message": "I've checked your key — it looks like the cache hadn't refreshed yet. Should be working now.",
"created_at": "2026-05-17T15:45:00Z"
}
]
},
"error": ""
}
POST /dashboard/api/support/tickets/{ticket_number}/replyPost a customer reply. Max 10,000 characters. Auto-translates to English for admin readability. Notifies admin via email. Re-opens tickets in awaiting_customer or resolved state.
Authentication: Bearer token (session cookie). Must own the ticket.
Request body:
{
"message": "Thanks, it's working now! One more question — can I add more assets to my plan?"
}
Response:
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/dashboard/api/support/tickets/TKT-20260517-A3F2/reply] [200]",
"status_code": 200,
"endpoint": "/dashboard/api/support/tickets/TKT-20260517-A3F2/reply",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "en",
"api_key_id": "ak_demo123",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbi-dashboard/v1",
"timestamp": "2026-05-17T19:10:00Z",
"data": {
"reply_id": "c4d5e6f7-a8b9-0123-4567-890abcdef123",
"message": "Reply posted"
},
"error": ""
}
Errors: 400 if message is empty or exceeds 10,000 chars. 400 if ticket is closed. 404 if ticket not found or not owned by user.
POST /dashboard/api/support/tickets/{ticket_number}/resolveMark own ticket as resolved. Notifies admin. Closed tickets cannot be resolved (already terminal). Idempotent — resolving an already-resolved ticket returns success.
Authentication: Bearer token (session cookie). Must own the ticket.
Request body: Empty {}
Response:
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/dashboard/api/support/tickets/TKT-20260517-A3F2/resolve] [200]",
"status_code": 200,
"endpoint": "/dashboard/api/support/tickets/TKT-20260517-A3F2/resolve",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "en",
"api_key_id": "ak_demo123",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbi-dashboard/v1",
"timestamp": "2026-05-17T19:15:00Z",
"data": {
"message": "Ticket marked resolved",
"status": "resolved"
},
"error": ""
}
System-wide ticket management. Admin can see all tickets, all replies (including internal notes), customer IPs, and full audit trail. Requires admin role on the session.
GET /dashboard/api/admin/support/ticketsList all tickets system-wide. Supports filtering by status and category. Limited to 200 results, ordered by created_at DESC.
Authentication: Bearer token + admin role
Query parameters:
| Param | Type | Description |
|---|---|---|
status | string | Filter by status: new, open, in_progress, awaiting_customer, resolved, closed |
category | string | Filter by category: billing, technical, api-access, rate-limit, feature-request, bug-report, account, general |
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/dashboard/api/admin/support/tickets] [200]",
"status_code": 200,
"endpoint": "/dashboard/api/admin/support/tickets",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "en",
"api_key_id": "ak_demo123",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbi-dashboard/v1",
"timestamp": "2026-05-17T19:00:00Z",
"data": {
"tickets": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"ticket_number": "TKT-20260517-A3F2",
"name": "Jane Developer",
"email": "jane@example.com",
"category": "technical",
"subject": "API key not working after renewal",
"status": "open",
"created_at": "2026-05-17T14:30:00Z",
"updated_at": "2026-05-17T15:45:00Z"
}
]
},
"error": ""
}
GET /dashboard/api/admin/support/tickets/{id}Full ticket detail by UUID. Includes all replies (internal notes visible), customer IP, email, preferred language, and English translation of the original message.
Authentication: Bearer token + admin role
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/dashboard/api/admin/support/tickets/a1b2c3d4-e5f6-7890-abcd-ef1234567890] [200]",
"status_code": 200,
"endpoint": "/dashboard/api/admin/support/tickets/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "en",
"api_key_id": "ak_demo123",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbi-dashboard/v1",
"timestamp": "2026-05-17T19:00:00Z",
"data": {
"ticket": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"ticket_number": "TKT-20260517-A3F2",
"name": "Jane Developer",
"email": "jane@example.com",
"category": "technical",
"subject": "API key not working after renewal",
"message": "I renewed my subscription yesterday but my API key returns 403...",
"message_en": "I renewed my subscription yesterday but my API key returns 403...",
"preferred_lang": "en",
"status": "open",
"client_ip": "203.0.113.50",
"created_at": "2026-05-17T14:30:00Z",
"updated_at": "2026-05-17T15:45:00Z"
},
"replies": [
{
"id": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
"author": "CoryDeanKalani@CPMP-Site.org",
"author_type": "admin",
"message": "I've checked your key — it looks like the cache hadn't refreshed yet. Should be working now.",
"is_internal": false,
"created_at": "2026-05-17T15:45:00Z"
},
{
"id": "d5e6f7a8-b9c0-1234-5678-90abcdef1234",
"author": "CoryDeanKalani@CPMP-Site.org",
"author_type": "admin",
"message": "Note: key cache was stale due to recent deploy. Forced invalidation.",
"is_internal": true,
"created_at": "2026-05-17T15:44:00Z"
}
]
},
"error": ""
}
POST /dashboard/api/admin/support/tickets/{id}/replyPost an admin reply. Auto-translates to customer's language and emails them. Includes a one-click resolve link in the email (single-use, 30-day TTL). Internal notes (is_internal: true) are never shown to the customer or emailed. First reply auto-advances status from new to open.
Authentication: Bearer token + admin role
Request body:
{
"message": "I've checked your key — it looks like the cache hadn't refreshed yet. Should be working now.",
"is_internal": false
}
Response:
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/dashboard/api/admin/support/tickets/a1b2c3d4/reply] [200]",
"status_code": 200,
"endpoint": "/dashboard/api/admin/support/tickets/a1b2c3d4/reply",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "en",
"api_key_id": "ak_demo123",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbi-dashboard/v1",
"timestamp": "2026-05-17T19:10:00Z",
"data": {
"reply_id": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
"message": "Reply posted"
},
"error": ""
}
Errors: 400 if message is empty. 404 if ticket not found.
POST /dashboard/api/admin/support/tickets/{id}/statusUpdate a ticket's status. Audit-logged with the admin's email. Valid statuses: new, open, in_progress, awaiting_customer, resolved, closed.
Authentication: Bearer token + admin role
Request body:
{
"status": "in_progress"
}
Response:
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/dashboard/api/admin/support/tickets/a1b2c3d4/status] [200]",
"status_code": 200,
"endpoint": "/dashboard/api/admin/support/tickets/a1b2c3d4/status",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "en",
"api_key_id": "ak_demo123",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbi-dashboard/v1",
"timestamp": "2026-05-17T19:12:00Z",
"data": {
"message": "Status updated",
"status": "in_progress"
},
"error": ""
}
Errors: 400 if status is invalid. 404 if ticket not found.
Tickets follow a lifecycle with automatic transitions:
| Transition | Trigger |
|---|---|
new → open | First admin reply |
awaiting_customer → open | Customer replies |
resolved → open | Customer replies (re-opens) |
Any → resolved | Customer clicks resolve (dashboard or email link) |
| Any → any valid status | Admin sets status manually |
Note: Closed tickets cannot be replied to by customers. They must open a new ticket.
GET /dashboard/api/admin/daily-rollupReturns per-day usage aggregates served from the mv_usage_daily materialized view in Aurora. Because the data is precomputed, this endpoint is fast and never scans the full usage_logs table. The view is refreshed every 5 minutes by the pg_cron job tbi-mv-usage-daily-refresh running natively inside Aurora; the last_refreshed field tells consumers how current the data is.
Authentication: Bearer token + admin role
| Parameter | Type | Description |
|---|---|---|
days | int | Number of days to return. Default 30, maximum 93 (matches the usage-log retention window). |
Response:
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/dashboard/api/admin/daily-rollup] [200]",
"status_code": 200,
"endpoint": "/dashboard/api/admin/daily-rollup",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "en",
"api_key_id": "ak_demo123",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbi-dashboard/v1",
"timestamp": "2026-05-30T23:30:00Z",
"data": {
"days": [
{
"day": "2026-05-30",
"total_requests": 4821,
"avg_latency_ms": 12.4,
"p50_latency_ms": 9,
"p95_latency_ms": 38,
"p99_latency_ms": 71,
"cached_requests": 4603,
"cache_hit_rate_pct": 95.48,
"distinct_assets": 142,
"distinct_api_keys": 6,
"distinct_sources": 6,
"distinct_nodes": 3
}
],
"count": 1,
"last_refreshed": "2026-05-30T23:26:00Z"
},
"error": ""
}
Notes: days is always an array (empty [] when no data, never null). last_refreshed reflects the most recent materialized-view refresh, not the request time.
AI-powered document translation with sentinel preprocessing and multi-layer validation. Translates technical documentation while preserving code blocks, Mermaid diagrams, brand terms and every technical identifier, into and out of any of 127 languages. Six AI agents are available: five at the same flat $3.00 per document-language pair, and Claude Opus 4.8 priced at its actual cost with a $3.00 floor. The engine is agent-agnostic, so any model reachable through Amazon Bedrock can serve as an agent without code changes.
The flow is quote → accept → follow the status → download. Along the way you can cancel any time before a job finishes, retry failed pairs once at no charge, and you are refunded automatically for anything that still can't be delivered. Every promise in that sentence is backed by an endpoint below, so an integration can do everything the dashboard can. The Translation Service document walks through it end to end.
Authentication: every /translate/* endpoint except GET /translate/models and GET /translate/languages (both public) needs a Translation API key, sent as the X-API-Key header, as Authorization: Bearer, or as the ?api_key= query parameter. A Prices-only key is refused with 403, so translation spend is always tied to a translation account.
The limits, from one place in the code: up to 12 languages and 42 documents per quote, each document up to 300 KB (307,200 bytes), up to 9 quotes per batch, and 72 hours to accept a quote. The quote, the accept, the admin submit and the dashboard all read these from the same constants, so a quote can never promise something the job would refuse. GET /translate/languages returns them live in limits.
GET /translate/modelsLists the six translation agents. Public — no authentication required. Use an id as the model in a quote; leave model out and you get Auto, the best agent per language: the agent named in default (Qwen 3 235B) for every language it supports, and Claude Sonnet 4.6 only for Amharic, Hausa, Mongolian, Pashto and Somali, at the same price. Naming an agent overrides Auto for every language, retries included.
{
"status_code": 200,
"data": {
"models": [
{ "id": "qwen3-235b", "name": "Qwen 3 235B", "tier": "Fast & Capable", "input_rate_per_1m": 0.22, "output_rate_per_1m": 0.88, "estimated_cost_per_pair": 0.019294, "speed": "fast", "quality": "good", "description": "Fast and capable. Excellent with Chinese, Japanese, and Korean. The default choice." },
{ "id": "deepseek-v3", "name": "DeepSeek V3", "tier": "Fast & Capable", "speed": "fast", "quality": "good", "description": "Fast and capable. Strong with European languages and technical content." },
{ "id": "mistral-large", "name": "Mistral Large", "tier": "Fast & Capable", "speed": "fast", "quality": "good", "description": "Fast and capable. Native-quality Romance languages (French, Spanish, Portuguese, Italian)." },
{ "id": "claude-haiku-5.5", "name": "Claude Haiku 5.5", "tier": "Fast & Capable", "speed": "fast", "quality": "good", "description": "Fast and capable. Versatile all-rounder, great for high-volume jobs." },
{ "id": "claude-sonnet-4.6", "name": "Claude Sonnet 4.6", "tier": "Deep Reasoning", "speed": "moderate", "quality": "excellent", "description": "Deep reasoning, nuanced output. Handles complex scripts (Arabic, Hindi, Japanese) and technical documents well." },
{ "id": "claude-opus-4.8", "name": "Claude Opus 4.8", "tier": "Maximum Fidelity", "input_rate_per_1m": 5.50, "output_rate_per_1m": 27.50, "estimated_cost_per_pair": 0.5731, "speed": "slow", "quality": "exceptional", "description": "Highest quality, comprehensive translation. Best for critical documents, nuanced content, and languages with complex grammar." }
],
"default": "qwen3-235b",
"pricing_note": "Rates are per 1M tokens. estimated_cost_per_pair is for a typical 50KB document (one language). Actual cost depends on document size and content density."
}
}
Every agent entry carries all nine fields; the first and the Claude Opus 4.8 entry are shown in full and the rest are abbreviated here for width. Opus 4.8's rates (5.50 / 27.50 USD per million tokens, refreshed nightly from the AWS Price List) put a typical 50 KB document at about $0.57 of our cost, so it is priced at the $3.00 floor until a document reaches about 200 KB. The earlier agent IDs claude-haiku-3.5 and claude-opus-4 are still accepted everywhere a model is taken, and are converted quietly to claude-haiku-5.5 and claude-opus-4.8, so an older integration never breaks.
| Field | Type | Description |
|---|---|---|
input_rate_per_1m | float | Our Bedrock cost per 1 million input tokens (USD), read from tx:params, which the nightly sync job refreshes from the AWS Price List |
output_rate_per_1m | float | Our Bedrock cost per 1 million output tokens (USD) |
estimated_cost_per_pair | float | Our estimated Bedrock cost for one language on a typical 50 KB document |
Our cost, not your price: the rates are published for transparency. Your price is the flat $3.00 per document-language pair on five of the six agents, and Claude Opus 4.8 is priced at its actual cost with a $3.00 floor, so a larger document can cost more per pair. The quote always shows the exact amount.
POST /translate/quoteSend document URLs and target languages and receive a fixed price. The engine fetches and analyzes each document (size, sections, code blocks, diagrams), resolves every language against the 127-language catalog (alternate codes such as nb or zh-Hant are converted quietly), works out each document's source language, and refuses a target that equals the source. Nothing is charged at this step.
{
"docs": ["https://your-site.com/docs/api-reference.html", "https://your-site.com/docs/faq.html"],
"langs": ["es", "fr", "de", "ja", "zh-TW"],
"source_lang": "en",
"source_langs": { "https://your-site.com/docs/faq.html": "it" },
"model": "qwen3-235b"
}
| Field | Type | Required | Description |
|---|---|---|---|
docs | string[] | Yes | Public http:// or https:// URLs of the HTML documents. Each must answer 200 and be 300 KB (307,200 bytes) or smaller. Up to 42 documents. A single doc_url string is also accepted. A document's name is the last part of its URL path, and two documents in one quote can't share a name. |
langs | string[] | Yes | Target language codes from 15.12. Up to 12, counted after alternate codes are converted and duplicates removed, so ["nb","no"] is one language. English is a target like any other. |
source_lang | string | No | The language your documents are written in. Applies to every document without its own source_langs entry. "auto", or leaving it out, means "work it out for me". Any catalog code is accepted as a source. |
source_langs | object | No | Per-document source language, keyed by the document URL exactly as you sent it in docs. Beats source_lang for that document. |
supersedes_quote_id | string | No | One of your pending quotes that this quote replaces (for example after you add a source language). It is marked superseded and named in superseded_quote_id. Any other ID is ignored and the new quote still stands. |
model | string | No | Agent ID from 15.1. Leave it out and the quote is auto-routed per language: qwen3-235b for almost every language, and claude-sonnet-4.6 for am, ha, mn, ps and so, at the same price. An agent you choose is honored for every language, in every attempt, including retries. The earlier IDs claude-haiku-3.5 and claude-opus-4 are accepted and converted quietly. |
delivery | string | No | "express", the only delivery mode and the default. |
For each document the first of these that gives an answer wins, and source_provenance records which one it was: stated (your source_langs or source_lang), declared (the page's <html lang>, with region tags reduced, so en-US is English and ar-EG is Modern Standard Arabic), detected (only a definitive detection counts: English by word frequency, and Japanese, Korean or Thai by script), otherwise auto. A declared language that a definitive detection contradicts becomes auto rather than a guess. An unsure source never fails a quote: the document gets source_lang_confirm_needed: true and a source_hint in your api_lang, and the agent translates from the document's original language as written.
{
"status_code": 200,
"data": {
"quote_id": "qt-a1b2c3d4-e5f",
"docs": [
{
"doc_url": "https://your-site.com/docs/api-reference.html",
"doc_name": "api-reference.html",
"analysis": { "size_bytes": 63488, "estimated_chunks": 4, "difficulty": "moderate", "code_blocks": 12, "diagrams": 2 },
"total_price": 15.00
},
{
"doc_url": "https://your-site.com/docs/faq.html",
"doc_name": "faq.html",
"analysis": { "size_bytes": 18944, "estimated_chunks": 2, "difficulty": "simple", "code_blocks": 0, "diagrams": 0 },
"total_price": 15.00
}
],
"doc_count": 2,
"languages": ["es", "fr", "de", "ja", "zh-TW"],
"lang_count": 5,
"total_pairs": 10,
"model": "qwen3-235b",
"auto_routed": false,
"model_by_lang": {},
"delivery": { "mode": "express", "label": "Express — real-time translation", "estimated_time": "~8 minutes", "qualified_reason": "10 translated documents (2 docs × 5 languages)" },
"pricing": { "price_per_pair": 3.00, "total_pairs": 10, "total_price": 30.00, "currency": "USD" },
"expires_at": "2026-10-13T15:00:00Z",
"accept_url": "/translate/accept/qt-a1b2c3d4-e5f",
"sources": [
{ "doc_url": "https://your-site.com/docs/api-reference.html", "doc_name": "api-reference.html", "source_lang": "en", "source_provenance": "stated", "source_lang_confirm_needed": false, "source_hint": "" },
{ "doc_url": "https://your-site.com/docs/faq.html", "doc_name": "faq.html", "source_lang": "it", "source_provenance": "stated", "source_lang_confirm_needed": false, "source_hint": "" }
],
"superseded_quote_id": ""
}
}
model_by_lang is {} when you chose an agent, and the per-language map when the quote was auto-routed (model: "per-language", auto_routed: true). Internal AI cost is never part of the response: you see the price and nothing else.
| Status | Key | Condition |
|---|---|---|
400 | tbts_langs_required / tbts_docs_required | No languages, or no documents, given |
400 | tbts_invalid_lang_code | A language code has the wrong shape (2–12 characters: a primary code, an optional script, an optional region) |
400 | tbts_unsupported_lang_code | A language code is well formed but not in the catalog (15.12) |
400 | tbts_too_many_langs / tbts_too_many_docs | More than 12 languages, or more than 42 documents |
400 | tbts_doc_url_invalid / tbts_doc_name_duplicate / tbts_doc_excluded | A URL that isn't http(s), two documents with the same name, or a URL we don't translate |
400 | tbts_doc_fetch_failed | A document couldn't be fetched, including a URL that resolves to a private or internal network address |
400 | tbts_doc_too_large | A document is larger than 300 KB. It is refused whole, never truncated, hashed or priced |
400 | tbts_source_lang_invalid / tbts_source_lang_doc_unknown | A stated source isn't a catalog code, or names a document that isn't in this quote |
400 | tbts_target_is_source | A target equals a document's source language. Variants such as es → es-MX or ar → arz are always allowed |
400 | tbts_invalid_model | Unknown agent ID |
403 | tbts_prices_only_key | The key is a Prices-only key |
503 | tbts_langs_unavailable | The language catalog couldn't be loaded. Nothing is priced or charged; try again in a moment |
Quote expiry: a quote can be accepted for 72 hours. After that, request a new one.
GET /translate/quote/{quote_id}Returns one quote with its current state: pending, accepted, expired, superseded (a newer quote replaced it; superseded_by names it, or is "" when the document now breaks a rule), refunded or partially_refunded.
{
"status_code": 200,
"data": {
"quote_id": "qt-a1b2c3d4-e5f",
"doc_url": "https://your-site.com/docs/api-reference.html",
"doc_name": "api-reference.html",
"docs": [
{ "url": "https://your-site.com/docs/api-reference.html", "name": "api-reference.html" },
{ "url": "https://your-site.com/docs/faq.html", "name": "faq.html" }
],
"doc_count": 2,
"languages": ["es", "fr", "de", "ja", "zh-TW"],
"total_pairs": 10,
"model": "qwen3-235b",
"analysis": { "size_bytes": 63488, "estimated_chunks": 4, "difficulty": "moderate", "code_blocks": 12, "diagrams": 2 },
"pricing": { "price_per_pair": 3.00, "pairs": 10, "total_price": 30.00, "currency": "USD" },
"sources": [
{ "doc_url": "https://your-site.com/docs/api-reference.html", "doc_name": "api-reference.html", "source_lang": "en", "source_provenance": "stated", "source_lang_confirm_needed": false, "source_hint": "" },
{ "doc_url": "https://your-site.com/docs/faq.html", "doc_name": "faq.html", "source_lang": "it", "source_provenance": "stated", "source_lang_confirm_needed": false, "source_hint": "" }
],
"state": "accepted",
"superseded_by": "",
"expires_at": "2026-10-13T15:00:00Z",
"accepted_at": "2026-10-10T15:04:12Z",
"job_id": "019e4b8a37bc-1f2e3d4c5b6a7980"
}
}
pricing.pairs is documents × languages, so a multi-document quote always shows its true pair count. doc_url, doc_name and analysis describe the first document and are kept for older integrations; docs lists them all.
GET /translate/quotesYour 30 most recent quotes for this API key, newest first. data is the array itself. A quote replaced by a newer one appears with state: "superseded".
{
"status_code": 200,
"data": [
{ "quote_id": "qt-a1b2c3d4-e5f", "doc_name": "api-reference.html", "lang_count": 5, "total_price": 30.00, "state": "accepted", "expires_at": "2026-10-13T15:00:00Z", "job_id": "019e4b8a37bc-1f2e3d4c5b6a7980", "model": "qwen3-235b" },
{ "quote_id": "qt-f6e5d4c3-b2a", "doc_name": "architecture-guide.html", "lang_count": 3, "total_price": 9.00, "state": "pending", "expires_at": "2026-10-13T18:00:00Z", "job_id": "", "model": "claude-sonnet-4.6" }
]
}
POST /translate/accept/{quote_id}Accepts a pending quote. No request body is needed. Before the card on file is touched, the engine claims the quote so a double-click can't charge twice, re-checks every limit, checks the service guards, and fetches every document again to compare it with what was priced. Only then is the quoted total charged and the job started.
{
"status_code": 200,
"data": {
"status": "accepted",
"quote_id": "qt-a1b2c3d4-e5f",
"job_id": "019e4b8a37bc-1f2e3d4c5b6a7980",
"model": "qwen3-235b",
"payment_id": "pi_3abc123def456",
"amount_usd": 30.00,
"status_url": "/translate/status/019e4b8a37bc-1f2e3d4c5b6a7980",
"notice": "",
"notice_key": "",
"updated_docs": [],
"key": "",
"message": "",
"refunded": false,
"refund_amount": 0
}
}
Every field is always present. When a document's readable content changed but its price and rule results did not, the job still starts at the quoted price on the current version: notice_key is tbts_quote_content_updated, notice says so in your api_lang, and updated_docs names the documents. On Claude Opus 4.8, a re-quoted document that changes again is charged at the lower of the two prices, with tbts_quote_price_held.
| Status | data.status / key | What it means |
|---|---|---|
202 | pending / tbts_translation_start_pending | You were charged and we are still confirming the job started. If it didn't, the full amount is refunded automatically. status_url is /translate/status/{quote_id}, so you can follow it either way. |
502 | refunded / tbts_translation_start_refunded | The job couldn't start, and the full amount has already been refunded (refunded: true, refund_amount). |
502 | refund_delayed / tbts_status_refund_delayed | The job couldn't start and the refund is taking longer than usual. It is retried automatically every 15 minutes and our team is alerted. |
| Status | Key | Condition |
|---|---|---|
400 | tbts_doc_too_large | A document grew past 300 KB since the quote. Nothing is charged, the quote is marked superseded and no new quote is made |
400 | tbts_target_is_source | A document now declares the same language as one of your targets. Nothing is charged and no new quote is made |
402 | tbts_payment_required / tbts_payment_failed | No payment method on file, or the payment was declined |
404 | tbts_quote_not_found | Quote not found on this API key |
409 | tbts_quote_conflict | The quote is no longer pending (the message names its state). For a superseded quote, data carries {state: "superseded", new_quote_id} |
409 | tbts_quote_accept_in_progress | Another request is accepting this quote right now. Nothing is staged or charged; wait a minute and check the quote |
409 | tbts_quote_requoted | A document changed and its price or source result changed with it. Nothing is charged; data is {old_quote_id, new_quote_id, new_total_price, changed_docs, accept_url, quote}, ready to accept |
409 | tbts_quote_needs_requote | The quote no longer passes today's checks, or a document couldn't be fetched again. Request a new quote |
410 | tbts_quote_expired_accept | Quote has expired (72 hours) — request a new one |
500 | tbts_payment_unavailable | Payment processing is temporarily unavailable. Nothing was charged |
503 | tbts_service_paused | The service's safety switch, daily spend cap or token cap is tripped. Checked before the charge, so nothing is charged or queued |
Job tracking: status_url points at GET /translate/status/{job_id} (15.8), which reports every document-language pair live, along with the ETA and any refund. The Account Dashboard shows the same data, you receive one email when the job completes, and GET /translate/download/{job_id} returns the files.
Why the second look: a quote lives for 72 hours and a web page can change in that time. The engine fingerprints only the readable content (scripts, styles, comments and tracking markup are ignored), so a page that reloads slightly differently every time is never re-priced, and a page whose words really changed is either translated at the same price with a notice or re-quoted. You never pay a price computed on other content.
POST /translate/accept-batchAccept multiple pending quotes in a single transaction. A single Stripe PaymentIntent is created for the batch total. Each quote becomes a separate translation job. Maximum 9 quotes per batch. Every quote is claimed, re-checked and re-fetched exactly as in 15.5 before the card is touched.
{
"quote_ids": [
"qt-a1b2c3d4-e5f",
"qt-b2c3d4e5-f6a",
"qt-c3d4e5f6-a7b"
]
}
| Field | Type | Required | Description |
|---|---|---|---|
quote_ids | array | Yes | Array of quote IDs to accept. Minimum 1, maximum 9. |
{
"status_code": 200,
"data": {
"status": "accepted",
"batch_size": 3,
"quote_ids": ["qt-a1b2c3d4-e5f", "qt-b2c3d4e5-f6a", "qt-c3d4e5f6-a7b"],
"task_id": "019e4b8a37bd-0a1b2c3d4e5f6a7b",
"task_status_url": "/translate/task/019e4b8a37bd-0a1b2c3d4e5f6a7b",
"jobs": [
{ "quote_id": "qt-a1b2c3d4-e5f", "doc_name": "api-reference.html", "job_id": "019e4b8a37bc-1f2e3d4c5b6a7980", "error": "", "key": "", "status_url": "/translate/status/019e4b8a37bc-1f2e3d4c5b6a7980", "refunded": false, "refund_amount": 0, "notice": "", "notice_key": "", "updated_docs": [] },
{ "quote_id": "qt-b2c3d4e5-f6a", "doc_name": "quick-start.html", "job_id": "019e4b8a37be-2c3d4e5f6a7b8c9d", "error": "", "key": "", "status_url": "/translate/status/019e4b8a37be-2c3d4e5f6a7b8c9d", "refunded": false, "refund_amount": 0, "notice": "", "notice_key": "", "updated_docs": [] },
{ "quote_id": "qt-c3d4e5f6-a7b", "doc_name": "webhooks.html", "job_id": "019e4b8a37bf-3d4e5f6a7b8c9d0e", "error": "", "key": "", "status_url": "/translate/status/019e4b8a37bf-3d4e5f6a7b8c9d0e", "refunded": false, "refund_amount": 0, "notice": "", "notice_key": "", "updated_docs": [] }
],
"payment_id": "pi_3abc123def456",
"amount_usd": 42.00,
"eta": {
"estimated_seconds": 540,
"estimated_low_seconds": 420,
"estimated_high_seconds": 660,
"estimated_formatted": "~9 minutes",
"estimated_range": "7-11 minutes",
"bottleneck_lang": "ja",
"elapsed_seconds": 0,
"remaining_seconds": 540,
"remaining_formatted": "~9 minutes",
"percent_complete": 0
},
"key": "",
"message": ""
}
}
In this example the three quotes cover 5, 3 and 6 document-language pairs: 14 pairs at the flat $3.00 is one charge of 42.00. task_status_url follows the whole batch (15.9), and each job's status_url follows that job (15.8). The eta is the slowest job in the batch, so it tells you when everything will be done.
Each jobs[] entry reports its own start, so one quote that can't start never holds the others back. A quote that couldn't start has an empty job_id, key tbts_translation_start_refunded (or tbts_status_refund_delayed, or tbts_translation_start_pending while we confirm), refunded and refund_amount for what came back, and a status_url that uses the quote ID. When no quote in the batch could start, the response is 502 with status: "start_failed" and the same jobs list, and the task still exists so GET /translate/task/{task_id} reports the refunds.
| Status | Condition |
|---|---|
400 | tbts_batch_ids_required (no quote IDs), tbts_batch_max_9 (more than 9), or a body that isn't valid JSON |
402 | Payment failed — no valid payment method on file, or the card was declined |
409 | status: "validation_failed": one or more quotes can't be accepted, nothing is charged, and errors[] lists each one as {quote_id, error, key, new_quote_id, new_total_price}. key is tbts_quote_not_found, tbts_quote_already_processed, tbts_quote_expired, tbts_quote_accept_in_progress, tbts_quote_needs_requote, tbts_quote_requoted (with new_quote_id and new_total_price set), or a rule's own key such as tbts_doc_too_large |
502 | status: "start_failed": the charge succeeded but no quote could start; every quote is refunded as described above |
503 | tbts_service_paused: checked before the charge, so nothing is charged |
Batch behavior: All quotes must belong to the same API key and be in pending state. If any quote in the batch fails validation, the entire batch is rejected with nothing charged (no partial acceptance); replace each tbts_quote_requoted ID with its new_quote_id and resubmit. Each accepted quote spawns an independent translation job, and you receive one completion email when the whole batch is done.
POST /translate/cancel/{quote_id}Changed your mind after accepting a quote? You can stop the translation at any point before it finishes, and you pay only for what you received. Every document-language pair already delivered is yours to keep. Every pair that was not delivered is refunded at its full per-pair price. No request body is needed; authenticate with the same API key you use for the rest of /translate/*.
curl -X POST -H "X-API-Key: your-api-key-here" \
https://api.cpmp-site.org/translate/cancel/qt-a1b2c3d4-e5f
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/translate/cancel/qt-a1b2c3d4-e5f] [200]",
"status_code": 200,
"data": {
"quote_id": "qt-a1b2c3d4-e5f",
"job_id": "019e4b8a37bc-1f2e3d4c5b6a7980",
"state": "cancelled",
"refund_status": "calculating",
"message": "Translation cancelled. Pages already delivered are yours to keep. The refund for the rest is being calculated."
},
"error": ""
}
The cancel takes effect the moment you send it, but the engine never cuts a language off mid-sentence. Each language finishes the section it is working on and stops at that boundary, so a page is either fully delivered or not delivered at all. You will never receive half a translation. On a large document one section can take up to about three minutes, and that short wait is what keeps every delivered page whole.
Once the last language has stopped, the refund is fixed by counting what was actually delivered. Your refund_status then moves through these states:
refund_status | What It Means For You |
|---|---|
calculating | The job is cancelled and the last language is finishing its current section. The refund is counted as soon as it stops. |
pending_approval | Your refund amount is fixed and waiting for a person to approve it. Every refund is reviewed by a human before it is issued. |
approved | The refund has been issued through Stripe. It appears on your statement within 5-10 business days. |
none | Every pair had already been delivered before the cancel reached it, so there is nothing to refund. All of it is yours. |
You get one email when the refund is issued. It tells you which languages were delivered, which were stopped, and the amount coming back to you. The Account Dashboard shows the same refund state live, and a cancelled job stays in your Translation History with the refunded amount under its price.
How the refund is counted: refund = total paid − (delivered pairs × per-pair price). The arithmetic runs in whole cents and any rounding goes to you, so what you were charged and what you get back always add up to exactly what you paid. If a language was in the middle of a section when you cancelled, the AI cost of that partial work is absorbed by us, never passed to you.
| Status | Condition |
|---|---|
401 | API key missing or invalid |
404 | Quote not found on your account. A quote belonging to another account also returns 404, so its existence is never confirmed to anyone else. |
409 | The translation has already been cancelled |
409 | The translation has already finished, so there is nothing left to stop |
409 | The quote was never accepted, so no job is running (the message includes the quote's current state) |
409 | tbts_cancel_already_finished also covers a quote whose finished job is already in the failed-pair refund flow (15.10, 15.11): those pairs are refunded automatically, so there is nothing to cancel |
When the job that is running is your free retry (15.10), cancel stops only the retry. The answer is 200 with tbts_cancel_retry_accepted ("The retry was cancelled. Anything it didn't finish will be refunded automatically."), refund_status stays "", and no approval is involved: the retry was free, so the pairs neither the job nor the retry delivered go straight to the automatic failed-pair refund.
Ownership follows you, not your key: a quote is matched to your account rather than to the single key that accepted it. If you rotate your API key while a job is running, the new key can still cancel it. Error messages arrive in your api_lang, like every other response.
GET /translate/status/{job_id}Live status of one translation: every document-language pair, the ETA while it runs, the free retry, and any refund. The path accepts a job ID, a retry job ID, or a quote ID (the form a start-failure response points at). Authentication is the same as the rest of /translate/*, and ownership is by your account, so any of your keys can read any of your jobs. Pair progress is read from Valkey first, so polling stays fast, with Aurora as the fallback; money and refund state always come from Aurora.
{
"status_code": 200,
"data": {
"job_id": "019e4b8a37bc-1f2e3d4c5b6a7980", "quote_id": "qt-a1b2c3d4-e5f", "parent_job_id": "019e4b8a37bc-1f2e3d4c5b6a7980",
"state": "partial", "job_state": "partial",
"model": "per-language", "model_by_lang": { "es": "qwen3-235b", "zh-TW": "qwen3-235b" }, "model_display": "Qwen 3 235B",
"submitted_at": "2026-10-10T15:04:13Z", "started_at": "2026-10-10T15:04:20Z", "completed_at": "2026-10-10T15:12:40Z",
"docs": ["api-reference.html"], "langs": ["es", "zh-TW"], "total_pairs": 2,
"counts": { "queued": 0, "running": 0, "succeeded": 1, "failed": 1, "cancelled": 0 },
"pairs": [
{ "doc": "api-reference.html", "lang": "es", "state": "succeeded", "job_id": "019e4b8a37bc-1f2e3d4c5b6a7980", "attempts": 1 },
{ "doc": "api-reference.html", "lang": "zh-TW", "state": "failed", "job_id": "019e4c02d9f1-6b5a4c3d2e1f0a9b", "attempts": 2 }
],
"eta": null,
"retry": { "allowed": false, "state": "closed", "retry_job_id": "019e4c02d9f1-6b5a4c3d2e1f0a9b", "window_ends_at": "" },
"cancel_refund": { "status": "", "amount_due": 0, "amount_refunded": 0 },
"failed_pair_refund": { "status": "issued", "amount_due": 0, "amount_refunded": 3.00, "pairs": [{ "doc": "api-reference.html", "lang": "zh-TW" }], "window_ends_at": "", "refunded_at": "2026-10-10T15:30:02Z" },
"refund_status": "issued", "refund_due": 0, "total_refunded": 3.00,
"downloads_ready": true, "download_url": "/translate/download/019e4b8a37bc-1f2e3d4c5b6a7980",
"message_key": "tbts_status_partial_refunded",
"message": "1 of 2 translations couldn't be completed. We refunded 3.00 for them automatically."
}
}
| Field | Meaning |
|---|---|
state | The whole quote across every attempt: queued, running, succeeded, partial, failed or cancelled. job_state is the raw state of the job you asked about. |
pairs | One entry per document and language, sorted by document then language. A pair delivered by any attempt counts as succeeded; job_id is the attempt that produced its state and attempts counts the tries. |
model, model_by_lang, model_display | The agent you were quoted for, so you can see your choice was honored. model_by_lang is {} for an agent you chose. |
eta | Filled while any job of the quote is active, null otherwise. |
retry | allowed is true when the free retry is available; state is open, running or closed; window_ends_at is when the retry window closes. |
cancel_refund, failed_pair_refund | The two refund kinds, kept apart. failed_pair_refund.status is window_open, retrying, due, issuing, issued, none, error or "". refund_status and refund_due summarize whichever applies, so a simple client reads one field. |
downloads_ready, download_url | True once at least one pair is delivered and no job is active, including partial and cancelled jobs. |
message_key, message | Where things stand, in your api_lang. Amounts have two decimals and no currency symbol, and dates read YYYY-MM-DD HH:MM UTC. |
Every field is always present and every list is [] rather than null. The full list of message_key values and what each one means is in the Translation Service document.
| Status | Key | Condition |
|---|---|---|
401 | missing_api_key / invalid_api_key | API key missing or invalid |
403 | tbts_prices_only_key | The key is a Prices-only key |
404 | tbts_job_not_found | Not found, not yours, never accepted, or a malformed ID. Always 404, never 403, so nobody can probe for another account's jobs |
503 | tbts_status_unavailable | The ledger is briefly unreachable; try again in a moment |
GET /translate/task/{task_id}Live status of a whole batch from 15.6. Each job is reported with the same object as 15.8, built by the same code, so a batch and its jobs can never disagree.
{
"status_code": 200,
"data": {
"task_id": "019e4b8a37bd-0a1b2c3d4e5f6a7b", "state": "running", "submitted_at": "2026-10-10T15:04:13Z",
"total_jobs": 3,
"counts": { "queued": 0, "running": 1, "succeeded": 1, "partial": 1, "failed": 0, "cancelled": 0 },
"jobs": [ "<one 15.8 status object per job>" ],
"total_pairs": 14, "succeeded_pairs": 11, "failed_pairs": 1,
"total_refunded": 0, "eta": { "estimated_seconds": 180, "remaining_formatted": "~3 minutes" },
"message": "2 of 3 jobs finished."
}
}
state is running while any job is active, then succeeded, partial or cancelled. eta is the slowest active job. A quote in the batch that was refunded because it couldn't start is reported too, with its refund. Not found or not yours → 404 tbts_task_not_found.
POST /translate/retry/{quote_id}Your one free retry per quote. It runs only the retryable failed pairs again, on the agent the quote was priced for, at no charge. It is always available, whether or not automatic failed-pair refunds are switched on. A pair skipped because the document was already in that language is never retried, only refunded. No request body is needed.
{
"status_code": 200,
"data": {
"quote_id": "qt-a1b2c3d4-e5f",
"retry_job_id": "019e4c02d9f1-6b5a4c3d2e1f0a9b",
"state": "queued",
"pairs": [{ "doc": "api-reference.html", "lang": "zh-TW" }],
"status_url": "/translate/status/019e4c02d9f1-6b5a4c3d2e1f0a9b",
"message": "Retrying 1 translations at no charge."
}
}
| Status | Key | Condition |
|---|---|---|
404 | tbts_job_not_found | Quote not found on your account |
409 | tbts_retry_not_available | Nothing to retry, or a job is still running |
409 | tbts_retry_in_progress | The retry is already running |
409 | tbts_retry_closed | The failed pairs were already refunded, or the one free retry was used |
409 | tbts_retry_window_expired | The 72-hour retry window has ended, so the refund is being processed instead |
502 | tbts_retry_start_failed | The retry couldn't start; nothing changed, so try again in a few minutes |
503 | tbts_status_unavailable | The ledger is briefly unreachable |
POST /translate/refund-failed/{quote_id}"Refund instead": you decline the free retry and ask for the failed pairs to be refunded now. It moves the quote's failed_pair_refund.status from window_open to due and issues the refund straight away. Each failed pair is refunded at exactly its own price, in whole cents with any rounding going to you, and the refund can never be sent twice. No request body and no approval step.
{
"status_code": 200,
"data": {
"quote_id": "qt-a1b2c3d4-e5f",
"failed_pair_refund": { "status": "issued", "amount_due": 0, "amount_refunded": 3.00, "pairs": [{ "doc": "api-reference.html", "lang": "zh-TW" }], "window_ends_at": "", "refunded_at": "2026-10-10T15:20:41Z" },
"message": "Refund requested for 1 translations. It's being processed now."
}
}
| Status | Key | Condition |
|---|---|---|
404 | tbts_refund_failed_unavailable | Only while automatic refunds for incomplete translations are paused (they have been on since October 11, 2026). Checked before authentication, so this one answer is always in English. The free retry (15.10) still works |
404 | tbts_job_not_found | Quote not found on your account |
409 | tbts_refund_failed_not_available | There is no refund to request right now: nothing failed, the retry is running, or the refund is already due or issued |
503 | tbts_status_unavailable | The ledger is briefly unreachable |
If you do nothing: 72 hours after the job finishes, anything still undelivered is refunded automatically, and if your free retry also fails, the refund happens as soon as it finishes. A 15-minute sweeper inside the LPO keeps those promises without anyone watching, and you get one email when the refund is issued.
GET /translate/languages — Supported Languages127 languages, every one usable as a source or a target, English included, translated directly from one to the other with no pivot through a third language. The list below is generated from the same catalog the quote checks; a code that isn't on it is refused before anything is priced or charged.
GET /translate/languages returns it live. It is public (no key) and cacheable for five minutes (Cache-Control: public, max-age=300), so your integration never has to hard-code the list:
{
"status_code": 200,
"data": {
"count": 127,
"languages": [
{ "code": "af", "english_name": "Afrikaans", "native_name": "Afrikaans", "script": "Latn", "rtl": false, "word_spaced": true, "base_lang": "", "status": "supported" }
],
"limits": { "max_langs_per_quote": 12, "max_docs_per_quote": 42, "max_doc_kb": 300, "max_quotes_per_batch": 9 },
"aliases": { "nb": "no", "zh-Hant": "zh-TW", "ar-EG": "ar" },
"source": "aurora"
}
}
count is the number of languages you can order right now. languages lists every targetable row, and a row an operator has paused shows status: "blocked" rather than disappearing. source says where the catalog was read from (aurora, valkey or empty); an empty catalog answers 200 with count: 0, and a quote then answers 503 tbts_langs_unavailable rather than guessing.
rtl: true), such as Arabic and its dialects, Hebrew, Persian, Urdu and Pashto.word_spaced: false): Japanese, Chinese, Traditional Chinese, Cantonese, Thai, Lao, Khmer and Burmese.ar); a dialect has its own code. The full list: nb → no, zh-Hant → zh-TW, zh-HK → zh-TW, zh-MO → zh-TW, zh-Hans → zh, zh-CN → zh, zh-SG → zh, ar-EG → ar, ar-MA → ar, ar-TN → ar, ar-SA → ar, ar-IQ → ar, ar-YE → ar, ar-SY → ar, ar-LB → ar, ar-JO → ar, ar-PS → ar, pt-BR → pt, pt-AO → pt-PT, pt-MZ → pt-PT, pt-CV → pt-PT, pt-GW → pt-PT, pt-ST → pt-PT, pt-TL → pt-PT, fil → tl, iw → he, in → id, ji → yi, prs → fa-AF, pes → fa, es-419 → es, sr-Cyrl → sr, uz-Latn → uz, az-Latn → az, pa-Guru → pa, mn-Cyrl → mn.| Code | Language | Code | Language | Code | Language |
|---|---|---|---|---|---|
af | Afrikaans | sq | Albanian | am | Amharic |
ar | Arabic | arz | Arabic (Egyptian) | apc | Arabic (Levantine) |
acm | Arabic (Mesopotamian) | ary | Arabic (Moroccan) | ars | Arabic (Najdi) |
acq | Arabic (Ta'izzi-Adeni) | aeb | Arabic (Tunisian) | hy | Armenian |
as | Assamese | ast | Asturian | awa | Awadhi |
az | Azerbaijani | ban | Balinese | bjn | Banjar |
ba | Bashkir | eu | Basque | be | Belarusian |
bn | Bengali | bho | Bhojpuri | bs | Bosnian |
bg | Bulgarian | my | Burmese | yue | Cantonese |
ca | Catalan | ceb | Cebuano | hne | Chhattisgarhi |
zh | Chinese (Simplified) | zh-TW | Chinese (Traditional) | hr | Croatian |
cs | Czech | da | Danish | fa-AF | Dari |
nl | Dutch | en | English | et | Estonian |
fo | Faroese | fi | Finnish | fr | French |
fr-CA | French (Canada) | fur | Friulian | gl | Galician |
ka | Georgian | de | German | el | Greek |
gu | Gujarati | ht | Haitian Creole | ha | Hausa |
he | Hebrew | hi | Hindi | hu | Hungarian |
is | Icelandic | ilo | Ilocano (Iloko) | id | Indonesian |
ga | Irish | it | Italian | ja | Japanese |
jv | Javanese | kea | Kabuverdianu | kn | Kannada |
kk | Kazakh | km | Khmer | ko | Korean |
| Code | Language | Code | Language | Code | Language |
|---|---|---|---|---|---|
lo | Lao | lv | Latvian | lij | Ligurian |
li | Limburgish | lt | Lithuanian | lmo | Lombard |
lb | Luxembourgish | mk | Macedonian | mag | Magahi |
mai | Maithili | ms | Malay | ml | Malayalam |
mt | Maltese | mr | Marathi | min | Minangkabau |
mn | Mongolian | ne | Nepali | no | Norwegian (Bokmål) |
nn | Norwegian (Nynorsk) | oc | Occitan | or | Odia (Oriya) |
pag | Pangasinan | pap | Papiamento | ps | Pashto |
fa | Persian | pl | Polish | pt | Portuguese (Brazil) |
pt-PT | Portuguese (Portugal) | pa | Punjabi | ro | Romanian |
ru | Russian | sc | Sardinian | sr | Serbian |
scn | Sicilian | szl | Silesian | sd | Sindhi |
si | Sinhala | sk | Slovak | sl | Slovenian |
so | Somali | es | Spanish | es-MX | Spanish (Mexico) |
su | Sundanese | sw | Swahili | sv | Swedish |
tl | Tagalog (Filipino) | tg | Tajik | ta | Tamil |
tt | Tatar | te | Telugu | th | Thai |
tpi | Tok Pisin | tr | Turkish | uk | Ukrainian |
ur | Urdu | uz | Uzbek | vec | Venetian |
vi | Vietnamese | war | Waray | cy | Welsh |
yi | Yiddish (Eastern) |
POST /admin/translate/refund/{quote_id}Issues a full or partial refund for a completed translation purchase. Admin-only — requires X-Admin-Key header. Refunds are processed via Stripe and the customer receives a confirmation email automatically.
| Parameter | Type | Description |
|---|---|---|
quote_id | string | The quote ID from the original translation purchase (e.g., qt-a1b2c3d4-e5f) |
POST /admin/translate/refund/qt-a1b2c3d4-e5f
X-Admin-Key: your-admin-key
Content-Type: application/json
{
"amount": 6.00,
"reason": "Customer unsatisfied with Japanese translation quality"
}
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | No | Partial refund amount in USD. If omitted or equal to the original charge, a full refund is issued. |
reason | string | No | Reason for the refund. Included in the customer's refund confirmation email and stored as metadata on the Stripe refund. |
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/admin/translate/refund/qt-a1b2c3d4-e5f] [200]",
"status_code": 200,
"data": {
"status": "refunded",
"quote_id": "qt-a1b2c3d4-e5f",
"refund_id": "re_1234567890",
"refund_amount": 15.00,
"original_amount": 15.00,
"refunded_total": 15.00,
"kind": "admin",
"partial": false,
"stripe_status": "succeeded"
},
"error": ""
}
Validation rules:
accepted or partially_refunded state (already paid)pending, expired, superseded, or already fully refundedtotal_price minus refunded_total, and never more than remains on the Stripe PaymentIntent (which matters for a batch, where several quotes share one charge). So a refund can never exceed what was paid, whichever path issued the earlier onesrefunded_total is the running total refunded on this quote by every kind (admin, cancel, failed pairs); kind is admin for this endpointCustomers can configure up to 150 brand terms that are automatically protected during every translation job. These terms are never translated or transliterated — they appear exactly as written in every target language. Terms are managed via the dashboard API and apply to all future translation jobs for the account.
GET /dashboard/api/protected-termsReturns the customer's current list of protected brand terms. Requires dashboard Bearer token authentication.
{
"status_code": 200,
"data": {
"terms": ["Acme Corp", "DataForge", "CloudSync API", "John Smith"],
"count": 4,
"limit": 150
},
"error": ""
}
PUT /dashboard/api/protected-termsReplaces the customer's entire brand terms list. Requires dashboard Bearer token authentication.
PUT /dashboard/api/protected-terms
Authorization: Bearer your-session-token
Content-Type: application/json
{
"terms": ["Acme Corp", "DataForge", "CloudSync API", "John Smith", "SyncEngine"]
}
| Field | Type | Required | Description |
|---|---|---|---|
terms | array of strings | Yes | The complete list of brand terms to protect. Replaces any existing terms. |
{
"status_code": 200,
"data": {
"message": "Saved 5 brand terms",
"terms": ["Acme Corp", "DataForge", "CloudSync API", "John Smith", "SyncEngine"],
"count": 5,
"limit": 150
},
"error": ""
}
Limits & validation:
Authenticated dashboard endpoints for translation spend tracking, refund history, retries, refunds, downloads and job completion notifications. All require Bearer token authentication via the dashboard session. Every one of them reads across all of your translation keys, revoked keys included, so rotating a key never hides your history.
GET /dashboard/api/translate/spendReturns translation spend aggregated by month and model for the last 6 months. Customers see only their own spend; admins see all accounts.
{
"status_code": 200,
"data": {
"summary": {
"total_revenue_usd": 247.50,
"total_bedrock_cost_usd": 89.30,
"total_jobs": 18,
"total_pairs": 142
},
"months": [
{
"month": "2026-05",
"revenue_usd": 132.00,
"bedrock_cost_usd": 48.20,
"jobs": 10,
"pairs": 78,
"by_model": {
"claude-haiku-5.5": {"jobs": 3, "pairs": 24, "cost_usd": 9.60},
"claude-sonnet-4.6": {"jobs": 6, "pairs": 48, "cost_usd": 79.20},
"claude-opus-4.8": {"jobs": 1, "pairs": 6, "cost_usd": 43.20}
}
}
]
},
"error": ""
}
GET /dashboard/api/translate/refundsReturns refund history for translation purchases. A customer sees their own refunds from every one of their translation keys; an admin sees every account. kind tells the three refund paths apart: failed_pairs (the automatic refund for incomplete translations), cancel and admin. refund_amount is the quote's running refunded_total.
{
"status_code": 200,
"data": {
"refunds": [
{
"quote_id": "qt-a1b2c3d4-e5f",
"doc_name": "api-reference.html",
"lang_count": 5,
"model": "qwen3-235b",
"original_price": "15.00",
"refund_amount": "3.00",
"refund_id": "re_1234567890",
"state": "partially_refunded",
"refunded_at": "2026-05-22T14:30:00Z",
"accepted_at": "2026-05-22T13:58:10Z",
"customer_email": "user@example.com",
"kind": "failed_pairs"
}
],
"count": 1
},
"error": ""
}
POST /dashboard/api/translate/cancel/{job_id}The endpoint behind the Cancel Translation button in your Account Dashboard. It follows the same policy and runs the same cancel path as POST /translate/cancel/{quote_id} (15.7), so the refund is identical whichever one you use. The only differences are that it takes the job_id your dashboard already holds and authenticates with your dashboard session instead of an API key. No request body is needed.
{
"status_code": 200,
"data": {
"job_id": "019e4b8a37bc-1f2e3d4c5b6a7980",
"state": "cancelled",
"refund_status": "calculating"
},
"error": ""
}
Refusals match 15.7: 404 when the job is not on your account, 409 when it has already been cancelled, has already finished, or was never accepted. Cancelling a running free retry stops only the retry, exactly as in 15.7.
The buttons know when to appear: GET /dashboard/api/translate/status/{job_id} always carries three cancel fields. can_cancel is true only while the job is queued or running, which is what shows or hides the button. For a cancelled job, refund_status and refund_due report the refund as it moves from calculating to pending_approval to approved, so the dashboard stays in step with your money without a page refresh. It is built by the same code as the public status (15.8), so it also carries quote_id, retry, failed_pair_refund, downloads_ready, model_display, model_by_lang and is_retry: the Retry button shows when retry.allowed is true, "Refund instead" when failed_pair_refund.status is window_open, and Download when downloads_ready is true.
POST /dashboard/api/translate/retry-failed/{job_id}The Retry button: the one free retry per quote (15.10), started from the dashboard with the job ID it already holds. Always available.
{
"status_code": 200,
"data": {
"job_id": "019e4c02d9f1-6b5a4c3d2e1f0a9b",
"quote_id": "qt-a1b2c3d4-e5f",
"state": "queued",
"submitted_at": "2026-10-10T15:14:02Z",
"retry_of": "019e4b8a37bc-1f2e3d4c5b6a7980",
"docs": ["api-reference.html"],
"langs": ["zh-TW"]
},
"error": ""
}
job_id here is the new retry job. Refusals use the dashboard error keys translate_retry_not_available, translate_retry_in_progress, translate_retry_closed and translate_retry_window_expired, with the same meanings as 15.10.
POST /dashboard/api/translate/refund-failed/{job_id}The "Refund instead" button (15.11). Response {job_id, quote_id, pairs, failed_pair_refund}. If automatic failed-pair refunds are ever paused, it answers 404 translate_refund_failed_unavailable, and when there is nothing to refund, 409 translate_refund_failed_not_available.
GET /dashboard/api/translate/download/{job_id}Signed download links for every delivered document and language, the same set as GET /translate/download/{job_id}. Allowed once no job of the quote is active and at least one pair was delivered, so partial and cancelled jobs can collect what they received.
GET /dashboard/api/translate/notificationsReturns unread notification count and recent completed job summaries across every translation API key of the authenticated user.
{
"status_code": 200,
"data": {
"unread_count": 2,
"notifications": [
{
"job_id": "019e5bdc36a5-c87d32a56bc39bee",
"completed_at": "2026-05-24T21:25:00Z",
"state": "succeeded",
"docs": ["Trinity-Beast-Translation-Service.html"],
"langs": 11,
"succeeded_pairs": 11,
"failed_pairs": 0
}
]
},
"error": ""
}
POST /dashboard/api/translate/notifications/seenMarks all notifications as seen. Resets the badge count to 0. No request body required.
{
"status_code": 200,
"data": {
"message": "Notifications marked as seen"
},
"error": ""
}
Polling behavior: The dashboard SPA polls /dashboard/api/translate/notifications every 60 seconds. The seen state is stored per user in Valkey at dashboard:notifications:seen:user:{users.id} with a 30-day TTL, so it follows you across key rotations; the old per-key value (dashboard:notifications:seen:{api_key_id}) is read once as a fallback. Only translation job completions generate notifications — quotes, cancellations, and in-progress updates do not.
Automated multi-lingual JSON key translation for the website's internationalization system. Detects missing keys across all 11 target languages and fills them via Amazon Bedrock (Claude) or manual injection. The website serves language JSON from Valkey via GET /public/lang/{code} — this endpoint keeps all languages in sync with English.
Detects missing keys and translates them. Bedrock mode is async (fire-and-forget) — returns a job ID immediately and processes in the background. Manual mode and dry-run are synchronous.
| Property | Value |
|---|---|
| Endpoint | POST /admin/translate-i18n-keys |
| Auth | X-Admin-Key header |
| Content-Type | application/json |
| Field | Type | Default | Description |
|---|---|---|---|
mode | string | "bedrock" | "bedrock" (async, AI-powered) or "manual" (sync, pre-translated values) |
namespace | string | "*" | Target namespace prefix (e.g., "translate", "docLibrary", "*" for all) |
target_langs | string[] | ["all"] | ["all"] or specific codes like ["es","pt","fr"] |
dry_run | bool | false | If true, report missing keys without writing |
keys | object | null | Manual mode only — pre-translated key/value pairs per language |
sync_s3 | bool | false | Reserved — sync updated JSON to S3 after translation |
POST /admin/translate-i18n-keys
X-Admin-Key: {admin_key}
Content-Type: application/json
{
"namespace": "translate",
"target_langs": ["all"]
}
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/admin/translate-i18n-keys] [202]",
"status_code": 202,
"data": {
"status": "accepted",
"job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"mode": "bedrock",
"namespace": "translate",
"source_keys": 62,
"target_langs": ["es","pt","fr","de","ru","hi","ur","it","ar","ja","zh"],
"message": "Translation job started. Poll /admin/translate-i18n-keys/status/{job_id} for progress."
}
}
POST /admin/translate-i18n-keys
X-Admin-Key: {admin_key}
Content-Type: application/json
{
"namespace": "*",
"dry_run": true,
"target_langs": ["es","ja"]
}
{
"data": {
"status": "dry_run",
"namespace": "*",
"source_keys": 1583,
"languages": [
{"lang": "es", "missing_keys": ["dailyReports.loadError", "..."], "status": "dry_run"},
{"lang": "ja", "missing_keys": ["dailyReports.loadError", "..."], "status": "dry_run"}
]
}
}
POST /admin/translate-i18n-keys
X-Admin-Key: {admin_key}
Content-Type: application/json
{
"mode": "manual",
"namespace": "translate",
"target_langs": ["all"],
"keys": {
"translate.hero.title": {
"es": "Motor de Traducción",
"pt": "Motor de Tradução",
"fr": "Moteur de Traduction",
"de": "Übersetzungsmaschine",
"ru": "Движок перевода",
"hi": "अनुवाद इंजन",
"ur": "ترجمہ انجن",
"it": "Motore di Traduzione",
"ar": "محرك الترجمة",
"ja": "翻訳エンジン",
"zh": "翻译引擎"
}
}
}
Poll the status of an async Bedrock translation job. Jobs are stored in Valkey with a 24-hour TTL.
| Property | Value |
|---|---|
| Endpoint | GET /admin/translate-i18n-keys/status/{job_id} |
| Auth | X-Admin-Key header |
{
"data": {
"job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "completed",
"mode": "bedrock",
"namespace": "translate",
"source_keys": 62,
"target_langs": ["es","pt","fr","de","ru","hi","ur","it","ar","ja","zh"],
"started_at": "2026-05-22T19:10:00Z",
"completed_at": "2026-05-22T19:12:34Z",
"total_translated": 682,
"languages": [
{"lang": "es", "translated": 62, "status": "ok"},
{"lang": "pt", "translated": 62, "status": "ok"},
{"lang": "fr", "translated": 62, "status": "ok"}
]
}
}
| Code | Language | Code | Language |
|---|---|---|---|
es | Spanish | ar | Arabic |
pt | Portuguese | ja | Japanese |
fr | French | zh | Chinese (Simplified) |
de | German | it | Italian |
ru | Russian | hi | Hindi |
ur | Urdu |
lang:en from Valkey (source of truth for all i18n keys)translate.hero.title)lang:{code} and identifies missing keysGET /public/lang/{code}us.anthropic.claude-sonnet-4-6)$0.001 per 15-key batch — under $0.25 for a full 11-language runi18n:job:{job_id} with 24-hour TTLbash scripts/kcc.sh push-langs to sync Valkey → S3 if needed