Skip to content

Latest commit

 

History

History
441 lines (300 loc) · 14.3 KB

File metadata and controls

441 lines (300 loc) · 14.3 KB

API Reference

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/* — see api-versioning.md
  • Default media type: application/json (the server returns 415 if you POST/PUT/PATCH anything else)
  • Success envelope: { "data": <payload> | null, "error": null }
  • Error media type: application/problem+json (RFC 7807) with stable type + code — see docs/error-envelope.md
  • Body limit: 100 kB (oversize returns 413)
  • Version header: X-API-Version: 1 on 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.


1. Authentication

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_KEY configured → 503 (fail closed).

Read endpoints are public-by-design but rate-limited.


2. Error envelope (problem+json)

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.

Status code semantics

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)

3. Pagination & filtering conventions

Cursor (standard — preferred for all list endpoints)

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/lines
  • GET /api/credit/lines/:id/transactions
  • GET /api/admin/api-keys and GET /api/admin/api-keys/audit
  • GET /api/webhooks/deliveries

Cursors are opaque base64url payloads; clients must pass nextCursor back verbatim. Full details: docs/cursor-pagination.md.

Offset / page (legacy, still supported)

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.

Filtering — transactions

GET /api/credit/lines/:id/transactions accepts:

  • type ∈ borrow | repay | interest_accrual | fee | status_change
  • from, to — ISO-8601 date strings (new Date(from).getTime() must be valid)
  • cursor, limit (standard) or page, limit (legacy)

Conditional GET (ETag)

Read-heavy endpoints emit an ETag and honour If-None-Match with 304 Not Modified:

  • GET /api/credit/lines/:id
  • GET /api/credit/lines/:id/transactions
  • GET /api/dashboard/summary

Responses include Cache-Control: private, must-revalidate. Full semantics, client examples, and security notes live in docs/etag-caching.md.


4. Endpoint inventory

Health

GET /health

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.


Credit Lines

Implemented in src/routes/credit.ts, backed by CreditLineService and the in-memory creditService helpers.

GET /api/credit/lines

List all credit lines (in-memory store list).

  • Auth: none
  • Response 200: { data: CreditLine[], error: null }

GET /api/credit/lines/:id

  • 404: Credit line "<id>" not found.

POST /api/credit/lines

  • Body (validated by createCreditLineSchema):
    {
      "walletAddress": "GDRXE2BQUC...",
      "requestedLimit": "1000.00",
      "interestRateBps": 640
    }
  • Validation: wallet must satisfy ^G[A-Z2-7]{55}$. Either creditLimit or requestedLimit is required.
  • Response 201: newly created CreditLine.
  • Errors: 400 on validation / domain error message; 409 problem+json (duplicate_resource) when an open credit line already exists for the wallet.

PUT /api/credit/lines/:id

Patches creditLimit, interestRateBps, or status.

  • 404: Credit line not found.

DELETE /api/credit/lines/:id

  • Response 204: No body.

GET /api/credit/wallet/:walletAddress/lines

  • Validation: Stellar address.
  • Response 200: { creditLines: CreditLine[] }

GET /api/credit/lines/:id/transactions

Filterable transaction history.

  • Query: type, from, to, plus cursor/limit (standard) or page/limit (legacy). See §3.
  • Errors: 400 for any bad filter; 404 if line not found.

POST /api/credit/lines/:id/draw

  • Body (drawSchema): { walletAddress, amount } — amount is a decimal string.
  • Response 200: draw result (status pending until Horizon confirms).

POST /api/credit/lines/:id/repay

  • Body (repaySchema): { walletAddress, amount }.
  • Response 200: repay result.

POST /api/credit/lines/:id/suspend (admin)

  • Auth: X-Admin-Api-Key.
  • Response 200: { data: CreditLine, message: 'Credit line suspended.', error: null }
  • 409: Invalid status transition.

POST /api/credit/lines/:id/close (admin)

  • Same envelope as suspend.

Risk

Implemented in src/routes/risk.ts, backed by RiskEvaluationService and the pluggable provider factory.

POST /api/risk/evaluate

  • Body (riskEvaluateSchema):
    { "walletAddress": "G...", "forceRefresh": false }
  • Behavior: returns the cached evaluation when fresh (< 24 h). forceRefresh: true forces a re-evaluation.
  • Response 200: RiskEvaluation (id, walletAddress, riskScore 0–100, creditLimit, interestRateBps, factors[], evaluatedAt, expiresAt).

GET /api/risk/evaluations/:id

  • 404: Risk evaluation not found.

GET /api/risk/wallet/:walletAddress/latest

  • 404: No risk evaluation found for wallet.

GET /api/risk/wallet/:walletAddress/history

  • Query: offset, limit (validated by riskHistoryQuerySchema).
  • Response 200: { data: { evaluations: RiskEvaluation[] }, error: null }.

GET /api/risk/admin/signals (API-key auth)

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 }.

GET /api/risk/admin/signals/:id (API-key auth)

  • Auth: X-API-Key.
  • 404: Risk signal not found.

POST /api/risk/admin/recalibrate (API-key auth)

Hook for triggering a recalibration of the risk model.

  • Auth: X-API-Key.

Webhooks

Implemented in src/routes/webhook.ts. These describe the server's outbound webhook fan-out, not inbound webhooks.

GET /api/webhooks/config

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.

POST /api/webhooks/test

Reachability probe for every configured URL. Returns { total, reachable, unreachable, results[] }.

GET /api/webhooks/health

GET /api/webhooks/subscriptions (API-key auth)

Returns active outbound webhook subscriber metadata without secret material.

GET /api/webhooks/deliveries (API-key auth)

Returns recent durable delivery rows. Optional query parameters:

  • status: queued, delivered, failed, or dead_letter
  • limit: 1-200

POST /api/webhooks/deliveries/:id/replay (API-key auth)

Requeues a stored delivery for asynchronous retry and returns 202 with the new job id.

active | disabled — disabled when no URLs are configured.

Outbound payload contract (subscriber side)

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:

  1. Re-compute HMAC-SHA256(body, secret) and compare in constant time.
  2. Reject when X-Webhook-Timestamp falls outside your tolerance window.
  3. Deduplicate by data.drawId.

Webhook delivery settings expose retry and backoff knobs. Implement idempotency on receive so repeated deliveries are safe.

Inbound partner webhooks

Implemented in src/routes/inboundWebhooks.ts.

POST /api/inbound-webhooks/events

  • 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.


Reconciliation

Implemented in src/routes/reconciliation.ts, all admin-gated.

POST /api/reconciliation/trigger

  • Auth: X-API-Key.
  • Response 202: { data: { jobId, message }, error: null }.

GET /api/reconciliation/status

  • Auth: X-API-Key.
  • Response 200: { data: { workerRunning, queueSize, failedJobs }, error: null }.

4.x Compliance exports (admin)

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.


5. Rate-limit headers (token bucket)

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).


6. Idempotency

  • Inbound writes: the events table enforces a partial-unique index on idempotency_key. Client-supplied keys can be wired into command handlers when needed.
  • Outbound webhooks: every event carries a stable drawId derived from on-chain identifiers.
  • Indexer: SHA-256 over ledger || contractId || topics || data produces an eventId deduplicated across polls via a 10 000-entry LRU set.

7. Generated client tips

  • The operationId field in openapi.yaml is stable — use it as the function name for any generator.
  • npm run validate:spec parses the YAML in CI to catch structural drift early.
  • Tags are: Health, Credit, Risk, Webhooks — useful for grouping in SDK output.