Skip to content

Integration conventions

Every server-to-server API core offers follows the same few rules. Learn them once and each integration section only has to describe its own endpoints.

Environment Core Guest accounts (Keycloak)
Test https://test-api.finnsbali.com https://test-auth.finnsbeachclub.com/realms/finns
Production to be announced to be announced

All traffic is HTTPS. Core refuses to start if a partner URL configured in it is not https://.

flowchart LR
  classDef ours fill:#13686C,stroke:#0B4A4D,color:#ffffff
  classDef ext fill:#F3ECDF,stroke:#B9A88B,color:#12191B

  P["Partner server"]:::ext
  CORE["Core"]:::ours
  P -- "Authorization: Bearer &lt;API token&gt;<br/>/api/v1/&lt;your prefix&gt;/…" --> CORE
  CORE -- "Authorization: Bearer &lt;revoke token&gt;<br/>POST /auth/revoke" --> P
  • Each integration has its own API prefix and its own bearer token, exchanged out of band. A token works only under its prefix.
  • Tokens are at least 32 characters. Keep them in a secret store, never in a client app or a repository.
  • When core calls you (for example the sign-out callback), it sends a second, different token in the Authorization header. Check that it matches the one you were given before acting on the request, and compare with a constant-time function so response timing can’t leak the token.
  • A wrong or missing token gets 401 with an empty body, on any path under the prefix, including paths that don’t exist. Core reveals nothing to an unauthenticated caller.

Errors are RFC 9457 problem details (application/problem+json). Switch on code: it is stable, while title is for humans and may change.

422 Unprocessable Content
{
"type": "about:blank",
"title": "Unprocessable Content",
"status": 422,
"code": "invalid",
"fields": { "totalIdr": "invalid", "bookingId": "required" }
}
code Status Meaning
malformed 400 Not valid JSON, or a required header or field is missing
not_found 404 Unknown id, or a code that is unknown, used, expired or meant for another partner. Core does not say which
insufficient_points 409 A debit would take the balance below zero
already_refunded / not_refundable 409 A refund that can’t be made
invalid 422 Field-level validation failed; fields maps each field to required or invalid
idempotency_mismatch 422 An Idempotency-Key reused with a different request
rate_limited 429 Too many requests; back off and retry
unavailable 503 A service core depends on is down; retry later

Core never translates messages: your product owns the wording guests see.

Anything that moves points is idempotent, so a retry after a timeout can never post twice.

Send an Idempotency-Key header (your own unique value, e.g. a UUID) on every credit, debit and refund.

  • Same key, same request: core replays the original answer and adds Idempotent-Replayed: true.
  • Same key, different request: 422 idempotency_mismatch.
  • Keys are scoped to you, so they can never collide with another partner’s or with staff adjustments.
  • Keys never expire.
  • Timestamps are ISO 8601 instants in UTC, e.g. 2026-10-13T02:30:00Z.
  • Business dates (reconciliation) are calendar days in Bali time, Asia/Makassar (UTC+8).
  • When you compare a time from core, compare it with another time from core, not with your own clock.
Rule Why
Serve it at /auth/revoke at the root of your origin Core resolves the path from the origin, ignoring any path in the configured URL
Answer exactly 200 Anything else, including 204, counts as a failure and is retried once
Check the bearer token in constant time It is the only thing proving the call is from core
Be idempotent The same revoke can arrive twice

Never log one-time codes, tokens, session ids, QR contents, emails or guest identifiers. Log your own request ids and the code of errors instead.

Internal and confidential. Share partner sections only as partner builds (pnpm build:partner).