Skip to content

API Reference

Tiana_ edited this page May 30, 2026 · 1 revision

API Reference

Tour of every public REST endpoint with curl examples, error codes, idempotency notes. Source of truth: api/openapi.yaml. Interactive Swagger UI: http://localhost:8080/swagger-ui.html (sandbox).

Conventions

  • All requests: Authorization: Bearer <jwt> (except /actuator/* and /v1/webhooks/* inbound)
  • All mutating requests: Idempotency-Key: <unique-string> header
  • All requests: X-Correlation-Id optional, auto-generated, echoed back
  • All responses: application/json for success, application/problem+json for errors (RFC 7807)
  • Pagination: ?cursor=<opaque>&limit=<1..200>. Response: { items, nextCursor, hasMore }
  • Versioning: /v1/... URL-path

API: Ledger

Create account

curl -X POST http://localhost:8080/v1/accounts \
  -H 'Authorization: Bearer eyJhbGc...' \
  -H 'Idempotency-Key: my-account-1' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "User Wallet - Alice",
    "type": "USER_WALLET",
    "currency": "EUR",
    "metadata": { "user_id": "01HX...", "tier": "premium" }
  }'

# 201 Created
{
  "id": "acc_01HX...",
  "name": "User Wallet - Alice",
  "type": "USER_WALLET",
  "currency": "EUR",
  "status": "ACTIVE",
  "metadata": { "user_id": "01HX...", "tier": "premium" },
  "version": 0,
  "createdAt": "2026-04-25T10:00:00Z",
  "updatedAt": "2026-04-25T10:00:00Z"
}

Get balance (current and historical)

curl http://localhost:8080/v1/accounts/{id}/balance \
  -H 'Authorization: Bearer ...'

# Time-travel - Killer Feature
curl 'http://localhost:8080/v1/accounts/{id}/balance?asOf=2026-03-15T12:00:00Z' \
  -H 'Authorization: Bearer ...'

# 200 OK
{ "accountId": "acc_01HX...", "currency": "EUR", "balance": "150.00", "asOf": "2026-04-25T10:00:00Z", "version": 12 }

Post double-entry transaction

curl -X POST http://localhost:8080/v1/transactions \
  -H 'Authorization: Bearer ...' \
  -H 'Idempotency-Key: tx-001' \
  -H 'Content-Type: application/json' \
  -d '{
    "reference": "transfer-001",
    "description": "Transfer from A to B",
    "entries": [
      { "accountId": "acc_A", "amount": "-100.00", "currency": "EUR", "direction": "DEBIT" },
      { "accountId": "acc_B", "amount":  "100.00", "currency": "EUR", "direction": "CREDIT" }
    ]
  }'

# 201 Created - full Transaction with entries
# 422 Unprocessable Entity if SUM != 0 or accounts inactive
# 409 Conflict if reference already used

List entries (paginated)

curl 'http://localhost:8080/v1/accounts/{id}/entries?limit=50&from=2026-04-01T00:00:00Z' \
  -H 'Authorization: Bearer ...'

# 200 OK
{
  "items": [ { ... }, { ... } ],
  "nextCursor": "eyJhbW91bnQ...",
  "hasMore": true
}

Reverse a transaction

curl -X POST http://localhost:8080/v1/transactions/{id}/reverse \
  -H 'Authorization: Bearer ...' \
  -H 'Idempotency-Key: rev-tx-001' \
  -H 'Content-Type: application/json' \
  -d '{ "reason": "Customer dispute resolved" }'

# 201 Created - compensating transaction

Errors

Status Cause
400 Validation failed (missing field, invalid currency, etc.)
401 Missing or invalid JWT
403 Token doesn't have required role/scope
404 Account/transaction not found
409 Idempotency conflict (same key, different body), or reference collision
422 Invariant violation (SUM ≠ 0, mixed currencies, account inactive)
429 Rate limit exceeded (with Retry-After)

API: Payments

Initiate payment

curl -X POST http://localhost:8080/v1/payments \
  -H 'Authorization: Bearer ...' \
  -H 'Idempotency-Key: pay-001' \
  -H 'Content-Type: application/json' \
  -d '{
    "fromAccountId": "acc_01HX...",
    "externalCounterparty": {
      "name": "Acme Corp",
      "iban": "DE89370400440532013000",
      "bic": "COBADEFFXXX",
      "country": "DE"
    },
    "amount": "1000.00",
    "currency": "EUR",
    "reference": "Invoice #INV-2026-0042"
  }'

# 202 Accepted - payment created (state: PROCESSING / REJECTED / PENDING_REVIEW)
{
  "id": "pay_01HX...",
  "state": "PROCESSING",
  "amount": "1000.00",
  "currency": "EUR",
  ...
}

Get payment

curl 'http://localhost:8080/v1/payments/{id}?include=events' \
  -H 'Authorization: Bearer ...'

# 200 OK with full event history if include=events

List payments (filters)

curl 'http://localhost:8080/v1/payments?state=PROCESSING&limit=20' \
  -H 'Authorization: Bearer ...'

Cancel payment

curl -X POST http://localhost:8080/v1/payments/{id}/cancel \
  -H 'Authorization: Bearer ...' \
  -H 'Idempotency-Key: cancel-pay-001'

# 200 OK if state was CREATED or PENDING_REVIEW
# 409 Conflict if state doesn't allow cancel

Sandbox bank simulator

In sandbox, the bank adapter responds based on the amount:

Amount Result
Ends in .00 success after 100ms
Ends in .99 timeout (retryable)
Ends in .01 permanent failure (rejected)
Ends in .42 callback after 5 sec via webhook

Useful for end-to-end tests in CI.


API: Compliance

Start KYC session

curl -X POST http://localhost:8080/v1/kyc/sessions \
  -H 'Authorization: Bearer ...' \
  -H 'Idempotency-Key: kyc-001' \
  -d '{
    "userId": "01HX...",
    "provider": "sandbox",
    "returnUrl": "https://app.example.com/onboarding/complete"
  }'

# 201 Created with hostedUrl that user follows

List compliance cases

curl 'http://localhost:8080/v1/compliance/cases?status=OPEN&priority=HIGH' \
  -H 'Authorization: Bearer ...'

Get case with AI explanation

curl 'http://localhost:8080/v1/compliance/cases/{id}' \
  -H 'Authorization: Bearer ...'

# 200 OK
{
  "id": "case_01HX...",
  "status": "OPEN",
  "alertId": "alert_01HX...",
  "paymentId": "pay_01HX...",
  "priority": "HIGH",
  "aiExplanation": "This payment was flagged because ...",  # LLM-generated, optional
  "draftReport": "Customer Subject ... Date ...",            # LLM-drafted SAR
  "notes": [ ... ]
}

Resolve case

curl -X POST http://localhost:8080/v1/compliance/cases/{id}/resolve \
  -H 'Authorization: Bearer ...' \
  -H 'Idempotency-Key: resolve-case-001' \
  -d '{
    "decision": "REJECTED",
    "reason": "Sanctions list match: SDN. Cannot proceed."
  }'

# 200 OK - case resolved, side-effects (reverse transaction if linked) applied

API: Decision Engine

Evaluate decision (the hot path)

curl -X POST http://localhost:8080/v1/decision/evaluate \
  -H 'Authorization: Bearer ...' \
  -d '{
    "ruleSetId": "payment-screening",
    "context": {
      "amount": { "amount": "15000", "currency": "EUR" },
      "destination": { "country": "NG" },
      "user": { "ageDays": 45, "kycStatus": "APPROVED" }
    }
  }'

# 200 OK
{
  "decision": "REVIEW",
  "matchedRules": [
    {
      "ruleId": "rule_01HX...",
      "ruleVersion": 3,
      "priority": 100,
      "explanation": "Amount > 10000 EUR + country in high-risk list"
    }
  ],
  "explanation": "high_amount_high_risk_country",
  "latencyMs": 4,
  "decisionLogId": "log_01HX..."
}

Create decision rule

curl -X POST http://localhost:8080/v1/decision/rules \
  -H 'Authorization: Bearer ...' \
  -H 'Idempotency-Key: rule-001' \
  -d '{
    "ruleSetId": "payment-screening",
    "name": "High amount foreign country",
    "priority": 100,
    "terminate": false,
    "definition": {
      "conditions": {
        "all": [
          { "field": "amount.amount", "op": ">", "value": 10000 },
          { "field": "destination.country", "op": "in", "value": ["NG", "PK", "RU"] }
        ]
      },
      "action": { "decision": "REVIEW", "reason": "high_amount_high_risk_country" }
    }
  }'

# 201 Created with status=DRAFT

Activate rule

curl -X POST http://localhost:8080/v1/decision/rules/{id}/activate \
  -H 'Authorization: Bearer ...'

LLM-powered rule synthesis (optional, requires AmlCopilot configured)

curl -X POST http://localhost:8080/v1/decision/rules/synthesize \
  -H 'Authorization: Bearer ...' \
  -d '{
    "naturalLanguage": "Block transactions over $10,000 from new users in high-risk countries"
  }'

# Returns a DraftRule the operator can review and create

API: Webhooks

Subscribe to events

curl -X POST http://localhost:8080/v1/webhooks/subscriptions \
  -H 'Authorization: Bearer ...' \
  -d '{
    "url": "https://my-app.example.com/webhooks/fincore",
    "events": ["payment.completed", "payment.failed", "aml.flagged"],
    "secret": "min-32-char-secret-stored-on-our-side"
  }'

# 201 Created

Verify a webhook delivery (subscriber side)

import hmac, hashlib

def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header)
fun verify(rawBody: ByteArray, signatureHeader: String, secret: String): Boolean {
    val mac = Mac.getInstance("HmacSHA256")
    mac.init(SecretKeySpec(secret.toByteArray(), "HmacSHA256"))
    val expected = "sha256=" + mac.doFinal(rawBody).joinToString("") { "%02x".format(it) }
    return MessageDigest.isEqual(expected.toByteArray(), signatureHeader.toByteArray())
}

Inspect deliveries

curl 'http://localhost:8080/v1/webhooks/subscriptions/{id}/deliveries?status=FAILED' \
  -H 'Authorization: Bearer ...'

# 200 OK with delivery history (status, attempts, errors)

Inbound webhooks (provider → us)

Payment provider callback

curl -X POST http://localhost:8080/v1/webhooks/payments/sandbox \
  -H 'X-Provider-Signature: sha256=...' \
  -H 'X-Provider-Event-Id: evt-12345' \
  -H 'Content-Type: application/json' \
  -d '{ "paymentRef": "pay_01HX...", "status": "COMPLETED", "providerRef": "BNK-..." }'

# Signature verified against per-provider HMAC secret
# 200 OK on success or duplicate; 401 if signature invalid

Common error format (RFC 7807)

Every 4xx/5xx returns:

{
  "type": "https://docs.fincore.dev/errors/insufficient-balance",
  "title": "Insufficient balance",
  "status": 422,
  "detail": "Account abc-123 has 50.00 EUR, transfer requires 100.00 EUR",
  "instance": "/v1/payments",
  "correlationId": "01HX..."
}

For validation errors, errors array provides field-level detail:

{
  "type": "https://docs.fincore.dev/errors/validation",
  "title": "Validation failed",
  "status": 400,
  "errors": [
    { "field": "amount", "code": "invalid", "message": "must be positive" },
    { "field": "currency", "code": "pattern", "message": "must match ^[A-Z]{3}$" }
  ]
}

Rate limits

Tier Limit
Per-IP 100 req/sec, burst 200
Per-user 1000 req/sec
Per-endpoint webhook 10 req/sec

Headers in every response:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1714050300

429 with Retry-After: <seconds> when exceeded.


SDK examples

Kotlin (planned, Phase 4)

val client = FinCoreClient(baseUrl = "http://localhost:8080", token = "...")
val account = client.ledger.createAccount(
    name = "Alice wallet",
    type = AccountType.USER_WALLET,
    currency = "EUR",
    idempotencyKey = "init-001",
)
val tx = client.ledger.postTransaction(
    PostTransactionCommand(
        reference = "test-1",
        entries = listOf(
            Entry(account.id, BigDecimal("-100"), "EUR", DEBIT),
            Entry(other.id, BigDecimal("100"), "EUR", CREDIT),
        ),
    ),
    idempotencyKey = "tx-1",
)

TypeScript (planned, Phase 4)

const client = new FinCoreClient({ baseUrl: 'http://localhost:8080', token: '...' });
const account = await client.ledger.createAccount({
  name: 'Alice wallet',
  type: 'USER_WALLET',
  currency: 'EUR',
}, { idempotencyKey: 'init-001' });

Postman / Insomnia collections

Generated from OpenAPI spec automatically:

make api-export-postman   # produces api/postman.json

OpenAPI spec

Single source of truth: api/openapi.yaml in the repo. Validated on every CI run. Used to generate:

  • Swagger UI / ReDoc
  • Server stubs (verify our impl matches)
  • Client SDKs
  • Postman collection

Breaking changes require a new major version path (/v2/...) and a 12-month deprecation cycle on /v1.


Related reading

Clone this wiki locally