REST API
Base URL: this site's address (for example https://embr.example/api/...). All bodies are JSON.
Authentication
Authorization: Bearer embr_live_<key>
Create keys under Settings → API keys. Keys are shown once and stored as SHA-256 hashes. API keys cannot create
other keys. Browser sessions also work, but mutating requests need a same-origin Origin header.
Session-only actions. Anything that moves funds or changes where they go requires a dashboard session and returns
403 for API keys: preparing, confirming, cancelling or retrying a sale, and changing the primary wallet.
Errors
{ "error": "Campaign not found" }
{ "error": "Invalid request", "issues": [{ "path": "maxSlippageBps", "message": "Number must be less than or equal to 5000" }] }
| Status | Meaning |
|---|---|
| 400 | Malformed input |
| 401 | Missing or invalid credentials, or an invalid / expired signature |
| 403 | Cross-origin cookie request, or an API key used for a session-only action |
| 404 | Not found or not yours |
| 409 | Identity already linked to another account, or a sale's return address changed since review |
| 422 | Validation failed (schema, mint refused, wallet already used, sale not possible); error explains why |
| 429 | Too many requests from this IP: 20 a minute for /api/auth/* and key exports, 300 a minute for the rest of the API. Honor Retry-After (seconds) |
Campaigns
GET /api/campaigns
{
"campaigns": [
{
"id": "cm5…", "ref": "ab12cd", "name": "CINDR hold",
"chain": "solana", "chainName": "Solana", "retired": false,
"mode": "hold",
"walletId": "…", "walletAddress": "9xQe…", "walletLabel": "Treasury hold",
"returnAddress": "7Np4…",
"targetTokenAddress": "…", "targetSymbol": "CINDR", "targetDecimals": 6,
"paymentTokenAddress": "native", "paymentSymbol": "SOL",
"amount": "0.25", "amountUnit": "token", "schedule": "every 1h",
"maxSlippageBps": 100, "maxPriceImpactBps": 200,
"maxRuns": 50, "runsCompleted": 12,
"isActive": true, "pauseReason": null, "consecutiveFailures": 0,
"lastRunAt": "2026-10-06T09:00:04.120Z", "nextRunAt": "2026-10-06T10:00:00.000Z",
"createdAt": "2026-10-01T12:00:00.000Z",
"explorerUrl": "https://solscan.io/account/9xQe…",
"ledger": null
}
]
}
ledger is filled by GET /api/campaigns/:id.
POST /api/campaigns
{
"name": "CINDR hold",
"walletId": "<campaign wallet id>",
"mode": "hold",
"targetTokenAddress": "<mint>",
"paymentTokenAddress": "native",
"amountPerInterval": "0.25",
"amountUnit": "token",
"intervalSeconds": 3600,
"cronExpression": null,
"maxSlippageBps": 100,
"maxPriceImpactBps": 200,
"startActive": true,
"maxRuns": 50
}
modeisburn,holdorlpand cannot be changed later.lp(Auto LP) requirespaymentTokenAddress: "native".lpPool(optional) pins a Raydium CPMM pool; otherwise the deepest token/SOL CPMM pool is used.lpBurn: trueburns the LP tokens each cycle mints. Both are immutable.- Requires a verified primary wallet; the wallet must be yours and not used by another campaign.
- Both mints are inspected on-chain; unsupported Token-2022 extensions are refused (policy).
- Exactly one of
intervalSeconds(≥ 60) orcronExpression(UTC).paymentTokenAddress: "native"spends SOL. maxRuns(1-100,000) completes the campaign after that many successful buys;nullruns until the wallet runs low.chainmay be sent as"solana"for compatibility.burnAfterBuy,burnAddressandrouteHintsare no longer accepted.
Returns 201 { "campaign": … }.
GET /api/campaigns/:id
The campaign plus its ledger:
"ledger": {
"spent": "3", "bought": "41,233.18", "burned": "0", "unburned": "41,233.18",
"held": "31,233.18", "available": "31,233.18", "sold": "10,000",
"solProceeds": "0.212", "solReturned": "0.212", "availableRaw": "31233180000"
}
PATCH /api/campaigns/:id
{ "action": "pause" } // or "resume"
Resuming is refused while a sale is open or for a retired network.
DELETE /api/campaigns/:id
Removes the schedule and the campaign with its purchase history. Refused (422) while it holds tokens or has any sale history.
POST /api/campaigns/:id/run
Queues one purchase now (guardrails still apply; refused while a sale is open). Returns
202 { "queued": true, "jobId": "manual-…" }. Repeated calls within 30 seconds return the same job.
Sell & Return SOL
Workflow and guarantees: Sell & Return SOL. Sale objects:
{
"id": "…", "configId": "…", "status": "completed",
"tokenSymbol": "CINDR", "tokenDecimals": 6,
"amountIn": "10,000", "amountSold": "10,000",
"quotedOut": "0.213", "minOut": "0.21087", "slippageBps": 100, "priceImpactBps": 18,
"route": "jupiter:Raydium", "returnAddress": "7Np4…",
"solReceived": "0.212", "sellFee": "0.0021", "returnAmount": "0.212", "feeReserve": "0",
"remainingHoldings": "31,233.18",
"sellTxHash": "5x…", "sellTxUrl": "https://solscan.io/tx/5x…",
"returnTxHash": "3k…", "returnTxUrl": "https://solscan.io/tx/3k…",
"errorMessage": null,
"quotedAt": "…", "quoteExpiresAt": "…", "createdAt": "…", "updatedAt": "…", "completedAt": "…",
"estimatedFees": "0.002115"
}
SOL amounts are in SOL, token amounts in whole tokens (formatted).
| Endpoint | Auth | Description |
|---|---|---|
GET /api/campaigns/:id/sales | any | Orders for a campaign, newest first |
POST /api/campaigns/:id/sales { amount: "all" | "<decimal>", idempotencyKey } | session | Pause purchases, reconcile, refresh balances, quote. 201 { sale } with status: "quoted". Repeating the same key returns the same order; a new key replaces an unconfirmed quote. |
GET /api/sales/:id | any | One order (poll while it is in flight) |
POST /api/sales/:id/confirm { confirm: true, returnAddress } | session | Authorize the sale and the return. returnAddress must equal the order's (409 otherwise). 202 { sale }. Idempotent. |
POST /api/sales/:id/cancel | session | Dismiss a quoted order; purchases are restored if they were running |
POST /api/sales/:id/retry-return | session | For return_failed: reconcile the last transfer, send a new one only if it provably never landed. Never repeats the sale. 202 { sale } |
Primary wallet
POST /api/primary-wallet (session only)
{ "message": "<Sign-In with Solana text, PRIMARY_WALLET_STATEMENT>", "signature": "<base64>", "confirm": true }
The message must use the statement Set this wallet as my CINDR primary wallet. Sell & Return SOL proceeds will be sent here., this site's domain and a nonce from POST /api/auth/nonce. Refused while any sale is open, or for an CINDR
campaign wallet. Returns { primaryWallet, campaignsUpdated }.
Executions
GET /api/executions?configId=&cursor=&limit=
Newest first, limit 1-100 (default 30). Pass nextCursor back as cursor for the next page.
{
"executions": [
{
"logId": "…", "configId": "…", "campaignName": "CINDR hold", "chain": "solana", "mode": "hold",
"status": "success", "txHash": "5x…", "txUrl": "https://solscan.io/tx/5x…",
"burnTxUrl": null,
"amountSpent": "0.25", "amountReceived": "41,233.18",
"paymentSymbol": "SOL", "targetSymbol": "CINDR",
"errorMessage": null, "timestamp": "2026-10-06T09:00:04.120Z"
}
],
"nextCursor": "…"
}
status is one of pending, submitted, success, failed, paused_slippage, paused_balance, skipped_gas.
GET /api/executions/stream
Server-Sent Events. Events: ready, execution (as above, plus kind), campaign
({ configId, isActive, pauseReason, nextRunAt }) and sale ({ saleId, configId, status, message }).
curl -N -H "Authorization: Bearer $EMBR_KEY" https://embr.example.com/api/executions/stream
Wallets
| Endpoint | Description |
|---|---|
GET /api/wallets | { wallets: [{ id, chain, chainName, retired, address, label, explorerUrl, createdAt, keyExportedAt, campaigns: [{ id, name, mode, isActive, targetSymbol }] }] } |
POST /api/wallets { label? } | Generates a Solana campaign wallet. Max 25. |
POST /api/wallets/:id/export (session only) { message, signature } | Reveals the campaign wallet's private key. The message is signed by your verified primary wallet with the statement Reveal the private key of my CINDR campaign wallet <address>. Anyone with this key can move its funds. and a nonce from POST /api/auth/nonce. Returns { address, secretKey } (base58, 64 bytes). See Wallets & keys. |
GET /api/wallets/balances | SOL, payment-token and holding balances per Solana wallet (cached 15s); each balance has role: sol, payment or holding. |
Tokens
GET /api/tokens/lookup?address=&walletId=
{
"token": { "address": "<mint>", "symbol": "CINDR", "decimals": 6, "name": "CINDR" },
"mint": { "program": "token-2022", "extensions": ["TransferFeeConfig"], "warnings": ["Transfer fee: …"], "blockers": [], "burnBlockers": [] },
"balance": { "raw": "250000000", "formatted": "250" }
}
address=native returns SOL (and the wallet's SOL balance).
Other
| Endpoint | Description |
|---|---|
GET /api/me | Identity: solanaAddress, primaryWallet, Telegram status |
GET /api/stats | Totals (sales, solReturned included) and per-campaign spent / bought / burned / held / sold / returned |
GET /api/chains | Networks; only solana is enabled, the rest are retired (public) |
GET /api/health | For uptime monitors (public): { ok, configured, database }, 200 when secrets are set and D1 answers, else 503 |
POST /api/telegram/link | One-time Telegram deep link |
GET/POST /api/keys, DELETE /api/keys/:id | API key management (create is session-only) |