OI Payments Docs

Authentication

How applications authenticate with an API key + secret, how credentials rotate and revoke, and how admin sessions differ.

The service has two parallel authentication mechanisms. As an integrating app you use the API key + secret. Operators using the dashboard use an admin session — its login, logout, profile, step-up re-auth, and password-reset endpoints are documented below. Authorization for individual admin actions is covered in the RBAC guide.

App API key + secret

Every app-facing request carries two headers:

X-Api-Key: oi_test_xxx
X-Api-Secret: your-secret
  • X-Api-Key is a deterministic lookup handle. The service stores only its SHA-256 digest, so the key resolves a credential row without the raw value being recoverable.
  • X-Api-Secret is verified against a bcrypt hash after the key resolves. Both must be valid.

The resolved credential determines the app and the mode (TEST/LIVE). The mode is never read from the request body — a oi_test_… key always operates on test data.

Send credentials only over HTTPS and only from your backend. Never embed the secret in a browser, mobile app, or anything a customer can inspect.

A credential is one of many

An app can hold more than one credential per mode. That is what makes rotation and revocation non-disruptive: each credential is an independent row that can expire or be revoked without touching the others.

Credential statusBehaviour
ACTIVE, no expiryAuthenticates normally.
ACTIVE, with expiresAtStill authenticates until the grace deadline passes (rotated-out key).
REVOKEDRejected immediately.

Rotation (zero-downtime)

When a credential is rotated, a new one is issued immediately and the old one is stamped with an expiresAt (a grace window, 24h by default). Both keys work during the window, so you can roll your deployment over with no downtime:

  1. Operator rotates the credential; you receive the new key + secret (shown once).
  2. Deploy the new key alongside the old — both authenticate.
  3. Once all traffic uses the new key, let the old one expire (or have it revoked).

Revocation

Revoking a credential takes effect immediately — the next request with it is rejected with UNAUTHORIZED. Use this if a secret may be compromised; issue a replacement by rotating first if you need continuity.

Admin sessions

The dashboard authenticates operators, not apps. An operator logs in with email

  • password and receives an opaque session token to send as a bearer header on every subsequent admin call:
Authorization: Bearer SESSION_TOKEN

App integrations never deal with admin sessions — these endpoints exist for the dashboard. They all live under /api/v1/auth and return the standard response envelope.

EndpointMethodAuth
/auth/loginPOSTPublic
/auth/password-reset/requestPOSTPublic
/auth/password-reset/confirmPOSTPublic
/auth/meGETSession (bearer)
/auth/logoutPOSTSession (bearer)
/auth/reauthPOSTSession (bearer)

/auth/login and the two /auth/password-reset/* endpoints are unauthenticated; /auth/logout, /auth/me, and /auth/reauth require an active session.

The admin session is unrelated to mode. Mode (TEST or LIVE) is a property of an app API credential, never of an admin session — the dashboard chooses which mode to view per screen.

Log in

POST /auth/loginpublic. Returns the session token (shown once) plus the operator's identity and effective permissions for the dashboard to render against.

Request body — LoginRequest

FieldTypeRequiredNotes
emailstringYesMust be a syntactically valid email.
passwordstringYesNon-blank; verified against the bcrypt hash.
curl -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "correct horse battery staple"
  }'
{
  "email": "[email protected]",
  "password": "correct horse battery staple"
}

The data payload is a LoginResultDto:

{
  "data": {
    "token": "oi_sess_3f9a1c44d2b84e7c9a6f0e2b1d7c5a83",
    "tokenType": "Bearer",
    "expiresAt": "2026-06-30T20:00:00",
    "userId": 42,
    "email": "[email protected]",
    "name": "Ops Operator",
    "superAdmin": false,
    "permissions": ["refund:approve", "app:read", "payment:read"]
  },
  "meta": {
    "success": true,
    "message": null,
    "errorCode": null,
    "timestamp": "2026-06-30T12:00:00Z"
  },
  "pagination": null
}

Response — LoginResultDto

FieldTypeNotes
tokenstringOpaque session token. Returned once — store it and send as Authorization: Bearer ….
tokenTypestringAlways Bearer.
expiresAtstring (date-time)Absolute session expiry. Sessions also expire after 30m idle; full lifetime is 8h by default.
userIdnumberThe operator's id.
emailstringThe operator's email.
namestringDisplay name.
superAdminbooleantrue bypasses per-permission checks.
permissionsstring[]Effective permission keys the UI gates on.

A wrong password or an unknown/disabled account both return INVALID_CREDENTIALS (no user enumeration). Too many failures in the window lock the account and return ACCOUNT_LOCKED (5 attempts / 15m by default).

Who am I

GET /auth/mesession. Re-reads the current session's identity and permissions so the dashboard can refresh a permission-aware UI without re-logging-in.

curl http://localhost:8080/api/v1/auth/me \
  -H "Authorization: Bearer SESSION_TOKEN"

The data payload is a MeDto:

{
  "data": {
    "userId": 42,
    "email": "[email protected]",
    "name": "Ops Operator",
    "superAdmin": false,
    "permissions": ["refund:approve", "app:read", "payment:read"]
  },
  "meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:00:00Z" },
  "pagination": null
}

Response — MeDto

FieldTypeNotes
userIdnumberThe operator's id.
emailstringThe operator's email.
namestringDisplay name.
superAdminbooleantrue bypasses per-permission checks.
permissionsstring[]Effective permission keys for the current session.

Log out

POST /auth/logoutsession. Revokes the session tied to the bearer token. The data is null on success.

curl -X POST http://localhost:8080/api/v1/auth/logout \
  -H "Authorization: Bearer SESSION_TOKEN"
{
  "data": null,
  "meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:00:00Z" },
  "pagination": null
}

Step-up re-authentication

Sensitive operations — rotating an app credential, overriding the gateway store, creating or disabling users, assigning roles — require a recent re-auth in addition to the right permission. If the session's last re-auth is older than the re-auth window (5m by default), the action is rejected with REAUTH_REQUIRED. The operator re-enters their password at POST /auth/reauth, which stamps the session and writes an entry to the append-only audit log; the original action then succeeds on retry.

Attempt the sensitive action

Call it normally with the bearer token. If your last re-auth is within the window, it just succeeds and no round-trip is needed.

Handle REAUTH_REQUIRED

A 403 with errorCode: "REAUTH_REQUIRED" means the step-up window lapsed. Prompt the operator for their password.

Re-authenticate

POST /auth/reauth with the password. On success the session's re-auth timestamp is refreshed and the call is audited (auth.reauth).

Retry

Re-issue the original request with the same bearer token. It now passes the step-up check.

Request body — ReauthRequest

FieldTypeRequiredNotes
passwordstringYesNon-blank; re-verified against the current operator's bcrypt hash.
curl -X POST http://localhost:8080/api/v1/auth/reauth \
  -H "Authorization: Bearer SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "password": "correct horse battery staple" }'
{ "password": "correct horse battery staple" }
{
  "data": null,
  "meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:00:00Z" },
  "pagination": null
}

Password reset

A two-step, public, self-service flow. Neither step leaks whether an account exists.

POST /auth/password-reset/requestpublic. Always returns the same uniform success response whether or not the email matches an account, so it cannot be used to enumerate users. When the email does match, a single-use reset token (valid 1h by default) is delivered out-of-band.

Request body — PasswordResetRequestRequest

FieldTypeRequiredNotes
emailstringYesMust be a syntactically valid email.
curl -X POST http://localhost:8080/api/v1/auth/password-reset/request \
  -H "Content-Type: application/json" \
  -d '{ "email": "[email protected]" }'
{
  "data": null,
  "meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:00:00Z" },
  "pagination": null
}

POST /auth/password-reset/confirmpublic. Completes the reset with the token and a new, policy-compliant password (minimum 12 characters by default). An invalid or expired token is rejected.

Request body — PasswordResetConfirmRequest

FieldTypeRequiredNotes
tokenstringYesThe single-use token from the request step.
newPasswordstringYesNon-blank; must satisfy the password policy (min length, not breached).
curl -X POST http://localhost:8080/api/v1/auth/password-reset/confirm \
  -H "Content-Type: application/json" \
  -d '{
    "token": "oi_pwr_8b21d0f6c4ae47119d3e2a5f7c0b9e64",
    "newPassword": "a much longer replacement secret"
  }'
{
  "data": null,
  "meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:00:00Z" },
  "pagination": null
}

Authentication failures

errorCodeMeaning
UNAUTHORIZEDMissing/invalid key or secret, or a revoked/expired credential.
INVALID_CREDENTIALSWrong password, or unknown/disabled admin account (login).
ACCOUNT_LOCKEDToo many failed admin logins — account temporarily locked.
FORBIDDENAuthenticated, but not permitted (admin RBAC).
SESSION_EXPIREDAdmin session no longer valid (expired, idle-timed-out, or revoked).
REAUTH_REQUIREDAdmin must re-authenticate before a sensitive action.

All failures use the standard response envelope.

On this page