Express + TypeScript backend for the Soroban milestone escrow platform.
- Node.js + Express
- TypeScript
- Stellar SDK
- Soroban RPC
- Setup
- Architecture
- API Documentation
- API Endpoints
- System & Health
- Jobs API
- GET /api/jobs/:contractId
- GET /api/jobs/by-wallet/:address
- POST /api/jobs/create-job-draft
- GET /api/jobs/drafts/:id
- GET /api/jobs/query
- GET /api/jobs/whitelisted-tokens
- GET /api/jobs/:contractId/history
- GET /api/jobs/:contractId/fee-preview
- GET /api/jobs/:contractId/whitelist
- POST /api/jobs/:contractId/whitelist/update
- POST /api/jobs/build-tx
- POST /api/jobs/:contractId/milestones/:index/partial-release
- GET /api/jobs/:contractId/milestones/:index/time-remaining
- POST /api/jobs/:contractId/milestones/:index/claim-auto-release
- POST /api/jobs/submit
- Admin API
- Webhooks API
- Error Responses
- Related Repos
npm install
cp .env.example .env # configure required production / local keys
npm run dev- Architecture overview — live request/indexer paths and reachability table
- CSV serialization guide
- Webhook delivery — event types, payload shapes, retry policy, idempotency
- OpenAPI 3.0 specification — complete machine-readable route definitions, parameter schemas, and response shapes
Webhooks are delivered at least once, so receivers must tolerate duplicates.
Each POST body includes an event_id, and each request carries:
X-Milesto-Event-Id: stable per on-chain event (the RPC event id; events indexed before it was stored use<contract_id>:<ledger_sequence>:<event_type>). Dedupe on this.X-Milesto-Delivery-Id: UUID per logical delivery (event to subscriber), constant across all of its retries, including those made by the background retry sweep.X-Milesto-Delivery-Attempt: the attempt number, starting at1.
See docs/webhooks.md for the full contract.
The complete OpenAPI specification is maintained in docs/openapi.yaml.
New integrations should use /api/v1. Existing /api routes remain available
for backwards compatibility and return Deprecation: true.
- Public endpoints: Endpoints that simulate public contract reads (
/api/jobs/:contractId,/api/jobs/by-wallet/:address,/api/jobs/:contractId/whitelist, etc.) or construct unsigned transactions for the caller's wallet to sign require no API key. - Admin endpoints:
/api/admin/*requires an admin key passed viax-api-key: <ADMIN_API_KEY>orAuthorization: Bearer <ADMIN_API_KEY>. - Client endpoints: When
API_KEYSis configured, client integrations pass their key viax-api-key: <API_KEY>.
Check server and contract configuration health.
- Auth: None (Public)
- Response:
{ "status": "ok", "contract": "<CONTRACT_ID>" }
Get current job state simulated directly from the Soroban contract.
- Auth: None (Public read)
- Rate Limit: 30 requests / minute (
JOB_CONTRACT_RATE_MAX) - Params:
contractId(Stellar contract C... address)
Get indexed jobs associated with a participant wallet address (as client, freelancer, or arbiter).
- Auth: None (Public read)
- Rate Limit: 30 requests / minute (
BY_WALLET_RATE_LIMIT_MAX) - Params:
address(Stellar G... account address) - Query:
page(default: 1),limit(default: 20, max: 100)
Validate and persist an off-chain job escrow draft before deploying on-chain.
- Auth: None (Public)
- Rate Limit: 5 requests / minute (
CREATE_JOB_DRAFT_RATE_MAX) - Body:
{ client, freelancer, arbiter, token, autoReleaseDays?, milestones: [{ amount, description? }] }
Fetch a job draft saved by create-job-draft (#667).
- Auth: None (Public)
- Response: the stored draft, or
404when no draft has that ID
Execute read-only simulation queries against a Soroban contract.
- Auth: None (Public)
- Rate Limit: 30 requests / minute (
JOB_CONTRACT_RATE_MAX)
Frontend query alias to fetch accepted token contract IDs for a contract.
- Auth: None (Public)
- Query:
contractId(required)
Fetch indexed contract lifecycle events (funded, milestone approved, completed, etc.).
- Auth: None (Public)
- Query:
page(default: 1),limit(default: 10, max: 100)
Calculate the platform fee breakdown for a specified amount.
- Auth: None (Public)
- Query:
amount(required)
Get the list of whitelisted payment token contract addresses accepted by the escrow contract.
- Auth: None (Public)
- Rate Limit: 20 requests / minute (
JOB_WHITELIST_RATE_MAX)
Build an unsigned transaction to add or remove whitelisted tokens.
- Auth: None (Public; wallet signature required on-chain)
- Rate Limit: 10 requests / minute (
JOB_WHITELIST_UPDATE_RATE_MAX) - Body:
{ token, action: "add" | "remove", adminAddress }or{ addresses: [...] }
Build an unsigned transaction XDR for authorized contract entrypoints.
- Auth: None (Public; caller wallet signs)
- Rate Limit: 20 requests / minute (
BUILD_TX_RATE_MAX) - Body:
{ contractId, method, args, sourceAddress }; each arg is{ type, value }withtypeone ofaddress,i128,u32,u64,bool,bytes(hex string) orvec - Response:
{ success: true, data: { xdr } }
Build an unsigned transaction XDR for partial milestone release.
- Auth: None (Public; caller wallet signs)
- Rate Limit: 10 requests / minute (
PARTIAL_RELEASE_RATE_MAX) - Params:
contractId,index(milestone index) - Body:
{ amount, sourceAddress } - Response:
{ success: true, data: { xdr } }
Get seconds remaining until an unapproved milestone is eligible for auto-release.
Also available via alias route GET /api/jobs/:contractId/milestones/:index/auto-release.
- Auth: None (Public)
- Rate Limit: 60 requests / minute (
TIME_REMAINING_RATE_MAX) - Params:
contractId,index(milestone index)
Build an unsigned transaction XDR to claim an auto-released milestone.
- Auth: None (Public; caller wallet signs)
- Rate Limit: 10 requests / minute (
CLAIM_AUTO_RELEASE_RATE_MAX) - Params:
contractId,index - Body:
{ sourceAddress } - Response:
{ success: true, data: { xdr } }
Submit a signed transaction to the Stellar network and return execution status.
- Auth: None (Public)
- Rate Limit: 5 requests / minute (
SUBMIT_RATE_MAX) - Body:
{ signedXdr, sourceAddress? }
Retrieve indexer health, current ledger, event counters, uptime, and reconciliation status.
- Auth: Admin API Key (
x-api-key: <ADMIN_API_KEY>orBearer) - Response:
{ success: true, data: { lastIndexedLedger, totalEvents, uptimeSeconds, lastEventAt, eventsByType, skippedLedgerRange, undecodableEvents, reconciliation, failedDeliveries, deadDeliveries, consecutiveFailures, lastSuccessfulPollAt, pollIntervalMs, ledgerLag } }(ledgerLagisnullwhen the RPC head cannot be read)
Snapshot of the indexer metrics collector.
- Auth: Admin API Key
- Response:
{ success: true, metrics };500if the metrics cannot be collected
Every webhook subscription, plus the active and disabled subsets. A subscription is auto-disabled after WEBHOOK_DISABLE_AFTER consecutive failed deliveries (#701); subscribing again re-enables it.
- Auth: Admin API Key
- Response:
{ success: true, subscriptions, active, disabled }
Import a historical ledger range in the background (#703). One backfill runs at a time.
- Auth: Admin API Key
- Body:
{ startLedger, endLedger, contractId? }(inclusive range;contractIdlimits the import to one contract) - Response:
202with the job ID;409while another backfill is running;400for an invalid range
Status of the current or last backfill: running, done or failed, with eventsFound / eventsImported, or { status: "idle" }.
- Auth: Admin API Key
List the contracts the indexer monitors, with label, active flag, ledger pointer and start ledger (#659).
- Auth: Admin API Key
Register a contract for indexing (#659).
- Auth: Admin API Key
- Body:
{ contractId, label?, startLedger? } - Response:
201with the registered contract
Stop indexing a contract (#659).
- Auth: Admin API Key
Register a webhook URL to receive real-time notifications for contract events.
- Auth: None (Public)
- Body:
{ contract_id, webhook_url, event_types?: [...] }
Unsubscribe an existing webhook registration.
- Auth: None (Public)
- Body:
{ contract_id, webhook_url }
List the webhook subscriptions for a contract (#705). Query strings in webhook URLs are masked.
- Auth: API Key
- Query:
?contract_id=C... - Response:
{ success: true, data: { subscriptions: [{ id, webhook_url, event_types, created_at }] } }
All endpoints return JSON errors in a consistent shape.
{
"success": false,
"error": "Request validation failed",
"fields": [
{
"field": "contractId",
"message": "contractId is required"
}
]
}{
"success": false,
"error": "Unauthorized"
}{
"success": false,
"error": "Too many requests, please try again later"
}{
"success": false,
"error": "Job not found"
}{
"success": false,
"error": "Internal server error"
}- escrow-contract — Soroban smart contract
- escrow-frontend — Next.js frontend