Human-readable companion to the machine-readable spec at src/openapi.yaml (served live at /docs and /docs.json). When in doubt, the YAML is authoritative.
- Base URL (dev):
http://localhost:3000 - Default media type:
application/json(the server returns415if youPOST/PUT/PATCHanything else) - Response envelope:
{ "data": <payload> | null, "error": <string> | null } - Body limit: 100 kB (oversize returns
413)
Two perpendicular headers:
| Header | Used by | Backed by |
|---|---|---|
X-API-Key |
Risk admin, reconciliation admin | src/middleware/auth.ts |
X-Admin-Api-Key |
Credit-line suspend / close |
src/middleware/adminAuth.ts |
- API keys are compared in constant time via
crypto.timingSafeEqual. - Missing →
401, present-but-wrong →403. - Admin endpoint with no
ADMIN_API_KEYconfigured →503(fail closed).
Read endpoints are public-by-design but rate-limited.
Every error response (4xx, 5xx) has this shape:
{
"data": null,
"error": "<human readable summary>"
}Validation errors additionally include details:
{
"data": null,
"error": "Validation failed",
"details": [
{ "field": "walletAddress", "message": "Invalid Stellar address" }
]
}Rate-limit responses additionally include retryAfter and the Retry-After HTTP header. See docs/error-envelope.md for the helper API.
| Code | When |
|---|---|
| 200 | Successful read or non-creation action |
| 201 | Resource created |
| 202 | Asynchronous accept (e.g. reconciliation trigger) |
| 204 | Successful delete |
| 400 | Schema validation failed |
| 401 | Auth header missing |
| 403 | Auth header present but invalid |
| 404 | Resource not found |
| 409 | Invalid state transition (e.g. close-of-closed) |
| 413 | Body > 100 kB |
| 415 | Mutating request lacked application/json |
| 429 | Rate limit exhausted |
| 500 | Internal error (no stack leaked) |
| 503 | Service unconfigured (e.g. admin key missing) |
Two pagination styles ship — pick the one the endpoint advertises in its query schema.
| Param | Type | Default | Bounds |
|---|---|---|---|
offset |
int | 0 | ≥ 0 |
limit |
int | 20 | 1–100 |
page (transactions only) |
int | 1 | ≥ 1 |
CreditLineService.getAllCreditLinesWithCursor(cursor?, limit?) returns { items, nextCursor }. Cursor is an opaque string; clients should pass it back verbatim. See docs/cursor-pagination.md.
GET /api/credit/lines/:id/transactions accepts:
type∈borrow | repay | interest_accrual | fee | status_changefrom,to— ISO-8601 date strings (new Date(from).getTime()must be valid)page,limit
Liveness + dependency probe.
- Auth: none
- Response 200:
{ "data": { "status": "ok", "service": "creditra-backend", "ready": true, "dependencies": { "database": { "status": "ok" }, "horizon": { "status": "ok" } } }, "error": null } - Dependency states:
ok | unconfigured | degraded. Both DB and Horizon are probed with their own timeouts (1 s and 2 s respectively).
Webhook health: GET /api/webhooks/health.
Implemented in src/routes/credit.ts, backed by CreditLineService and the in-memory creditService helpers.
List all credit lines (in-memory store list).
- Auth: none
- Response 200:
{ data: CreditLine[], error: null }
- 404:
Credit line "<id>" not found.
- Body (validated by
createCreditLineSchema):{ "walletAddress": "GDRXE2BQUC...", "requestedLimit": "1000.00", "interestRateBps": 640 } - Validation: wallet must satisfy
^G[A-Z2-7]{55}$. EithercreditLimitorrequestedLimitis required. - Response 201: newly created
CreditLine. - Errors:
400on validation,400on domain error message.
Patches creditLimit, interestRateBps, or status.
- 404: Credit line not found.
- Response 204: No body.
- Validation: Stellar address.
- Response 200:
{ creditLines: CreditLine[] }
Filterable transaction history.
- Query:
type,from,to,page,limit(see §3). - Errors:
400for any bad filter;404if line not found.
- Body (
drawSchema):{ walletAddress, amount }—amountis a decimal string. - Response 200: draw result (status
pendinguntil Horizon confirms).
- Body (
repaySchema):{ walletAddress, amount }. - Response 200: repay result.
- Auth:
X-Admin-Api-Key. - Response 200:
{ data: CreditLine, message: 'Credit line suspended.', error: null } - 409: Invalid status transition.
- Same envelope as
suspend.
Implemented in src/routes/risk.ts, backed by RiskEvaluationService and the pluggable provider factory.
- Body (
riskEvaluateSchema):{ "walletAddress": "G...", "forceRefresh": false } - Behavior: returns the cached evaluation when fresh (< 24 h).
forceRefresh: trueforces a re-evaluation. - Response 200:
RiskEvaluation(id, walletAddress, riskScore 0–100, creditLimit, interestRateBps, factors[], evaluatedAt, expiresAt).
- 404:
Risk evaluation not found.
- 404:
No risk evaluation found for wallet.
- Query:
offset,limit(validated byriskHistoryQuerySchema). - Response 200:
{ data: { evaluations: RiskEvaluation[] }, error: null }.
Hook for triggering a recalibration of the risk model.
- Auth:
X-API-Key.
Implemented in src/routes/webhook.ts. These describe the server's outbound webhook fan-out, not inbound webhooks.
Returns subscriber URLs, retry/backoff settings, and timeout — never the secret.
Reachability probe for every configured URL. Returns { total, reachable, unreachable, results[] }.
active | disabled — disabled when no URLs are configured.
POST <subscriber URL> with:
Content-Type: application/json
X-Webhook-Signature: sha256=<hex HMAC>
X-Webhook-Timestamp: <epoch ms>
User-Agent: Creditra-Webhook/1.0{
"event": "draw_confirmed",
"timestamp": "2024-01-01T00:00:00.000Z",
"data": {
"ledger": 123456,
"contractId": "C…",
"drawAmount": "100.00",
"drawId": "draw_…",
"borrowerWallet": "G…",
"creditLineId": "cl_…",
"horizonTimestamp": "2024-01-01T00:00:00Z"
}
}HMAC is computed over the raw JSON body with WEBHOOK_SECRET. Subscribers must:
- Re-compute
HMAC-SHA256(body, secret)and compare in constant time. - Reject when
now - X-Webhook-Timestampexceeds your tolerance window. - Deduplicate by
data.drawId.
Server retries up to WEBHOOK_MAX_RETRIES + 1 times with exponential backoff — implement idempotency on receive.
Implemented in src/routes/reconciliation.ts, all admin-gated.
- Auth:
X-API-Key. - Response 202:
{ data: { jobId, message }, error: null }.
- Auth:
X-API-Key. - Response 200:
{ data: { workerRunning, queueSize, failedJobs }, error: null }.
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1718243400 # epoch seconds
Retry-After: 12 # only on 429
Defaults: RATE_LIMIT_WINDOW_MS=60000, RATE_LIMIT_MAX_REQUESTS=100, RATE_LIMIT_MAX_EVALUATE=10 (the risk endpoint is more expensive).
- Inbound writes: the
eventstable enforces a partial-unique index onidempotency_key. Client-supplied keys can be wired into command handlers when needed. - Outbound webhooks: every event carries a stable
drawIdderived from on-chain identifiers. - Indexer: SHA-256 over
ledger || contractId || topics || dataproduces aneventIddeduplicated across polls via a 10 000-entry LRU set.
- The
operationIdfield inopenapi.yamlis stable — use it as the function name for any generator. npm run validate:specparses the YAML in CI to catch structural drift early.- Tags are:
Health,Credit,Risk,Webhooks— useful for grouping in SDK output.