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 & path | Authority | Purpose |
|---|---|---|
GET /admin/apps | app:read | List apps (paginated, filterable). |
GET /admin/apps/{id} | app:read | Fetch one app's full configuration. |
POST /admin/apps | app:manage | Register an app; returns plaintext credentials once. |
PATCH /admin/apps/{id}/settings | app:manage | Update configuration (partial). |
GET /admin/apps/{id}/credentials | app:read | List credential lifecycle metadata. |
POST /admin/apps/{id}/credentials/rotate | app:manage | Rotate one mode's credential. |
POST /admin/apps/{id}/credentials/{credentialId}/revoke | app:manage | Revoke a credential immediately. |
PUT /admin/apps/{id}/gateway-override | app:manage | Set a per-mode SSLCOMMERZ store. |
DELETE /admin/apps/{id}/gateway-override/{mode} | app:manage | Clear 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 param | Type | Default | Notes |
|---|---|---|---|
page | integer | 0 | Zero-based page index. |
size | integer | 20 | Page size. |
sortBy | string | createdAt | Field to sort on. |
order | ASC | DESC | DESC | Sort direction. |
paginate | boolean | true | Set false to return every row unpaged. |
search | string | — | Free-text match on the app name. |
status | ACTIVE | DISABLED | — | Filter 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
| Field | Type | Required | Notes |
|---|---|---|---|
id | integer | always | App id. |
name | string | always | Display name. |
status | ACTIVE | DISABLED | always | Lifecycle status; apps are disabled, never deleted. |
logoUrl | string | optional | Omitted when unset. |
createdAt | datetime | always | When the app was registered. |
Register an app
POST /admin/apps (requires app:manage) creates the app and two credential
pairs — TEST 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
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | Yes | Display name. Non-blank, max 255 chars. |
logoUrl | string | No | Max 2048 chars. |
webhookUrl | string | No | Where signed webhooks are delivered. Max 2048. |
refundApprovalThresholdMinor | integer (int64) | No | Minor units (paisa). Operator refunds above this are parked for approval. Must be ≥ 0. |
receiptsEnabled | boolean | No | Whether settled payments expose a receipt. Defaults to true when omitted. |
minAmountMinor | integer (int64) | No | Minor units. Lower per-payment bound. Must be ≥ 0. |
maxAmountMinor | integer (int64) | No | Minor units. Upper per-payment bound. Must be ≥ 0 and ≥ minAmountMinor. |
returnUrlAllowlist | string | No | Newline-separated allow-list of permitted browser return-URL prefixes (scheme included). Max 4096. |
defaultSuccessUrl | string | No | Default browser success redirect. Must match the allow-list. Max 2048. |
defaultFailUrl | string | No | Default failure redirect. Must match the allow-list. Max 2048. |
defaultCancelUrl | string | No | Default 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.
| Field | Type | Required | Notes |
|---|---|---|---|
id | integer | always | App id. |
name | string | always | Display name. |
status | ACTIVE | DISABLED | always | Lifecycle status. |
logoUrl | string | optional | Omitted when unset. |
webhookUrl | string | optional | Webhook delivery target. |
refundApprovalThresholdMinor | integer | optional | Minor units. |
receiptsEnabled | boolean | always | Receipts toggle. |
dunningEnabled | boolean | always | Overdue-invoice reminders toggle (see below). |
minAmountMinor | integer | optional | Minor units. |
maxAmountMinor | integer | optional | Minor units. |
returnUrlAllowlist | string | optional | Newline-separated prefixes. |
defaultSuccessUrl | string | optional | Default success redirect. |
defaultFailUrl | string | optional | Default failure redirect. |
defaultCancelUrl | string | optional | Default cancel redirect. |
createdAt | datetime | always | Registration time. |
updatedAt | datetime | always | Last 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
| Field | Type | Required | Notes |
|---|---|---|---|
logoUrl | string | No | Max 2048. |
webhookUrl | string | No | Max 2048. |
refundApprovalThresholdMinor | integer (int64) | No | Minor units. Must be ≥ 0. |
receiptsEnabled | boolean | No | null leaves it unchanged. |
dunningEnabled | boolean | No | Opt-in switch for automated overdue-invoice reminders (dunning). null leaves it unchanged. Not settable at registration — it defaults to false. |
minAmountMinor | integer (int64) | No | Minor units. Must be ≥ 0. |
maxAmountMinor | integer (int64) | No | Minor units. Must be ≥ 0 and ≥ minAmountMinor. |
returnUrlAllowlist | string | No | Newline-separated prefixes. Max 4096. |
defaultSuccessUrl | string | No | Must match the allow-list. Max 2048. |
defaultFailUrl | string | No | Must match the allow-list. Max 2048. |
defaultCancelUrl | string | No | Must 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.comThe 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
| Field | Type | Required | Notes |
|---|---|---|---|
id | integer | always | Credential id (use it to revoke). |
mode | TEST | LIVE | always | Which mode this key authenticates. |
status | ACTIVE | REVOKED | always | A revoked key never authenticates again. |
expiresAt | datetime | optional | Set only on a rotated-out key — the end of its grace window. null for an open-ended key. |
createdAt | datetime | always | When 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:
| Field | Type | Required | Notes |
|---|---|---|---|
mode | TEST | LIVE | Yes | Which 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:
| Field | Type | Required | Notes |
|---|---|---|---|
credentialId | integer | always | Id of the newly issued credential. |
mode | TEST | LIVE | always | Mode that was rotated. |
apiKey | string | always | New plaintext key — shown once. |
apiSecret | string | always | New plaintext secret — shown once. |
previousKeyExpiresAt | datetime | optional | When 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
| Field | Type | Required | Notes |
|---|---|---|---|
mode | TEST | LIVE | Yes | Which mode's store to configure. |
storeId | string | Yes | SSLCOMMERZ store id. Non-blank. |
storePassword | string | Yes | SSLCOMMERZ 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:
| Field | Type | Required | Notes |
|---|---|---|---|
id | integer | always | Override row id. |
mode | TEST | LIVE | always | Mode this override applies to. |
storeId | string | always | Configured store id. |
createdAt | datetime | always | When the override was first set. |
updatedAt | datetime | always | Last 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).
Roles & permissions
The seeded roles and their exact permission sets, the full permission catalogue, app-scoped grants, and the role / user / session admin endpoints.
Transactions
Search payments across every application you are scoped to, export the filtered set as CSV, and read per-app dashboard metrics and daily trends.