OI Payments Docs
Admin dashboard

Applications

Register applications, manage their API credentials, and override the gateway for testing.

Applications are the tenants of the service. Registering one provisions its identity, settings, and the credentials it uses to call the API. Every record the service stores carries an app_id and a mode — one app never sees another app's data, and TEST and LIVE data are fully isolated. See App isolation and Modes.

These are admin endpoints under /admin/apps, authenticated by an admin session (Authorization: Bearer …) and gated on fine-grained authorities:

  • app:read — list and inspect apps and their credential metadata.
  • app:manage — register, change settings, rotate/revoke credentials, and set gateway overrides. The credential and gateway actions are sensitive: they require step-up re-auth and write an audit entry.

All responses use the standard envelope — { "data": …, "meta": …, "pagination": … }. The examples below show just the data payload; see Responses & errors.

Method & pathAuthorityPurpose
GET /admin/appsapp:readList apps (paginated, filterable).
GET /admin/apps/{id}app:readFetch one app's full configuration.
POST /admin/appsapp:manageRegister an app; returns plaintext credentials once.
PATCH /admin/apps/{id}/settingsapp:manageUpdate configuration (partial).
GET /admin/apps/{id}/credentialsapp:readList credential lifecycle metadata.
POST /admin/apps/{id}/credentials/rotateapp:manageRotate one mode's credential.
POST /admin/apps/{id}/credentials/{credentialId}/revokeapp:manageRevoke a credential immediately.
PUT /admin/apps/{id}/gateway-overrideapp:manageSet a per-mode SSLCOMMERZ store.
DELETE /admin/apps/{id}/gateway-override/{mode}app:manageClear a per-mode override.

Manage roles and authorities to decide who holds app:read versus app:manage.

List & inspect apps

GET /admin/apps returns a paginated list of ApplicationSummaryDto rows. GET /admin/apps/{id} returns the full ApplicationDto. Both require app:read.

Query paramTypeDefaultNotes
pageinteger0Zero-based page index.
sizeinteger20Page size.
sortBystringcreatedAtField to sort on.
orderASC | DESCDESCSort direction.
paginatebooleantrueSet false to return every row unpaged.
searchstringFree-text match on the app name.
statusACTIVE | DISABLEDFilter by lifecycle status.
curl "http://localhost:8080/api/v1/admin/apps?status=ACTIVE&size=20" \
  -H "Authorization: Bearer SESSION_TOKEN"
{
  "data": [
    {
      "id": 42,
      "name": "Acme Storefront",
      "status": "ACTIVE",
      "logoUrl": "https://cdn.example.com/acme/logo.png",
      "createdAt": "2026-06-30T09:12:00"
    }
  ],
  "pagination": { "page": 0, "size": 20, "totalElements": 1, "totalPages": 1 }
}

Fetch a single app's full configuration:

curl http://localhost:8080/api/v1/admin/apps/42 \
  -H "Authorization: Bearer SESSION_TOKEN"

ApplicationSummaryDto

FieldTypeRequiredNotes
idintegeralwaysApp id.
namestringalwaysDisplay name.
statusACTIVE | DISABLEDalwaysLifecycle status; apps are disabled, never deleted.
logoUrlstringoptionalOmitted when unset.
createdAtdatetimealwaysWhen the app was registered.

Register an app

POST /admin/apps (requires app:manage) creates the app and two credential pairsTEST and LIVE — plus a webhook signingSecret. The raw key, secret, and signing secret are returned once in the registration response; the server keeps only hashes, so relay them to the integrating team securely. If they are lost, rotate rather than re-register.

Only name is required; every other field can be supplied here or set later via settings.

RegisterApplicationRequest

FieldTypeRequiredNotes
namestringYesDisplay name. Non-blank, max 255 chars.
logoUrlstringNoMax 2048 chars.
webhookUrlstringNoWhere signed webhooks are delivered. Max 2048.
refundApprovalThresholdMinorinteger (int64)NoMinor units (paisa). Operator refunds above this are parked for approval. Must be ≥ 0.
receiptsEnabledbooleanNoWhether settled payments expose a receipt. Defaults to true when omitted.
minAmountMinorinteger (int64)NoMinor units. Lower per-payment bound. Must be ≥ 0.
maxAmountMinorinteger (int64)NoMinor units. Upper per-payment bound. Must be ≥ 0 and ≥ minAmountMinor.
returnUrlAllowliststringNoNewline-separated allow-list of permitted browser return-URL prefixes (scheme included). Max 4096.
defaultSuccessUrlstringNoDefault browser success redirect. Must match the allow-list. Max 2048.
defaultFailUrlstringNoDefault failure redirect. Must match the allow-list. Max 2048.
defaultCancelUrlstringNoDefault cancel redirect. Must match the allow-list. Max 2048.

Money fields are integer minor units (paisa)150000 means 1,500.00 BDT. Never floating point. See Money.

curl -X POST http://localhost:8080/api/v1/admin/apps \
  -H "Authorization: Bearer SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Storefront",
    "logoUrl": "https://cdn.example.com/acme/logo.png",
    "webhookUrl": "https://acme.example.com/oi/webhooks",
    "refundApprovalThresholdMinor": 500000,
    "receiptsEnabled": true,
    "minAmountMinor": 1000,
    "maxAmountMinor": 50000000,
    "returnUrlAllowlist": "https://acme.example.com\nhttps://checkout.acme.example.com",
    "defaultSuccessUrl": "https://acme.example.com/pay/success",
    "defaultFailUrl": "https://acme.example.com/pay/failed",
    "defaultCancelUrl": "https://acme.example.com/pay/cancelled"
  }'

The 201 Created response wraps a RegisterApplicationResultDto — the new app's read model plus its plaintext credentials:

{
  "data": {
    "application": {
      "id": 42,
      "name": "Acme Storefront",
      "status": "ACTIVE",
      "logoUrl": "https://cdn.example.com/acme/logo.png",
      "webhookUrl": "https://acme.example.com/oi/webhooks",
      "refundApprovalThresholdMinor": 500000,
      "receiptsEnabled": true,
      "dunningEnabled": false,
      "minAmountMinor": 1000,
      "maxAmountMinor": 50000000,
      "returnUrlAllowlist": "https://acme.example.com\nhttps://checkout.acme.example.com",
      "defaultSuccessUrl": "https://acme.example.com/pay/success",
      "defaultFailUrl": "https://acme.example.com/pay/failed",
      "defaultCancelUrl": "https://acme.example.com/pay/cancelled",
      "createdAt": "2026-06-30T09:12:00",
      "updatedAt": "2026-06-30T09:12:00"
    },
    "credentials": {
      "testApiKey": "oi_test_8f3a2b1c9d4e",
      "testApiSecret": "sk_test_a1b2c3d4e5f6a7b8c9d0",
      "liveApiKey": "oi_live_2c7e9a4f1b3d",
      "liveApiSecret": "sk_live_f6e5d4c3b2a1f0e9d8c7",
      "signingSecret": "whsec_3b9f7c1e4a8d2f60"
    }
  }
}

credentials (and the signingSecret) appear in this response only. They are never returned by any read endpoint and never recoverable afterwards. The mode of each key — TEST or LIVE — is fixed at issue time; an app's own API calls derive their mode from the key that authenticated them, never from a request body. See Authentication.

ApplicationDto

The read model returned by GET /admin/apps/{id}, PATCH …/settings, and inside the registration result. It deliberately carries no credential material.

FieldTypeRequiredNotes
idintegeralwaysApp id.
namestringalwaysDisplay name.
statusACTIVE | DISABLEDalwaysLifecycle status.
logoUrlstringoptionalOmitted when unset.
webhookUrlstringoptionalWebhook delivery target.
refundApprovalThresholdMinorintegeroptionalMinor units.
receiptsEnabledbooleanalwaysReceipts toggle.
dunningEnabledbooleanalwaysOverdue-invoice reminders toggle (see below).
minAmountMinorintegeroptionalMinor units.
maxAmountMinorintegeroptionalMinor units.
returnUrlAllowliststringoptionalNewline-separated prefixes.
defaultSuccessUrlstringoptionalDefault success redirect.
defaultFailUrlstringoptionalDefault failure redirect.
defaultCancelUrlstringoptionalDefault cancel redirect.
createdAtdatetimealwaysRegistration time.
updatedAtdatetimealwaysLast settings change.

Settings

PATCH /admin/apps/{id}/settings (requires app:manage) applies a partial update — any field left null is unchanged. It returns the updated ApplicationDto. The use case enforces minAmountMinor ≤ maxAmountMinor and re-validates the return-URL defaults against the allow-list.

UpdateApplicationSettingsRequest

FieldTypeRequiredNotes
logoUrlstringNoMax 2048.
webhookUrlstringNoMax 2048.
refundApprovalThresholdMinorinteger (int64)NoMinor units. Must be ≥ 0.
receiptsEnabledbooleanNonull leaves it unchanged.
dunningEnabledbooleanNoOpt-in switch for automated overdue-invoice reminders (dunning). null leaves it unchanged. Not settable at registration — it defaults to false.
minAmountMinorinteger (int64)NoMinor units. Must be ≥ 0.
maxAmountMinorinteger (int64)NoMinor units. Must be ≥ 0 and ≥ minAmountMinor.
returnUrlAllowliststringNoNewline-separated prefixes. Max 4096.
defaultSuccessUrlstringNoMust match the allow-list. Max 2048.
defaultFailUrlstringNoMust match the allow-list. Max 2048.
defaultCancelUrlstringNoMust match the allow-list. Max 2048.
curl -X PATCH http://localhost:8080/api/v1/admin/apps/42/settings \
  -H "Authorization: Bearer SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "dunningEnabled": true }'

dunningEnabled turns on automated reminders for overdue invoices; it is off by default and only togglable here. See Invoicing and Admin invoices.

Return-URL configuration

returnUrlAllowlist is a newline-separated list of permitted browser return-URL prefixes (scheme included), for example:

https://acme.example.com
https://checkout.acme.example.com

The three defaults — defaultSuccessUrl, defaultFailUrl, and defaultCancelUrl — are used verbatim when a create-payment call omits its own return URLs. Because a default is consumed without a per-request check, every non-blank default must start with an allow-listed prefix. On both register and settings save the service re-validates the post-merge state and rejects an off-allow-list default with 400 (deny-by-default), mirroring the per-payment check in Accept a payment.

Manage credentials

TEST and LIVE credentials are fully independent — rotating or revoking one mode never touches the other.

List an app's credentials with GET /admin/apps/{id}/credentials (requires app:read). It returns metadata only; the key hash and secret hash are never exposed, and plaintext is shown only once at issue time.

ApiCredentialDto

FieldTypeRequiredNotes
idintegeralwaysCredential id (use it to revoke).
modeTEST | LIVEalwaysWhich mode this key authenticates.
statusACTIVE | REVOKEDalwaysA revoked key never authenticates again.
expiresAtdatetimeoptionalSet only on a rotated-out key — the end of its grace window. null for an open-ended key.
createdAtdatetimealwaysWhen the credential was issued.
curl http://localhost:8080/api/v1/admin/apps/42/credentials \
  -H "Authorization: Bearer SESSION_TOKEN"
{
  "data": [
    { "id": 7001, "mode": "TEST", "status": "ACTIVE", "expiresAt": null, "createdAt": "2026-06-30T09:12:00" },
    { "id": 7002, "mode": "LIVE", "status": "ACTIVE", "expiresAt": null, "createdAt": "2026-06-30T09:12:00" }
  ]
}

Rotate

POST /admin/apps/{id}/credentials/rotate (requires app:manage) issues a fresh credential for one mode and bounds the outgoing key(s) for that mode to a grace window, so callers roll over with no downtime. The new plaintext key and secret are returned once.

The request body selects the mode:

FieldTypeRequiredNotes
modeTEST | LIVEYesWhich mode's credential to rotate.
curl -X POST http://localhost:8080/api/v1/admin/apps/42/credentials/rotate \
  -H "Authorization: Bearer SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "mode": "TEST" }'

The 200 OK response is a RotatedCredentialDto:

FieldTypeRequiredNotes
credentialIdintegeralwaysId of the newly issued credential.
modeTEST | LIVEalwaysMode that was rotated.
apiKeystringalwaysNew plaintext key — shown once.
apiSecretstringalwaysNew plaintext secret — shown once.
previousKeyExpiresAtdatetimeoptionalWhen the rotated-out key for this mode stops authenticating (end of the grace window); null if there was no previous active key.
{
  "data": {
    "credentialId": 7003,
    "mode": "TEST",
    "apiKey": "oi_test_5d1a9c3e7b2f",
    "apiSecret": "sk_test_0f1e2d3c4b5a69788776",
    "previousKeyExpiresAt": "2026-07-07T09:30:00"
  }
}

Revoke

POST /admin/apps/{id}/credentials/{credentialId}/revoke (requires app:manage) flips a single credential to REVOKED immediately — no grace window — leaving the app's other credentials untouched. The credential is looked up scoped to its owning app, so one app can never revoke another's key. The response carries no data.

curl -X POST http://localhost:8080/api/v1/admin/apps/42/credentials/7001/revoke \
  -H "Authorization: Bearer SESSION_TOKEN"

Credential lifecycle

Rotation gives integrators a window to swap keys without dropping requests:

See Authentication for the integrator's side of rotation.

Rotation and revocation are sensitive actions: they call SensitiveActionGuard.requireRecentReauth() (step-up re-auth) and write an audit entry (app.credentials.rotate / app.credentials.revoke). See Webhooks & logs.

Gateway override

An app can run on its own SSLCOMMERZ store instead of the platform default. The override is per (app, mode), so TEST and LIVE are configured independently; there is at most one override per mode, and re-setting one updates it in place.

  • PUT /admin/apps/{id}/gateway-override — set or replace the override for a mode.
  • DELETE /admin/apps/{id}/gateway-override/{mode} — clear it; payments for that mode then fall back to the platform default store. Clearing a missing override is a no-op.

Both require app:manage.

SetGatewayOverrideRequest

FieldTypeRequiredNotes
modeTEST | LIVEYesWhich mode's store to configure.
storeIdstringYesSSLCOMMERZ store id. Non-blank.
storePasswordstringYesSSLCOMMERZ store password. Non-blank. Encrypted at rest and never returned on any read.
curl -X PUT http://localhost:8080/api/v1/admin/apps/42/gateway-override \
  -H "Authorization: Bearer SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "TEST",
    "storeId": "acme_test_store",
    "storePassword": "acme_test_store_pass"
  }'

The 200 OK response is a GatewayOverrideDto — it confirms the configured store but omits the password:

FieldTypeRequiredNotes
idintegeralwaysOverride row id.
modeTEST | LIVEalwaysMode this override applies to.
storeIdstringalwaysConfigured store id.
createdAtdatetimealwaysWhen the override was first set.
updatedAtdatetimealwaysLast change.
{
  "data": {
    "id": 9001,
    "mode": "TEST",
    "storeId": "acme_test_store",
    "createdAt": "2026-06-30T10:00:00",
    "updatedAt": "2026-06-30T10:00:00"
  }
}

Clear an override for a mode:

curl -X DELETE http://localhost:8080/api/v1/admin/apps/42/gateway-override/TEST \
  -H "Authorization: Bearer SESSION_TOKEN"

Setting and clearing a gateway override are sensitive actions: they require step-up re-auth and are audited (app.gateway-override.set / app.gateway-override.clear, recording only the mode and store id — never the password).

On this page