-
Notifications
You must be signed in to change notification settings - Fork 0
API Reference
Tiana_ edited this page May 30, 2026
·
1 revision
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).
-
All requests:
Authorization: Bearer <jwt>(except/actuator/*and/v1/webhooks/*inbound) -
All mutating requests:
Idempotency-Key: <unique-string>header -
All requests:
X-Correlation-Idoptional, auto-generated, echoed back -
All responses:
application/jsonfor success,application/problem+jsonfor errors (RFC 7807) -
Pagination:
?cursor=<opaque>&limit=<1..200>. Response:{ items, nextCursor, hasMore } -
Versioning:
/v1/...URL-path
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"
}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 }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 usedcurl '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
}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| 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) |
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",
...
}curl 'http://localhost:8080/v1/payments/{id}?include=events' \
-H 'Authorization: Bearer ...'
# 200 OK with full event history if include=eventscurl 'http://localhost:8080/v1/payments?state=PROCESSING&limit=20' \
-H 'Authorization: Bearer ...'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 cancelIn 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.
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 followscurl 'http://localhost:8080/v1/compliance/cases?status=OPEN&priority=HIGH' \
-H 'Authorization: Bearer ...'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": [ ... ]
}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) appliedcurl -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..."
}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=DRAFTcurl -X POST http://localhost:8080/v1/decision/rules/{id}/activate \
-H 'Authorization: Bearer ...'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 createcurl -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 Createdimport 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())
}curl 'http://localhost:8080/v1/webhooks/subscriptions/{id}/deliveries?status=FAILED' \
-H 'Authorization: Bearer ...'
# 200 OK with delivery history (status, attempts, errors)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 invalidEvery 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}$" }
]
}| 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.
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",
)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' });Generated from OpenAPI spec automatically:
make api-export-postman # produces api/postman.jsonSingle 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.
- User-Flows - sequence diagrams for each endpoint
- Domain-Model - meaning of the resources
- Architecture-Security - auth and authorization
- Code-Rules - implementation conventions
- Overview
- Services
- Data Model
- Domain Model
- Event Flow
- Security
- Observability
- Resilience
- SLA / SLI / SLO