3. Authentication
3.1 Portal application identity and user access
DLT Merchant Portal is a DLT-owned ecosystem application used by businesses registered with DLT. In this guide, merchant means the registered business, and Portal user means a person authorized to manage that business through DLT Merchant Portal.
Portal API consumption involves two credentials:
| Credential | Identifies | Used by the DLT Merchant Portal backend for |
|---|---|---|
| Portal ecosystem application client ID/secret | DLT Merchant Portal itself | HTTP Basic authentication at login and invitation acceptance, with X-DLT-Application: merchant_portal |
| User personal access token (PAT) | The signed-in merchant user | Bearer authentication for protected Portal APIs and logout |
DLT provisions the Portal application identity. Successful login issues a user PAT after validating the application, user and merchant. The backend API resolves merchant access from that user and enforces the required role and merchant-portal scope. The Portal backend keeps both credentials in server-side storage.
Credentials managed on the Portal's merchant account screens are account assets, not additional Portal authentication methods. Section 6 documents those management APIs. Authentication by merchant-owned applications has its own Merchant integrations guide.
3.2 Portal login
Merchant Portal is DLT's own frontend within the DLT ecosystem, paired with the DLT backend/back office. DLT merchants use it to manage their accounts. In this flow, the browser displays DLT Merchant Portal; it does not represent a merchant-owned website or mobile application.
sequenceDiagram
participant Browser as Merchant Portal browser
participant Portal as DLT Merchant Portal backend
participant DLT as DLT backend API
Browser->>Portal: Username and password over HTTPS
Portal->>DLT: POST /api/v1/auth/login (Basic application credentials)
DLT->>DLT: Validate application, user, merchant and scopes
DLT-->>Portal: User PAT, expires_at and user
Portal-->>Browser: Portal session cookie
Browser->>Portal: Request merchant data
Portal->>DLT: Bearer user PAT
DLT-->>Portal: Authorized merchant data
Portal-->>Browser: Portal response
Use Merchant Portal browser → DLT Merchant Portal backend → DLT backend API. Keep application credentials and the user PAT on the backend; use Secure/HttpOnly cookies and CSRF protection for the browser session.
Request:
POST /api/v1/auth/login
Authorization: Basic <base64(ecosystem_client_id:ecosystem_client_secret)>
X-DLT-Application: merchant_portal
Accept: application/json
Content-Type: application/json
{
"username": "merchant.admin@example.com",
"password": "<user_password>"
}
| Input | Existing validation/behavior |
|---|---|
username |
Required string, maximum 255 characters; searched against username or email |
password |
Required string, minimum 6 and maximum 128 characters; existing shorter passwords retain login compatibility |
Authorization |
HTTP Basic credentials for the active DLT Merchant Portal ecosystem application |
X-DLT-Application |
merchant_portal, matching the supplied Portal application client ID |
Origin |
If supplied, must match one of that application's allowed origins |
Login requires an active merchant and an active Administrator (1) or Developer (10). DLT provisions the confidential application and PAT issuer; protected Portal calls require the merchant-portal scope.
200 response example:
{
"success": true,
"message": "Success",
"data": {
"access_token": "<user_passport_personal_access_token>",
"token_type": "Bearer",
"expires_at": "2026-10-01T10:00:00+00:00",
"user": {
"id": 42,
"reference_id": "USR_EXAMPLE",
"merchant_reference_id": "MER_EXAMPLE",
"merchant_id": 7,
"first_name": "Alex",
"last_name": "Reyes",
"role": 1,
"email": "merchant.admin@example.com",
"email_verified_at": "2026-09-01T00:00:00.000000Z",
"created_at": "2026-09-01T00:00:00.000000Z"
}
}
}
PAT lifetime defaults to 60 minutes. Store data.access_token and respect the returned data.expires_at; no user refresh token is issued.
On expiration or 401, clear the Portal session and require login. On 403, handle the authorization failure rather than repeatedly retrying login.
Logout: POST /api/v1/auth/logout with the user PAT, no body. Returns 200:
{
"success": true,
"message": "Logged out successfully"
}
Logout revokes the current PAT only. Also destroy the Portal browser session.
3.3 Authenticated Portal requests
After login, the DLT Merchant Portal backend reads the user PAT from its server-side session and sends it to the DLT backend API:
GET /api/v1/portal/me
Authorization: Bearer <user_pat>
Accept: application/json
The browser uses its Portal session cookie when calling the Portal backend. The backend API uses the PAT to resolve the current user, merchant and permissions. Do not send a merchant selector or repeat the application Basic credentials on protected Portal requests.
When the PAT expires or is revoked, clear the Portal session and require login again. This flow does not issue a refresh token.
3.4 Password recovery and change
| Method and path | Authentication | Body | Success |
|---|---|---|---|
POST /auth/reset-password/reset-link |
Public | email, reset_url |
200, generic account-independent message |
POST /auth/reset-password/new-password |
Public reset token | email, token, password, password_confirmation |
200, new password saved |
PUT /portal/me/password |
Administrator or Developer user PAT with portal eligibility | current_password, password, password_confirmation |
200, portal envelope with reauthentication_required=true |
Reset-link validation: email maximum 255 characters; URL maximum 150 characters, matching the configured Merchant Portal origin, without URL credentials, query, or fragment. DLT appends the reset token and email. Password reset tokens use the configured password broker; this checkout sets expiry to 60 minutes.
New passwords require 12–128 characters, uppercase and lowercase letters, numbers, symbols and matching confirmation. Current passwords have a maximum of 128 characters. Login retains its 6-character minimum for existing accounts.
Successful password change/reset revokes all user tokens and requires login. Invalid, expired or consumed reset proofs return 400. New-account invitations use the same new-password policy.
3.5 Authentication failures and throttling
| HTTP | Existing error code | Meaning |
|---|---|---|
| 401 | ECOSYSTEM_CLIENT_UNAUTHENTICATED |
Application code/credentials/origin eligibility failed |
| 401 | INVALID_CREDENTIALS |
Username/password failed |
| 401 | UNAUTHENTICATED / ACCESS_TOKEN_INVALID |
Protected-route token invalid, expired, revoked, or absent |
| 403 | MERCHANT_USER_NOT_AUTHORIZED |
Merchant user or merchant is not eligible |
| 403 | ECOSYSTEM_APPLICATION_NOT_AUTHORIZED |
Token's ecosystem application is not eligible |
| 403 | ACCESS_TOKEN_SCOPE_INSUFFICIENT |
User token lacks required application scope |
| 403 | MERCHANT_SCOPE_MISMATCH |
Supplied merchant identifier differs from authenticated merchant |
| 503 | TOKEN_ISSUER_MISCONFIGURED |
Personal-access token issuer does not match application |
Existing application API throttling defaults to 60 requests/minute per resolved user ID or IP. Public routes also have a 30/minute limiter. Login additionally defaults to 5 attempts/minute keyed by application, submitted identity, and IP. These are configured code defaults; deployment configuration can change them. Respect 429 and any Retry-After header.
3.6 Authentication/header matrix for implementation
| Call | Authorization | Additional headers | Credential holder |
|---|---|---|---|
| Login | Basic Portal application client ID/secret | X-DLT-Application: merchant_portal, Accept/JSON content type |
Portal backend |
| Invitation acceptance | Same Basic Portal application credentials | Same application header, Accept/JSON content type | Portal backend; submit emailed proof separately in JSON |
| Protected Portal APIs and logout | Bearer user PAT | Accept; JSON content type when sending JSON, multipart for logo | Portal backend acting for the logged-in user |
| Public password reset | No Basic/Bearer required by these routes | Accept/JSON content type | Portal backend can relay the public flow |
Use X-DLT-Application: merchant_portal for Portal login and invitation acceptance. Basic application authentication and the resulting user's PAT are separate credentials; after login, use Bearer for protected resource requests rather than repeating Basic login credentials. The same Authorization header cannot hold both schemes. If sending Origin to login/acceptance, it must be an allowed configured origin. A browser's session cookie alone does not authenticate it to DLT.
The DLT Merchant Portal architecture is Merchant Portal browser → DLT Merchant Portal backend → DLT backend API: keep the ecosystem secret and user PAT in server-side secret/session storage, exchange a Secure/HttpOnly browser cookie with the browser, rotate the session identifier at login and enforce CSRF on browser cookie-authenticated mutations. The browser must not call confidential login directly. The DLT Merchant Portal backend manages its browser session and cookie/CORS policies; the DLT backend API does not create or manage that session. A one-use merchant credential handover is a privileged operation described in section 6.2, not permission to persist integration secrets in frontend bundles, local storage, URLs or telemetry.
Session duration must not exceed the PAT's returned expiry. Password change/reset and actual team role/removal mutations revoke affected tokens; a successful current-user mutation can therefore be followed immediately by 401. Clear the local session on successful self-removal/demotion or password change, and on a protected-call invalid-token response. A login 401 INVALID_CREDENTIALS is a form error; 401 ECOSYSTEM_CLIENT_UNAUTHENTICATED is a Portal application/provisioning failure and should not be shown as a wrong user password. No public refresh or silent renewal is available for this user PAT.
3.7 Request validation reference
These tables reflect the current implementation’s request rules. See input conventions for required/optional/null behavior and error formats.
Validation: POST /api/v1/auth/login
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
username |
JSON body | Required | Nonempty string, maximum 255 characters; matched against stored username or email. |
password |
JSON body | Required | String, 6–128 characters; must match the stored password. Existing login passwords use this minimum; new/reset passwords use 12 characters. |
The current validator does not reject unlisted fields; they are not validated or used by this operation. Send only documented fields. Application headers must satisfy section 2.5. Invalid body fields return the legacy 422 validation envelope; incorrect credentials return 401 INVALID_CREDENTIALS.
Validation: POST /api/v1/auth/logout
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
| (none) | Body/query | No documented inputs | No payload is required; see behavior below. |
Send no request body. The controller does not validate or use payload fields. An eligible user PAT is required.
Validation: POST /api/v1/auth/reset-password/reset-link
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
email |
JSON body | Required | Valid email address, maximum 255 characters. |
reset_url |
JSON body | Required | URL, maximum 150 characters. Scheme/host/effective port must match configured APP_MERCHANT_URL (default ports normalized). HTTP or HTTPS according to that configured origin; no URL username/password, query or fragment. A path is permitted; missing/invalid Portal configuration fails validation. |
The current validator does not reject unlisted fields; they are not validated or used by this operation. Send only documented fields. A nonexistent email still receives the generic success message; this is not an account-existence check.
Validation: POST /api/v1/auth/reset-password/new-password
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
email |
JSON body | Required | Valid email address, maximum 255 characters; must identify the account bound to the reset proof. |
token |
JSON body | Required | Nonempty string, maximum 255 characters; must be the valid, unexpired proof for that email (broker expiry currently 60 minutes). |
password |
JSON body | Required | String, 12–128 characters; at least one letter, uppercase and lowercase letters, number and symbol. Must exactly match password_confirmation. |
password_confirmation |
JSON body | Required by confirmation | Same value as password; do not trim passwords. |
The current validator does not reject unlisted fields; they are not validated or used by this operation. Send only documented fields. Body validation returns the legacy 422 envelope. Invalid/expired broker proof returns 400, rather than 410.
3.8 Logout and password recovery 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.
POST /auth/logout
Request (no body):
POST /api/v1/auth/logout
Accept: application/json
Authorization: Bearer <user_pat>
Response: 200.
{
"success": true,
"message": "Logged out successfully"
}
POST /auth/reset-password/reset-link
Request:
POST /api/v1/auth/reset-password/reset-link
Accept: application/json
Content-Type: application/json
{
"email": "merchant.admin@example.com",
"reset_url": "https://portal.example.com/reset-password"
}
Response: 200.
{
"success": true,
"message": "If the account exists, a password reset link has been sent."
}
POST /auth/reset-password/new-password
Request:
POST /api/v1/auth/reset-password/new-password
Accept: application/json
Content-Type: application/json
{
"email": "merchant.admin@example.com",
"token": "<password_reset_token>",
"password": "<new_password>",
"password_confirmation": "<new_password>"
}
Response: 200.
{
"success": true,
"message": "New password has been saved."
}