This document describes the HTTP API surface exposed by the frontend backend
(src/app/api). The routes are intentionally thin stubs in the current code
base; they exist primarily for analytics hooks and development/testing.
Each entry includes the HTTP method, path, expected request body (if any), and an example response. All endpoints return JSON.
- Public browser routes return wildcard CORS without credentials.
- First-party browser routes echo only trusted Commitlabs origins and may allow credentials.
- Implemented routes answer
OPTIONSpreflight requests automatically.
See docs/backend-cors-policy.md for the full origin configuration and route classification.
All endpoints follow these conventions.
{
"success": true,
"data": { ... },
"meta": { ... } // optional pagination / additional metadata
}{
"success": false,
"error": {
"code": "TOO_MANY_REQUESTS",
"message": "Too many requests. Please try again later.",
"retryAfterSeconds": 60 // present on 429 and 503 only
}
}When a request is rate-limited, the response includes the Retry-After HTTP header:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
| Status | retryAfterSeconds default |
Meaning |
|---|---|---|
| 429 | 60 s | Client exceeded rate limit |
| 503 | 30 s | Service temporarily unavailable |
Clients should wait the indicated seconds before retrying. See error-handling.md for the full client retry strategy (exponential backoff + jitter).
Purchases a marketplace listing. Requires an active session cookie. Runs preflight eligibility checks, triggers on-chain ownership transfer, and records the event in the audit log.
- Authentication: session cookie (
requireAuth) - Path parameter:
id— the marketplace listing ID - Request body: none
- Response:
200 OK: Purchase completed.401 Unauthorized: Missing or invalid session.404 Not Found: Listing does not exist.409 Conflict: Preflight failed (e.g. listing inactive, buyer is seller).502 Bad Gateway: On-chain transfer failed.
curl -X POST http://localhost:3000/api/marketplace/listings/listing_1/purchase \
-H 'Cookie: session=<token>'{
"success": true,
"data": {
"listingId": "listing_1",
"commitmentId": "cm_abc",
"buyerAddress": "GBUYER...",
"price": "52000",
"currencyAsset": "USDC",
"txHash": null,
"reference": "TODO_CHAIN_CALL_TRANSFER_OWNERSHIP"
}
}Creates a new commitment on the Stellar network.
- Headers:
Idempotency-Key: (Optional) A unique string to identify the request and prevent duplicate processing. Recommended for safe retries.
- Request body:
ownerAddress: (string, required) The Stellar address of the owner.asset: (string, required) The asset code.amount: (string, required) The amount to commit.durationDays: (number, required) The duration of the commitment in days.maxLossBps: (number, required) Maximum loss in basis points.metadata: (object, optional) Additional metadata.
- Response:
201 Created: The commitment was successfully created.409 Conflict: A request with the sameIdempotency-Keyis already in progress.429 Too Many Requests: Rate limit exceeded.
curl -X POST http://localhost:3000/api/commitments \
-H 'Content-Type: application/json' \
-d '{"asset":"XLM","amount":100}'{
"message": "Commitments creation endpoint stub - rate limiting applied",
"ip": "::1"
}Marks the commitment identified by id as settled. Currently a stub that emits
CommitmentSettled events.
- Path parameter:
id(string) - Headers:
Idempotency-Key: (Optional) A unique string to identify the request and prevent duplicate processing. Replayed requests within the 24-hour replay window return the original prior result.
- Request body: optional JSON payload with additional details.
- Response: stub confirmation message.
curl -X POST http://localhost:3000/api/commitments/abc123/settle \
-H 'Content-Type: application/json' \
-d '{"finalValue":105}'{
"message": "Stub settlement endpoint for commitment abc123",
"commitmentId": "abc123"
}Funds an existing commitment that was previously created but not yet funded. The route validates ownership, enforces CREATED state, and submits the on-chain fund_escrow transaction.
- Path parameter:
id(string) - Headers:
Idempotency-Key: (Optional) A unique string to identify the request and prevent duplicate processing. Replayed requests within the 24-hour replay window return the original prior result.
- Request body:
callerAddress(string, optional) — Stellar address of the funding wallet. If omitted, the commitment owner is used.
- Response: confirmation of the funded commitment with
txHashandreference.
curl -X POST http://localhost:3000/api/commitments/abc123/fund \
-H 'Content-Type: application/json' \
-d '{"callerAddress":"GOWNER..."}'{
"success": true,
"data": {
"commitmentId": "abc123",
"txHash": "tx-abc123",
"reference": "funded",
"fundedAt": "2026-05-27T00:00:00.000Z"
}
}Executes an early exit from an active commitment. The caller must be authenticated via session cookie and must own the commitment. The route validates the request body, verifies ownership, and invokes the blockchain contract to process the early exit with applicable penalties.
- Required: Session cookie with valid authentication token.
- Ownership Check: The
callerAddressin the request body must match:- The authenticated user's address (from the session).
- The actual owner of the commitment on-chain.
- Returns:
401 UNAUTHORIZEDif no valid session token.403 FORBIDDENif addresses do not match or caller does not own the commitment.
Path parameter: id (string) — The commitment ID to exit early.
Headers:
Idempotency-Key: Optional. Replayed requests within the 24-hour replay window return the original prior result.Cookie: Required session cookie with valid token.Content-Type:application/json
Body Schema (validated via Zod):
{
reason: string; // Non-empty, max 500 characters (reason for early exit)
callerAddress: string; // Valid 56-character Stellar public key
}Body Validation Errors:
reasonmissing or empty:400 VALIDATION_ERRORreason> 500 characters:400 VALIDATION_ERRORcallerAddressmissing:400 VALIDATION_ERRORcallerAddressnot a valid Stellar address:400 VALIDATION_ERROR
Success (200 OK):
{
"success": true,
"data": {
"exitAmount": "950.00", // Amount returned to owner
"penaltyAmount": "50.00", // Penalty deducted
"finalStatus": "EARLY_EXIT", // Updated commitment status
"txHash": "abc123...", // Transaction hash (if on-chain)
"reference": null // Reference for mock mode
},
"meta": {
"correlationId": "...",
"timestamp": "2026-05-27T10:00:00Z"
}
}Errors:
| Status | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR |
Invalid request body (missing/malformed fields) |
| 401 | UNAUTHORIZED |
No valid session token |
| 403 | FORBIDDEN |
Session address ≠ callerAddress OR caller doesn't own commitment |
| 404 | NOT_FOUND |
Commitment does not exist |
| 409 | CONFLICT |
Commitment status prevents early exit (already settled/violated/exited) |
| 429 | TOO_MANY_REQUESTS |
Rate limit exceeded |
| 502 | BLOCKCHAIN_CALL_FAILED |
Blockchain RPC call failed |
| 504 | GATEWAY_TIMEOUT |
Blockchain operation timed out |
Contract-service failures are normalized before they are returned, so clients always receive the standard { success: false, error: ... } envelope with stable status codes.
Error Response Example (403 Forbidden):
{
"success": false,
"error": {
"code": "FORBIDDEN",
"message": "You do not own this commitment and cannot exit it early.",
"correlationId": "...",
"timestamp": "2026-05-27T10:00:00Z"
}
}Request:
curl -X POST http://localhost:3000/api/commitments/cm_123456/early-exit \
-H 'Content-Type: application/json' \
-H 'Cookie: session=valid-token-abc123' \
-d '{
"reason": "Need liquidity for unexpected investment",
"callerAddress": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
}'Success Response (200):
{
"success": true,
"data": {
"exitAmount": "950",
"penaltyAmount": "50",
"finalStatus": "EARLY_EXIT",
"txHash": "abc123def456",
"reference": null
},
"meta": {
"correlationId": "xyz789",
"timestamp": "2026-05-27T10:00:00Z"
}
}Ownership Violation (403):
{
"success": false,
"error": {
"code": "FORBIDDEN",
"message": "You do not own this commitment and cannot exit it early.",
"correlationId": "xyz789",
"timestamp": "2026-05-27T10:00:00Z"
}
}- Input Validation: Request body is validated against
EarlyExitRequestBodySchema(Zod) before processing. - Ownership Verification: After authentication, the route fetches the commitment from chain and verifies the owner matches the authenticated caller.
- Contract Interaction: Calls
earlyExitCommitmentOnChain()which:- Checks commitment status (must be ACTIVE, not SETTLED/VIOLATED/EARLY_EXIT).
- Submits transaction to Soroban contract.
- Returns penalty and exit amounts.
- Error Mapping: Contract errors are normalized via
normalizeBackendError()to ensure consistent error codes and messages. - Rate Limiting: All requests are subject to per-IP rate limiting
(
api/commitments/early-exit).
Returns the most recent attestations sorted by observedAt descending, with
page-based pagination metadata.
- Query parameters:
page: (integer, optional) Page number (1-based). Must be ≥ 1. Defaults to 1.pageSize: (integer, optional) Items per page. Must be 1–100. Defaults to 10.ownerAddress: (string, optional) Filter by commitment owner address. Requires a validAuthorization: Bearer <token>header.
- Response:
200 OKwith attestation list and pagination meta. - Error codes:
400 VALIDATION_ERROR—pageorpageSizeout of range, orownerAddressis blank.401 UNAUTHORIZED—ownerAddressprovided without a valid Bearer token.429 TOO_MANY_REQUESTS— Rate limit exceeded.
curl 'http://localhost:3000/api/attestations/recent?page=1&pageSize=2'{
"success": true,
"data": {
"attestations": [
{ "id": "ATT-005", "commitmentId": "CMT-005", "observedAt": "2026-04-24T10:00:00Z" },
{ "id": "ATT-004", "commitmentId": "CMT-004", "observedAt": "2026-04-23T10:00:00Z" }
],
"total": 5
},
"meta": {
"page": 1,
"pageSize": 2,
"total": 5,
"totalPages": 3,
"hasNextPage": true,
"hasPrevPage": false
}
}Filtered by owner (requires authentication):
curl 'http://localhost:3000/api/attestations/recent?ownerAddress=GAAA...WHF' \
-H 'Authorization: Bearer <token>'Records an attestation event. Stub implementation logs
AttestationReceived.
- Request body: JSON describing the attestation (e.g. signature, commitmentId).
- Response: stub message with requester IP.
curl -X POST http://localhost:3000/api/attestations \
-H 'Content-Type: application/json' \
-d '{"commitmentId":"abc123","status":"valid"}'{
"message": "Attestations recording endpoint stub - rate limiting applied",
"ip": "::1"
}Returns the authenticated owner's derived notification feed. Notifications are derived on-read from the owner's commitments and attestations (expiry warnings, violations, attestation health checks); they are not persisted.
The feed is filtered by the owner's notification delivery preferences. Each
notification has a type (expiry, violation, health_check), and only
types the owner has opted into are returned. Preferences are read from stored
user preferences (the notificationCategories field) and updated via the
PUT /api/user/preferences endpoint.
- Query parameters:
ownerAddress: (string, required) The Stellar address whose feed to return.page: (number, optional, default1) 1-indexed page number. Must be>= 1.pageSize: (number, optional, default10) Items per page. Must be1–100.
- Preference filtering:
- Notification categories the owner has set to
falseinnotificationCategoriesare excluded from the feed. - When no preferences are stored, or a category key is absent, the category is delivered by default (safe opt-in). An owner only stops receiving a category by explicitly opting out.
- Filtering is applied before pagination, so
totalreflects the count of notifications the owner can actually see — not the raw derived count.
- Notification categories the owner has set to
- Response:
200 OK: Paginated, preference-filtered feed.400 Bad Request:ownerAddressis missing, or pagination params are out of range.429 Too Many Requests: Rate limit exceeded.
curl 'http://localhost:3000/api/notifications?ownerAddress=0x123&page=1&pageSize=10'{
"success": true,
"data": {
"items": [
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"ownerAddress": "0x123",
"title": "Commitment Nearing Expiry",
"message": "Your commitment CMT-1 for XLM expires in 5 days.",
"severity": "warning",
"type": "expiry",
"read": false,
"createdAt": "2026-02-25T00:00:00.000Z",
"relatedCommitmentId": "CMT-1"
}
],
"page": 1,
"pageSize": 10,
"total": 1
}
}Returns the public protocol constants used by UX copy and calculations, including fee parameters, penalty tiers, and commitment limits. This endpoint is public and includes caching headers.
curl http://localhost:3000/api/protocol/constants{
"success": true,
"data": {
"protocolVersion": "v1",
"network": "Test SDF Network ; September 2015",
"fees": {
"networkBaseFeeStroops": 100,
"platformFeePercent": 0
},
"penalties": [...],
"commitmentLimits": { ... },
"cachedAt": "2026-02-25T00:00:00.000Z"
}
}Opens a dispute for the named commitment. Calls the escrow contract's
dispute method and records an audit log event.
- Path parameter:
id(string) — the commitment ID - Request body:
| Field | Type | Required | Description |
|---|---|---|---|
reason |
string | Yes | Reason for the dispute (1–500 characters) |
evidence |
string | No | Optional URL or reference to supporting evidence |
callerAddress |
string | No | Stellar address of the caller (defaults to commitment owner) |
- Response: dispute details including
disputeId,status, andtxHash.
curl -X POST http://localhost:3000/api/commitments/abc123/dispute \
-H 'Content-Type: application/json' \
-d '{"reason":"Payment not received","evidence":"https://example.com/proof"}'{
"success": true,
"data": {
"commitmentId": "abc123",
"disputeId": "DSP-001",
"status": "DISPUTED",
"txHash": "0xdispute123",
"disputedAt": "2026-05-28T14:00:00.000Z"
}
}| Status | Condition |
|---|---|
| 400 | Missing or invalid reason, empty commitment ID |
| 409 | Commitment already settled, exited, or already in dispute |
| 502 | Blockchain call failed |
Resolves an open dispute on a commitment. Admin access only — the caller
must authenticate with a valid Bearer token and the address must be listed in
ADMIN_ADDRESSES. Calls the escrow contract's resolve_dispute method and
records an audit log event.
- Path parameter:
id(string) — the commitment ID - Authentication: Bearer token required (admin only)
- Request body:
| Field | Type | Required | Description |
|---|---|---|---|
resolution |
enum | Yes | One of: resolved_in_favor_of_owner, resolved_in_favor_of_counterparty, dismissed |
notes |
string | No | Optional resolution notes (max 1000 characters) |
- Response: resolution details including
disputeId,resolution, andfinalStatus.
curl -X POST http://localhost:3000/api/commitments/abc123/resolve \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <session_token>' \
-d '{"resolution":"resolved_in_favor_of_owner","notes":"Evidence reviewed and validated"}'{
"success": true,
"data": {
"commitmentId": "abc123",
"disputeId": "DSP-001",
"resolution": "resolved_in_favor_of_owner",
"finalStatus": "ACTIVE",
"txHash": "0xresolve123",
"resolvedAt": "2026-05-28T14:30:00.000Z"
}
}| Status | Condition |
|---|---|
| 400 | Missing or invalid resolution, empty commitment ID, notes too long |
| 401 | Missing or invalid Bearer token |
| 403 | Caller is not an admin |
| 409 | Commitment is not currently in dispute |
| 502 | Blockchain call failed |
Server-Sent Events (SSE) stream that pushes real-time commitment status updates and transitions (Active, Settled, Early Exit, Violated).
- Path parameter:
id(string) - Headers:
Accept:text/event-stream(required)
- Security: Requires an authenticated session via browser cookies.
- Protocol Details:
- Snapshot: The server emits a
snapshotevent immediately upon connection carrying the current status. - Transitions: The server emits a
status_changeevent only when a status transition is detected on-chain. - Heartbeat: The server enqueues a comment heartbeat (
: keepalive) every 20 seconds to prevent intermediates (proxies, load balancers) from dropping the idle connection.
- Snapshot: The server emits a
event: snapshot
data: {"commitmentId":"abc123","status":"Active","timestamp":"2026-05-27T01:30:00.000Z"}
: keepalive
event: status_change
data: {"commitmentId":"abc123","status":"Settled","timestamp":"2026-05-27T01:30:15.000Z"}
- Automatic Reconnection: Standard browser
EventSourcehandles connection drops and reconnection attempts automatically. - Exponential Backoff: For non-browser clients or custom connection wrappers, implement exponential backoff on reconnection failures:
- Start with an initial delay of
1 second. - Double the delay on each consecutive failure (
2s,4s,8s,16s). - Cap the maximum delay at
30 secondsto protect server resources.
- Start with an initial delay of
- Graceful Fallback: When SSE is unsupported or fails repeatedly, clients should fall back to polling the lightweight
/api/commitments/[id]/statusroute at a standard, low-frequency interval (e.g., every 10–30 seconds).
Simple health/metrics endpoint used by monitoring tools.
- Response: JSON object containing uptime, mock request/error counts, and current timestamp.
curl http://localhost:3000/api/metrics{
"status": "up",
"uptime": 123.456,
"mock_requests_total": 789,
"mock_errors_total": 2,
"timestamp": "2026-02-25T00:00:00.000Z"
}🔧 This reference will grow as the backend implements real business logic.