6. Merchant account management in the Portal
These endpoints power DLT Merchant Portal screens for the signed-in user's registered business. Every protected request uses the Portal user PAT. Integration credentials returned or edited here are assets managed for that business; the Portal does not use those assets to authenticate its own DLT API calls.
6.1 Merchant and logo endpoints
| Method and path | Role | Request | Result |
|---|---|---|---|
GET /portal/merchant |
Both | None | 200, merchant profile |
PATCH /portal/merchant |
Administrator | Editable fields below | 200, updated merchant profile |
GET /portal/merchant/logo |
Both | None | 200, { "url": null } or logo URL |
POST /portal/merchant/logo |
Administrator | Multipart file |
200, { "url": "https://..." } |
DELETE /portal/merchant/logo |
Administrator | None | 204 |
Editable fields: name (1–150 characters), contact_person (1–100), email (valid email, maximum 255), dial_code (nonempty string, maximum 8), phone (nonempty string, maximum 30), address (nonempty string, maximum 500), and nullable website (HTTPS URL, maximum 2,048). PATCH requires at least one allowed field; omitted fields remain unchanged. Supplied fields other than website cannot be null/empty. Strings are trimmed by Laravel middleware. Merchant email is a business contact field; it does not change any user's login email or trigger the self-profile verification flow.
Read-only fields include id, reference_id, currency, is_active, is_production, services, and timestamps. PATCH rejects unexpected fields, including fees, settlement/provider settings, and credential data, with 422 VALIDATION_ERROR. Account activation, enabling live processing, settlement settings, and provider assignments remain DLT-controlled. Account creation/deletion is outside this portal version.
Merchant scope comes from the authenticated user's server-resolved context. Routes accept no merchant ID in the path or request. Mismatched compatibility selectors return 403 MERCHANT_SCOPE_MISMATCH; other unexpected mutation fields return 422. Developer mutation requests return 403 FORBIDDEN before file processing or field validation. Missing/revoked access returns 401; inactive user/merchant or ineligible application/scope returns the existing context 403 errors.
Profile and logo GET/POST/PATCH responses use the standard success/message/data envelope with Cache-Control: no-store. Logo DELETE returns 204 with an empty body and is idempotent when no logo exists. DELETE accepts no fields. Profile mutations return message Merchant profile updated.; logo GET/POST return Success. These endpoints also use the existing API-wide request limit.
Example PATCH body:
{
"name": "Example Merchant",
"contact_person": "Alex Reyes",
"website": null
}
Logo POST uses multipart field file, validated as a real PNG, JPEG, or WebP image up to 2,048 KB. Other file types, invalid image contents, extra fields, and oversized uploads return 422 VALIDATION_ERROR. Server-generated filenames are stored through the configured default filesystem beneath public/uploads/img/merchants/{authenticated_merchant_id}/; client filenames and supplied paths cannot select the storage destination.
Use the returned logo URL; missing or unusable stored files return url:null. DLT must configure public file serving before integration.
merchant data object:
{
"id": 7,
"reference_id": "MER_EXAMPLE",
"name": "Example Merchant",
"contact_person": "Alex Reyes",
"email": "operations@example.com",
"dial_code": "+63",
"phone": "9000000000",
"address": "Example business address",
"website": "https://merchant.example.com",
"logo_url": null,
"currency": "PHP",
"is_active": true,
"is_production": true,
"services": [{ "id": 11, "code": "COLLECTION", "type": "collection", "is_active": true, "is_production": false }],
"created_at": "2026-09-01T00:00:00Z",
"updated_at": "2026-10-01T09:00:00Z"
}
services projects actual provider-service assignments using their stored id, code, and type. is_active requires both the service and assignment to be active; is_production is the assignment's stored environment flag. It is not a synthesized test/live readiness matrix. Empty assignments return []. OAuth secrets, provider credentials, integration keys, callback settings, settlement balances, and fee configuration are excluded.
6.2 Portal management of merchant integration credentials
Implement the Portal's integration credential screen with these APIs. The backend calls them using the signed-in user's PAT. An authorized Merchant Administrator can manage credentials for their business's own integration; Developer users can view metadata. The separate Merchant integrations guide explains how the business's integration uses issued credentials.
| Method and path | Role | Body | Result |
|---|---|---|---|
GET /portal/merchant/machine-clients |
Both | Pagination query | 200, metadata list |
POST /portal/merchant/machine-clients |
Administrator | label, reason, current_password |
201, client metadata and one-use delivery reference |
POST /portal/merchant/machine-clients/{client_id}/rotate |
Administrator | label, reason, current_password, overlap_minutes |
201, replacement metadata and delivery reference |
POST /portal/merchant/machine-clients/{client_id}/revoke |
Administrator | reason, current_password |
200, revoked metadata |
POST /portal/merchant/credential-deliveries/{delivery_id}/consume |
Issuing Administrator | current_password |
200, one-use credential data |
client_id is the merchant ownership record ID, distinct from oauth_client_id; the route accepts a positive decimal ID up to 20 digits. delivery_id is a UUID. All requests use a merchant user PAT and the server-resolved merchant; no merchant selector is accepted. Both roles may list metadata; only Administrators may perform changes. Consumption additionally requires the same user who issued the delivery. Framework/personal-access/ecosystem client mappings cannot be rotated or revoked through these APIs.
Implemented validation: unique merchant label, 3–100 characters, beginning with an alphanumeric character and allowing letters, numbers, spaces, periods, underscores, and hyphens; reason 10–500 characters; current password required as a string up to 128 characters. Rotation requires integer overlap_minutes from 5 through 10,080. Unexpected fields, including environment or supplied OAuth secrets, are rejected with 422 VALIDATION_ERROR. Labels remain reserved after revocation; use a new operational label when issuing a replacement. These credentials are merchant-bound; no per-credential test/live restriction is asserted.
Wrong current password returns 422 with a current_password field error and leaves credentials unchanged. Credential mutations and audit writes are atomic.
Mutation limits are 5/minute per actor, merchant, and operation; consumption is 10/minute per actor and delivery, in addition to the API-wide throttle. Issuance, rotation, and revocation have separate operation buckets. Throttling returns 429 RATE_LIMITED with Retry-After; mutation-limit failures are audited. Developer mutation denial is 403 FORBIDDEN; wrong merchant selectors use the existing context 403 codes. Cross-merchant/missing ownership IDs or non-machine mappings return 404 NOT_FOUND.
The list uses standard pagination (default 25, maximum 100), ordered by descending ownership ID. Empty lists return 200 with data=[] and pagination metadata. Items expose id, oauth_client_id, label, is_active, created_at, last_used_at, revoked_at, rotation_deadline_at, and replaces_merchant_oauth_client_id. is_active accounts for mapping state, Passport client revocation, machine-client eligibility, and rotation deadline. Timestamps can be null. A delivery reference (id, expires_at) is shown only to its issuing Administrator while the client and delivery remain usable; it is null for Developers, other actors, expired/consumed deliveries, or revoked clients. Reading the list performs system-audited expired-delivery cleanup for the merchant; it does not revoke the underlying client.
Creation/rotation returns 201, data.client metadata, and data.delivery containing only id and expires_at. No secret appears in issuance or metadata responses. Revocation returns 200 with the revoked metadata directly under data and message Machine client and its tokens were revoked. All credential responses use Cache-Control: no-store.
Rotation: the API creates a replacement integration client and sets the prior client's overlap deadline. Show rotation_deadline_at and the replacement relationship (replaces_merchant_oauth_client_id) so the Administrator can coordinate their business's integration update before cutoff. Explicit revocation can end the overlap early. Repeating rotation on a prior record already assigned a deadline, or rotating an unavailable client, returns 409 CONFLICT without another replacement. The integration-side token behavior is covered in the separate Merchant integrations guide.
Revocation: revoke the Passport client, its access/refresh/auth-code records, deactivate the ownership, clear its rotation deadline, and delete any pending secret delivery. Repeated revocation returns 409 CONFLICT. Other machine clients and Portal user tokens are unaffected.
Secret delivery: the encrypted secret is available for five minutes to the issuing Administrator only. Consumption returns exactly data.oauth_client_id, data.oauth_client_secret, data.grant_type, and data.token_url, then deletes the delivery in the audited transaction. Responses include Cache-Control: no-store, private, max-age=0, Pragma: no-cache, and X-Content-Type-Options: nosniff. An unavailable, consumed, expired, or client-invalidated delivery returns 410 GONE; an existing delivery belonging to another merchant/actor returns 404 NOT_FOUND. Invalid UUID route syntax returns 404. Expired delivery removal commits before the 410 response; failed consumption audits preserve the delivery for retry.
Provide the one-use credential handover only to the issuing Administrator, so they can configure their business's integration. Keep secret responses out of browser persistence, shared caches and telemetry; do not replace the Portal ecosystem credentials or user PAT with the returned values. If consumption succeeds but the response is lost, the secret cannot be retrieved again; issue/rotate new credentials as appropriate. Never automatically retry issuance/rotation or consumption after an ambiguous response. Read metadata first; issuer-only delivery references support recovery when the issuance response is lost.
issuance body:
{
"label": "Merchant backend primary",
"reason": "Initial merchant integration setup",
"current_password": "<administrator_password>"
}
Example issuance response:
{
"success": true,
"message": "Machine client issued. Consume the secret within five minutes.",
"data": {
"client": {
"id": 12,
"oauth_client_id": 31,
"label": "Merchant backend primary",
"is_active": true,
"created_at": "2026-10-01T09:00:00Z",
"last_used_at": null,
"revoked_at": null,
"rotation_deadline_at": null,
"replaces_merchant_oauth_client_id": null
},
"delivery": {
"id": "00000000-0000-4000-8000-000000000001",
"expires_at": "2026-10-01T09:05:00Z"
}
}
}
Consume through POST /api/v1/portal/merchant/credential-deliveries/{delivery_id}/consume with only current_password. Example response (placeholder secret):
{
"success": true,
"message": "Success",
"data": {
"oauth_client_id": 31,
"oauth_client_secret": "<one-use-secret>",
"grant_type": "client_credentials",
"token_url": "https://checkout.example.com/oauth/token"
}
}
6.3 Portal management of merchant integration keys
These APIs power the Portal's settings screen for a business's integration keys. The Portal calls them with the user PAT; the key values are managed account assets. GET returns presence metadata; saved values cannot be retrieved.
| Method and path | Role | Contract |
|---|---|---|
GET /portal/merchant/integration-keys |
Both | 200, data with has_authentication_token, has_secret_key, updated_at |
PUT /portal/merchant/integration-keys/{key_type} |
Administrator | value, current_password, reason; 200, metadata only |
DELETE /portal/merchant/integration-keys/{key_type} |
Administrator | current_password, reason; 204 |
key_type must be exactly authentication_token or secret_key; another type returns 404 NOT_FOUND. value is a non-empty, non-whitespace-only string up to 4,096 characters. The Portal preserves its exact bytes, including surrounding spaces; confirm the value matches the merchant integration. reason is required, 10–500 characters; current_password is required, at most 128 characters. Unaccepted fields return 422 VALIDATION_ERROR. A conflicting merchant selector returns 403 MERCHANT_SCOPE_MISMATCH; there is no merchant-ID parameter on these routes.
Both roles may GET; only Administrator may PUT/DELETE. Wrong current password returns 422 on current_password without changing keys. Mutations are limited to 5/minute per actor, merchant, operation and key type, plus the API limit.
Example replacement body:
{
"value": "<merchant_integration_key>",
"current_password": "<administrator_password>",
"reason": "Replace merchant integration signing key"
}
GET and successful PUT return presence metadata only:
{
"success": true,
"message": "Success",
"data": {
"has_authentication_token": true,
"has_secret_key": false,
"updated_at": "2026-10-02T10:00:00+08:00"
}
}
PUT uses message Integration key updated.. GET with no integration returns both flags false and updated_at: null, without creating a record. DELETE uses the replacement body with value omitted, returns 204 with no body, and is idempotent even when the key/integration is absent. It preserves the other key, logo, webhook URLs, and x_token. Responses use Cache-Control: no-store, private. No endpoint reads back plaintext or ciphertext.
Impact shown to the Administrator: changing integration keys can affect the business's payment notifications. Explain that the integration owner must coordinate a signing-key change and that removing a key without a fallback can interrupt future notifications. Previously queued notifications may still use an earlier key. The Portal does not verify or replay those notifications; its responsibility is the authenticated settings operation and clear feedback. OAuth clients and Portal user PATs remain active after these key changes.
6.4 Request validation reference
These tables reflect the current implementation’s request rules. See input conventions for required/optional/null behavior and error formats.
Validation: GET /api/v1/portal/merchant
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
| (none) | Body/query | No documented inputs | No payload is required; see behavior below. |
Send no request body. No body/query fields are validated or used to choose a merchant; scope comes from the user PAT.
Validation: PATCH /api/v1/portal/merchant
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
name |
JSON body | Optional; required if supplied | Nonempty string, maximum 150 characters. |
contact_person |
JSON body | Optional; required if supplied | Nonempty string, maximum 100 characters. |
email |
JSON body | Optional; required if supplied | RFC-valid email string, maximum 255 characters; business contact only, no user-email uniqueness/verification flow. |
dial_code |
JSON body | Optional; required if supplied | Nonempty string, maximum 8 characters; no additional dial-code pattern is imposed. |
phone |
JSON body | Optional; required if supplied | Nonempty string, maximum 30 characters; no digits-only pattern is imposed. |
address |
JSON body | Optional; required if supplied | Nonempty string, maximum 500 characters. |
website |
JSON body | Optional; nullable | Null or HTTPS URL string, maximum 2,048 characters. Null clears the website. |
Supply at least one allowed field. Omission preserves existing values; null/empty is invalid except website (empty string becomes null). Strings are trimmed. Unlisted payload/query fields are rejected with 422 VALIDATION_ERROR. A conflicting merchant selector may instead be rejected earlier with 403 MERCHANT_SCOPE_MISMATCH. Administrator only; read-only account, service, environment and credential fields cannot be changed.
Validation: GET /api/v1/portal/merchant/logo
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
| (none) | Body/query | No documented inputs | No payload is required; see behavior below. |
Send no request body. No payload/query fields are validated or used; returns the scoped merchant’s logo.
Validation: POST /api/v1/portal/merchant/logo
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
file |
Multipart form | Required | Uploaded file containing a real PNG, JPEG (jpg/jpeg) or WebP image; maximum 2,048 KB. A filename/URL string is not an uploaded image. |
Unlisted payload/query fields are rejected with 422 VALIDATION_ERROR. A conflicting merchant selector may instead be rejected earlier with 403 MERCHANT_SCOPE_MISMATCH. Administrator only. Let the client generate the multipart boundary; client filename/path cannot choose storage destination.
Validation: DELETE /api/v1/portal/merchant/logo
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
| (none) | Body/query | No documented inputs | No payload is required; see behavior below. |
Send no request body. Any supplied body/query fields are rejected with 422. Administrator only; success returns 204 with no response payload.
Validation: GET /api/v1/portal/merchant/machine-clients
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
page |
Query | Optional; default 1 | Integer, minimum 1. Null/empty values are invalid. |
per_page |
Query | Optional; default 25 | Integer, 1–100 inclusive. Null/empty values are invalid. |
Send no request body. Pagination is validated; additional fields are not rejected by the list validator and do not add filters. Merchant context still applies.
Validation: POST /api/v1/portal/merchant/machine-clients
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
label |
JSON body | Required | String, 3–100 characters; first character is ASCII alphanumeric, subsequent characters are ASCII letters/digits, spaces, ., _ or -. Unique within this merchant, including revoked labels. |
reason |
JSON body | Required | Nonempty string, 10–500 characters inclusive. |
current_password |
JSON body | Required | Nonempty string, maximum 128 characters; must match the authenticated Administrator’s current password. Incorrect password returns a field error without mutation. |
Unlisted payload/query fields are rejected with 422 VALIDATION_ERROR. A conflicting merchant selector may instead be rejected earlier with 403 MERCHANT_SCOPE_MISMATCH. Administrator only. No environment, merchant selector or caller-provided OAuth secret is accepted.
Validation: POST /api/v1/portal/merchant/machine-clients/{client}/rotate
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
client |
Path | Required | Positive canonical decimal ID, 1–20 digits; pattern [1-9][0-9]{0,19}. No leading zero, sign or decimal point. Malformed, missing or inaccessible resources return 404 NOT_FOUND. Use the merchant ownership ID, not oauth_client_id. |
label |
JSON body | Required | String, 3–100 characters; first character is ASCII alphanumeric, subsequent characters are ASCII letters/digits, spaces, ., _ or -. Unique within this merchant, including revoked labels. |
reason |
JSON body | Required | Nonempty string, 10–500 characters inclusive. |
current_password |
JSON body | Required | Nonempty string, maximum 128 characters; must match the authenticated Administrator’s current password. Incorrect password returns a field error without mutation. |
overlap_minutes |
JSON body | Required | Integer, 5–10,080 inclusive; no independent environment field. |
Unlisted payload/query fields are rejected with 422 VALIDATION_ERROR. A conflicting merchant selector may instead be rejected earlier with 403 MERCHANT_SCOPE_MISMATCH. Administrator only. Prior client must be usable and not already assigned a rotation deadline; otherwise 409. Use a fresh unique label.
Validation: POST /api/v1/portal/merchant/machine-clients/{client}/revoke
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
client |
Path | Required | Positive canonical decimal ID, 1–20 digits; pattern [1-9][0-9]{0,19}. No leading zero, sign or decimal point. Malformed, missing or inaccessible resources return 404 NOT_FOUND. Use the merchant ownership ID. |
reason |
JSON body | Required | Nonempty string, 10–500 characters inclusive. |
current_password |
JSON body | Required | Nonempty string, maximum 128 characters; must match the authenticated Administrator’s current password. Incorrect password returns a field error without mutation. |
Unlisted payload/query fields are rejected with 422 VALIDATION_ERROR. A conflicting merchant selector may instead be rejected earlier with 403 MERCHANT_SCOPE_MISMATCH. Administrator only. Already-revoked client returns 409; this operation requires JSON despite being a revocation.
Validation: POST /api/v1/portal/merchant/credential-deliveries/{delivery}/consume
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
delivery |
Path | Required | UUID route identifier from the returned resource. Invalid syntax, missing or foreign resources return 404 NOT_FOUND. |
current_password |
JSON body | Required | Nonempty string, maximum 128 characters; must match the authenticated Administrator’s current password. Incorrect password returns a field error without mutation. |
Unlisted payload/query fields are rejected with 422 VALIDATION_ERROR. A conflicting merchant selector may instead be rejected earlier with 403 MERCHANT_SCOPE_MISMATCH. Issuing Administrator only; an existing delivery belonging to another actor/merchant returns 404. Expired (five minutes), consumed, missing or client-invalidated delivery returns 410 GONE.
Validation: GET /api/v1/portal/merchant/integration-keys
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
| (none) | Body/query | No documented inputs | No payload is required; see behavior below. |
Send no request body. No payload/query fields are validated or used; only presence metadata is returned.
Validation: PUT /api/v1/portal/merchant/integration-keys/{keyType}
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
keyType |
Path | Required | Exactly authentication_token or secret_key, case-sensitive; unsupported values return 404. |
value |
JSON body | Required | Nonempty, non-whitespace-only string, maximum 4,096 characters. Surrounding spaces are preserved; do not trim the integration key. |
current_password |
JSON body | Required | Nonempty string, maximum 128 characters; must match the authenticated Administrator’s current password. Incorrect password returns a field error without mutation. |
reason |
JSON body | Required | Nonempty string, 10–500 characters inclusive. |
Unlisted payload/query fields are rejected with 422 VALIDATION_ERROR. A conflicting merchant selector may instead be rejected earlier with 403 MERCHANT_SCOPE_MISMATCH. Administrator only; key value cannot be read back.
Validation: DELETE /api/v1/portal/merchant/integration-keys/{keyType}
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
keyType |
Path | Required | Exactly authentication_token or secret_key, case-sensitive; unsupported values return 404. |
current_password |
JSON body | Required | Nonempty string, maximum 128 characters; must match the authenticated Administrator’s current password. Incorrect password returns a field error without mutation. |
reason |
JSON body | Required | Nonempty string, 10–500 characters inclusive. |
A JSON request body is required for this DELETE; value is not accepted. Unlisted payload/query fields are rejected with 422 VALIDATION_ERROR. A conflicting merchant selector may instead be rejected earlier with 403 MERCHANT_SCOPE_MISMATCH. Administrator only; success returns 204 with no response payload.
6.5 Complete merchant request and response examples
The following are synthetic request/response pairs. Replace IDs and credential placeholders with values from your environment. GET requests have no body; filters belong in the query string. Examples show successful operations; validation and access rules above still apply.
GET /portal/merchant
Request (no body):
GET /api/v1/portal/merchant
Accept: application/json
Authorization: Bearer <user_pat>
Response: 200.
{
"success": true,
"message": "Success",
"data": {
"id": 7,
"reference_id": "MER_EXAMPLE",
"name": "Example Merchant",
"contact_person": "Alex Reyes",
"email": "operations@example.com",
"dial_code": "+63",
"phone": "9000000000",
"address": "Example business address",
"website": "https://merchant.example.com",
"logo_url": null,
"currency": "PHP",
"is_active": true,
"is_production": true,
"services": [
{
"id": 11,
"code": "COLLECTION",
"type": "collection",
"is_active": true,
"is_production": false
}
],
"created_at": "2026-09-01T00:00:00Z",
"updated_at": "2026-10-01T09:00:00Z"
}
}
PATCH /portal/merchant
Request:
PATCH /api/v1/portal/merchant
Accept: application/json
Authorization: Bearer <user_pat>
Content-Type: application/json
{
"name": "Example Merchant Updated",
"contact_person": "Alex Reyes",
"website": null
}
Response: 200.
{
"success": true,
"message": "Merchant profile updated.",
"data": {
"id": 7,
"reference_id": "MER_EXAMPLE",
"name": "Example Merchant Updated",
"contact_person": "Alex Reyes",
"email": "operations@example.com",
"dial_code": "+63",
"phone": "9000000000",
"address": "Example business address",
"website": null,
"logo_url": null,
"currency": "PHP",
"is_active": true,
"is_production": true,
"services": [
{
"id": 11,
"code": "COLLECTION",
"type": "collection",
"is_active": true,
"is_production": false
}
],
"created_at": "2026-09-01T00:00:00Z",
"updated_at": "2026-10-03T09:00:00+08:00"
}
}
GET /portal/merchant/logo
Request (no body):
GET /api/v1/portal/merchant/logo
Accept: application/json
Authorization: Bearer <user_pat>
Response: 200.
{
"success": true,
"message": "Success",
"data": {
"url": null
}
}
POST /portal/merchant/logo
Request (multipart file upload): Let curl generate the multipart boundary. Replace the local filename with a valid image.
curl --request POST "https://dlt.example.com/api/v1/portal/merchant/logo" \
--header "Authorization: Bearer <user_pat>" \
--header "Accept: application/json" \
--form "file=@merchant-logo.png;type=image/png"
Response: 200. The URL is illustrative; use the returned URL.
{
"success": true,
"message": "Success",
"data": {
"url": "https://dlt.example.com/storage/uploads/img/merchants/7/example.png"
}
}
DELETE /portal/merchant/logo
Request (no body):
DELETE /api/v1/portal/merchant/logo
Accept: application/json
Authorization: Bearer <user_pat>
Response: 204 No Content — empty body; do not parse JSON.
HTTP/1.1 204 No Content
GET /portal/merchant/machine-clients
Request (no body):
GET /api/v1/portal/merchant/machine-clients?page=1&per_page=25
Accept: application/json
Authorization: Bearer <user_pat>
Response: 200.
{
"success": true,
"message": "Success",
"data": [
{
"id": 12,
"oauth_client_id": 31,
"label": "Merchant backend primary",
"is_active": true,
"created_at": "2026-10-01T09:00:00Z",
"last_used_at": null,
"revoked_at": null,
"rotation_deadline_at": null,
"replaces_merchant_oauth_client_id": null,
"delivery": null
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
}
}
POST /portal/merchant/machine-clients
Request:
POST /api/v1/portal/merchant/machine-clients
Accept: application/json
Authorization: Bearer <user_pat>
Content-Type: application/json
{
"label": "Merchant backend primary",
"reason": "Initial merchant integration setup",
"current_password": "<administrator_password>"
}
Response: 201.
{
"success": true,
"message": "Machine client issued. Consume the secret within five minutes.",
"data": {
"client": {
"id": 12,
"oauth_client_id": 31,
"label": "Merchant backend primary",
"is_active": true,
"created_at": "2026-10-01T09:00:00Z",
"last_used_at": null,
"revoked_at": null,
"rotation_deadline_at": null,
"replaces_merchant_oauth_client_id": null
},
"delivery": {
"id": "00000000-0000-4000-8000-000000000001",
"expires_at": "2026-10-01T09:05:00Z"
}
}
}
POST /portal/merchant/machine-clients/12/rotate
Request:
POST /api/v1/portal/merchant/machine-clients/12/rotate
Accept: application/json
Authorization: Bearer <user_pat>
Content-Type: application/json
{
"label": "Merchant backend replacement",
"reason": "Scheduled merchant credential rotation",
"current_password": "<administrator_password>",
"overlap_minutes": 60
}
Response: 201.
{
"success": true,
"message": "Replacement issued. Complete cutover before the rotation deadline.",
"data": {
"client": {
"id": 13,
"oauth_client_id": 32,
"label": "Merchant backend replacement",
"is_active": true,
"created_at": "2026-10-03T09:00:00+08:00",
"last_used_at": null,
"revoked_at": null,
"rotation_deadline_at": null,
"replaces_merchant_oauth_client_id": 12
},
"delivery": {
"id": "00000000-0000-4000-8000-000000000002",
"expires_at": "2026-10-03T09:05:00+08:00"
}
}
}
The replacement points to ownership ID 12. Read the client list to see the old client's rotation_deadline_at; the replacement's deadline is null. The following revocation ends the old client's overlap early.
POST /portal/merchant/machine-clients/12/revoke
Request:
POST /api/v1/portal/merchant/machine-clients/12/revoke
Accept: application/json
Authorization: Bearer <user_pat>
Content-Type: application/json
{
"reason": "Cutover to replacement is complete",
"current_password": "<administrator_password>"
}
Response: 200.
{
"success": true,
"message": "Machine client and its tokens were revoked.",
"data": {
"id": 12,
"oauth_client_id": 31,
"label": "Merchant backend primary",
"is_active": false,
"created_at": "2026-10-01T09:00:00Z",
"last_used_at": null,
"revoked_at": "2026-10-03T09:03:00+08:00",
"rotation_deadline_at": null,
"replaces_merchant_oauth_client_id": null
}
}
POST /portal/merchant/credential-deliveries/00000000-0000-4000-8000-000000000002/consume
Request:
POST /api/v1/portal/merchant/credential-deliveries/00000000-0000-4000-8000-000000000002/consume
Accept: application/json
Authorization: Bearer <user_pat>
Content-Type: application/json
{
"current_password": "<administrator_password>"
}
Response: 200.
{
"success": true,
"message": "Success",
"data": {
"oauth_client_id": 32,
"oauth_client_secret": "<one-use-secret>",
"grant_type": "client_credentials",
"token_url": "https://checkout.example.com/oauth/token"
}
}
GET /portal/merchant/integration-keys
Request (no body):
GET /api/v1/portal/merchant/integration-keys
Accept: application/json
Authorization: Bearer <user_pat>
Response: 200.
{
"success": true,
"message": "Success",
"data": {
"has_authentication_token": false,
"has_secret_key": false,
"updated_at": null
}
}
PUT /portal/merchant/integration-keys/authentication_token
Request:
PUT /api/v1/portal/merchant/integration-keys/authentication_token
Accept: application/json
Authorization: Bearer <user_pat>
Content-Type: application/json
{
"value": "<merchant_integration_key>",
"current_password": "<administrator_password>",
"reason": "Replace merchant integration key"
}
Response: 200.
{
"success": true,
"message": "Integration key updated.",
"data": {
"has_authentication_token": true,
"has_secret_key": false,
"updated_at": "2026-10-03T09:00:00+08:00"
}
}
DELETE /portal/merchant/integration-keys/authentication_token
Request:
DELETE /api/v1/portal/merchant/integration-keys/authentication_token
Accept: application/json
Authorization: Bearer <user_pat>
Content-Type: application/json
{
"current_password": "<administrator_password>",
"reason": "Remove unused merchant integration key"
}
Response: 204 No Content — empty body; do not parse JSON.
HTTP/1.1 204 No Content
PUT /portal/merchant/integration-keys/secret_key
Request:
PUT /api/v1/portal/merchant/integration-keys/secret_key
Accept: application/json
Authorization: Bearer <user_pat>
Content-Type: application/json
{
"value": "<merchant_integration_key>",
"current_password": "<administrator_password>",
"reason": "Replace merchant integration key"
}
Response: 200.
{
"success": true,
"message": "Integration key updated.",
"data": {
"has_authentication_token": false,
"has_secret_key": true,
"updated_at": "2026-10-03T09:00:00+08:00"
}
}
DELETE /portal/merchant/integration-keys/secret_key
Request:
DELETE /api/v1/portal/merchant/integration-keys/secret_key
Accept: application/json
Authorization: Bearer <user_pat>
Content-Type: application/json
{
"current_password": "<administrator_password>",
"reason": "Remove unused merchant integration key"
}
Response: 204 No Content — empty body; do not parse JSON.
HTTP/1.1 204 No Content
Each integration-key PUT example assumes the other key is absent. DELETE accepts a JSON body, unlike logo and team deletion.