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.
Environments
Section titled “Environments”| 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://.
Authentication
Section titled “Authentication”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 <API token><br/>/api/v1/<your prefix>/…" --> CORE CORE -- "Authorization: Bearer <revoke token><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
Authorizationheader. 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
401with an empty body, on any path under the prefix, including paths that don’t exist. Core reveals nothing to an unauthenticated caller.
Errors
Section titled “Errors”Errors are RFC 9457 problem details (application/problem+json). Switch on code: it is stable, while title is for humans and may change.
{ "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.
Idempotency
Section titled “Idempotency”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.
Some postings are keyed by a reference you already have, with no header:
- A paid booking is credited once per
bookingId. - A check-in is credited once per QR code.
Retrying with the same reference is always safe.
- 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.
Callbacks you host
Section titled “Callbacks you host”| 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 |
Logging
Section titled “Logging”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).

