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 - API prefix (canonical):
/api/v1/*— seeapi-versioning.md - Default media type:
application/json(the server returns415if youPOST/PUT/PATCHanything else) - Success envelope:
{ "data": <payload> | null, "error": null } - Error media type:
application/problem+json(RFC 7807) with stabletype+code— seedocs/error-envelope.md - Body limit: 100 kB (oversize returns
413) - Version header:
X-API-Version: 1on all/api/v1/*and legacy/api/*responses
Unversioned /api/* paths remain available during the transition window and
include Deprecation, Sunset, and Link: rel="successor-version" headers.
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, support tools |
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) uses RFC 7807 application/problem+json
with a stable taxonomy. Legacy data / error fields remain for older clients.
{
"type": "https://docs.creditra.dev/problems/validation_failed",
"title": "Validation Error",
"status": 400,
"detail": "Validation failed",
"code": "validation_failed",
"details": [
{ "field": "walletAddress", "message": "Invalid Stellar address" }
],
"data": null,
"error": "Validation failed"
}| Category | Codes |
|---|---|
| Validation | validation_failed |
| Auth | unauthorized, forbidden |
| Not found | not_found |
| Conflict | duplicate_resource, version_conflict, invalid_state_transition, unique_constraint_violation |
| Rate limited | rate_limited (+ retryAfter / Retry-After) |
| Upstream | upstream_failure, upstream_timeout |
| Other | payload_too_large, unsupported_media_type, service_unavailable, internal_error |
See docs/error-envelope.md for the full contract and helpers.
| Code | When |
|---|---|
| 200 | Successful read or non-creation action |
| 201 | Resource created |
| 202 | Asynchronous accept (e.g. reconciliation trigger) |
| 204 | Successful delete |
| 304 | Not Modified — conditional GET matched If-None-Match (see etag-caching.md) |
| 400 | Schema validation failed |
| 401 | Auth header missing |
| 403 | Auth header present but invalid |
| 404 | Resource not found |
| 409 | Conflict: invalid state transition, optimistic-lock version mismatch, or duplicate resource (problem+json; see error-envelope.md) |
| 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) |
Presence of the cursor query param (even empty) selects cursor mode.
| Param | Type | Default | Bounds |
|---|---|---|---|
cursor |
string | — | opaque |
limit |
int | 25 (varies by endpoint) | 1–100 |
Response pagination block:
{ "limit": 25, "nextCursor": "<opaque>|null", "hasMore": true }Applied to:
GET /api/credit/linesGET /api/credit/lines/:id/transactionsGET /api/admin/api-keysandGET /api/admin/api-keys/auditGET /api/webhooks/deliveries
Cursors are opaque base64url payloads; clients must pass nextCursor back
verbatim. Full details: docs/cursor-pagination.md.
| Param | Type | Default | Bounds |
|---|---|---|---|
offset |
int | 0 | ≥ 0 |
limit |
int | 20 | 1–100 |
page (transactions only) |
int | 1 | ≥ 1 |
Used when cursor is omitted on credit-line list, transaction history, and
risk history.
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)cursor,limit(standard) orpage,limit(legacy)
Read-heavy endpoints emit an ETag and honour If-None-Match with 304 Not Modified:
GET /api/credit/lines/:idGET /api/credit/lines/:id/transactionsGET /api/dashboard/summary
Responses include Cache-Control: private, must-revalidate. Full semantics, client examples, and security notes live in docs/etag-caching.md.
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 / domain error message;409problem+json (duplicate_resource) when an open credit line already exists for the wallet.
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, pluscursor/limit(standard) orpage/limit(legacy). 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 }.
List anomaly risk signals (rapid draws, draw bursts, unusual repay patterns)
for operator review. Signals are advisory only — see
ANOMALY_DETECTION.md for rules and thresholds.
- Auth:
X-API-Key. - Query (
riskSignalsQuerySchema, all optional):walletAddress,creditLineId,signalType,status,correlationId,offset,limit. - Response 200:
{ data: { signals, total, offset, limit }, error: null }.
- Auth:
X-API-Key. - 404:
Risk signal not found.
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.
Subscriber implementation details, HMAC verification code, timestamp checks,
and idempotency guidance are documented in
webhook-subscribers.md.
Reachability probe for every configured URL. Returns { total, reachable, unreachable, results[] }.
Returns active outbound webhook subscriber metadata without secret material.
Returns recent durable delivery rows. Optional query parameters:
status:queued,delivered,failed, ordead_letterlimit: 1-200
Requeues a stored delivery for asynchronous retry and returns 202 with the
new job id.
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: <payload ISO timestamp>
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
X-Webhook-Timestampfalls outside your tolerance window. - Deduplicate by
data.drawId.
Webhook delivery settings expose retry and backoff knobs. Implement idempotency on receive so repeated deliveries are safe.
Implemented in src/routes/inboundWebhooks.ts.
- Auth: HMAC headers only (
X-Signature,X-Timestamp,X-Nonce). No API key. - Secret:
INBOUND_WEBHOOK_SECRET(503 when unset). - Signed payload:
X-Timestamp + "." + X-Nonce + "." + raw_body. - Replay: nonce TTL cache; duplicate nonces →
401 Replay detected. - Response 202:
{ data: { accepted: true, event }, error: null }.
Partner signing guide: docs/webhooks.md.
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 }.
See COMPLIANCE_EXPORTS.md for full detail. Summary:
| Method | Path | Auth |
|---|---|---|
GET |
/api/admin/exports/credit-lines |
X-Admin-Api-Key |
GET |
/api/admin/exports/transactions |
X-Admin-Api-Key |
GET |
/api/admin/exports/audit |
X-Admin-Api-Key |
Required query: from, to (max 90-day span). Optional: format=json|csv, limit (max 5000), offset, plus resource filters. Responses stream JSON envelopes or CSV attachments.
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1718243400 # epoch seconds when the bucket is next full
X-RateLimit-Bypass: admin # only when X-Admin-Api-Key bypass applied
Retry-After: 12 # only on 429 — seconds until ≥1 token
Defaults: RATE_LIMIT_WINDOW_MS=60000, RATE_LIMIT_MAX_REQUESTS=100, RATE_LIMIT_MAX_EVALUATE=10 (the risk endpoint is more expensive), RATE_LIMIT_MAX_EXPORT=5 (compliance exports).
- 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.