Skip to content
DLTdevelopers
Sign in
DLT Merchant Portal/Account management Public documentation

Team & invitations

For DLT Merchant Portal developers · DLT maintainers · Contract v1.7

Configured API origin: https://stage-checkout.dxp.dtic.com.ph

Build with the right credentials. API access requires the documented authentication and permissions. Examples use synthetic data and placeholder origins.

7. Team management

These APIs manage the registered business's users inside DLT Merchant Portal. Both roles may list members. Administrator manages members/invitations. Acceptance uses the confidential Portal application and emailed proof, without a PAT.

7.1 Endpoints

Method and path Access Body/query Result Status
GET /portal/team/members Both page, per_page 200, member list Implemented
GET /portal/team/invitations Administrator Pagination; optional status 200, invitation list Implemented
POST /portal/team/invitations Administrator email, role 201, invitation metadata Implemented
POST /portal/team/invitations/{invitation_id}/resend Administrator None 200, new expiry and sent status Implemented
DELETE /portal/team/invitations/{invitation_id} Administrator None 204, revoked invitation Implemented
POST /portal/team/invitations/accept Confidential Portal application plus invitation proof Acceptance fields below 200, accepted membership; then normal login Implemented
PATCH /portal/team/members/{user_id}/role Administrator role 200, updated member Implemented
DELETE /portal/team/members/{user_id} Administrator None 204, membership removed Implemented

Member data: id, first_name, last_name, email, role, is_active, and joined_at. Invitation data: id, email, role, status, invited_by, created_at, and expires_at. Invitation statuses are pending, accepted, expired, and revoked. Never include invitation tokens in list/create responses.

Invitation creation:

{
  "email": "developer@example.com",
  "role": "developer"
}

201 response:

{
  "success": true,
  "message": "Invitation sent.",
  "data": {
    "id": "9b94f9dc-5482-45a5-bc65-8b360d858fc7",
    "email": "developer@example.com",
    "role": "developer",
    "status": "pending",
    "invited_by": 42,
    "created_at": "2026-10-01T09:00:00Z",
    "expires_at": "2026-10-03T09:00:00Z",
    "sent": true
  }
}

7.2 Invitations and acceptance

Invitations are valid for 48 hours and bind a normalized lowercase email to the merchant, current inviter, and exact administrator/developer role. Creation requires a valid email up to 255 characters and rejects extra fields. Resend and revoke accept no fields. UUID route syntax and merchant ownership are enforced; unknown/foreign IDs return 404. Only Administrators may list or manage invitations; Developer denial precedes validation.

GET uses the standard 25-item pagination default, per_page 1–100, and newest creation timestamp/UUID first. Optional status accepts pending, accepted, expired, or revoked; expired means an unconsumed pending record whose expiry has passed. Lists expose only the seven invitation fields in section 7.1. Creation/resend adds sent; no response includes the token, hash, or acceptance URL.

Each proof is 64 lowercase hexadecimal characters generated from 32 random bytes. Only its SHA-256 hash is persisted. Email links use DLT's Merchant Portal URL (APP_MERCHANT_URL) plus /team/invitations/accept#token=<proof>. The Portal must implement that page, extract the fragment, and have its backend submit the proof to the API; the fragment stays out of server URL query logs. No GET request consumes an invitation.

Email is sent synchronously after the audited transaction commits. sent: true means the mail send completed, not guaranteed inbox delivery. If transport throws, the invitation remains pending and the response includes sent: false with message Invitation saved. Email delivery failed; resend to retry.; provider diagnostics and secret-bearing content are not returned/logged. Resend generates a new proof, resets password-attempt count, and restarts 48-hour expiry. Old proof returns 404. An expired pending invitation may be resent; accepted/revoked invitations return 410 INVITATION_UNAVAILABLE. Another pending invitation for the same merchant/email blocks resend with 409 INVITATION_ALREADY_PENDING. Revoke returns 204, including repeated revocation, and preserves history; accepted invitations cannot be revoked (410).

Acceptance is a dedicated pre-membership flow. The Portal backend authenticates with the same Basic client credentials as login and X-DLT-Application: merchant_portal; optional Origin must match the configured allowlist. Invalid credentials, another application, or a disallowed origin return 401 ECOSYSTEM_CLIENT_UNAUTHENTICATED before accepting the body. Use the Portal application credentials for this pre-login request; no user PAT is required. Submit token plus:

  • New account: required first_name and last_name, each 1–50 characters, plus password and password_confirmation. Password is 12–128 characters with letters, mixed case, numbers, and symbols, and matching confirmation. The invitation email becomes the initial username/email.

  • Existing detached account: only token and current_password (required, max 128 characters). Additional name/new-password fields are rejected. Only a detached Administrator/Developer account may join; membership in either this or another merchant is a conflict, and internal DLT accounts are ineligible. An eligible detached inactive account is reactivated after its existing password is proven; password/profile/username remain intact.

Example new-account acceptance body:

{
  "token": "<proof_from_invitation_email>",
  "first_name": "Example",
  "last_name": "Member",
  "password": "<new_password>",
  "password_confirmation": "<new_password>"
}

Acceptance requires the inviter to remain an active Administrator of the active merchant. Merchant and role come from the invitation. Success returns the member projection, message Invitation accepted. Log in to continue., and no PAT. The invitation email is verified; prior user tokens are revoked.

Additional rules:

  • Reject invalid roles and malformed email with 422.
  • Return 409 MEMBER_ALREADY_EXISTS for an existing member; 409 INVITATION_ALREADY_PENDING for a duplicate pending invitation.
  • Under the initial single-merchant model, return 409 USER_BELONGS_TO_ANOTHER_MERCHANT rather than silently moving an existing user.
  • Malformed proofs/body/passwords return 422 VALIDATION_ERROR; unknown proofs (including rotated proofs) return 404; expired, revoked, consumed, or otherwise unavailable invitations return 410 INVITATION_UNAVAILABLE.
  • Internal DLT accounts return 409 USER_NOT_ELIGIBLE; reserved/colliding username/email identities or a concurrent unique-account conflict return 409 USER_IDENTITY_CONFLICT.
  • Wrong current password returns 422 on current_password, commits a failed-confirmation audit and attempt count, and leaves membership unchanged. After 10 wrong-password attempts, acceptance returns 410; an Administrator must resend to reset the count and rotate the proof.

Create/resend/revoke share 5 requests/minute per Administrator/merchant. Acceptance limits both 5/minute per proof-hash/IP and 5/minute per IP, plus the API limit. 429 RATE_LIMITED includes retry headers. All invitation responses are no-store. Tokens are excluded from validation-session flashing and response serialization.

7.3 Member listing and mutations

GET returns only this merchant's Administrator/Developer accounts, including inactive members, ordered by ascending user ID. Detached users and internal DLT roles are excluded. Pagination defaults to 25; per_page accepts 1–100 and page starts at 1. Empty/out-of-range pages use an empty data array with normal meta. Invalid pagination returns 422 VALIDATION_ERROR. Each item contains only the seven fields listed in section 7.1. The legacy schema has no separate membership-start timestamp, so joined_at currently projects the account's created_at (nullable), not a reconstructed invitation acceptance date.

PATCH accepts only this body, with exact lowercase role strings:

{
  "role": "developer"
}

Successful PATCH returns 200, message Member role updated., and the updated member:

{
  "success": true,
  "message": "Member role updated.",
  "data": {
    "id": 42,
    "first_name": "Example",
    "last_name": "Member",
    "email": "developer@example.com",
    "role": "developer",
    "is_active": true,
    "joined_at": "2026-10-01T09:00:00+08:00"
  }
}

DELETE accepts no input fields and returns 204 with no body. Both mutations require an active Merchant Administrator; Developer denial precedes field validation. Numeric roles, unsupported roles, or unexpected fields return 422 VALIDATION_ERROR. Member IDs must be positive canonical decimal integers, at most 20 digits; malformed IDs return 404 NOT_FOUND. A missing, detached, other-merchant, or internal-role target also returns 404, without exposing its account data. Conflicting merchant selectors return 403 MERCHANT_SCOPE_MISMATCH.

Last active Administrator: demotion/removal returns 409 LAST_ADMINISTRATOR if the target is the sole active Administrator. Inactive Administrators do not satisfy the invariant. An Administrator may demote/remove themselves when another active Administrator remains. Changing an inactive member's role does not activate them. Repeating the current role returns 200 without changing membership, revoking tokens, or creating another audit. Repeating removal returns 404 because the user is no longer a member.

Access revocation and removal: actual role changes and removals revoke the target user's active Passport access tokens and all associated refresh tokens (including those tied to already-revoked access tokens) and delete their Sanctum tokens using the existing user-token service. Other users and machine-client credentials remain unaffected. Role changes retain the membership; the target logs in again with the new role. Removal sets merchant_id to null, is_active to false, and clears remember_token; it preserves the user/password, merchant transactions, and audit records. Removal grants no access to another merchant.

Successful self-demotion/removal revokes the current PAT. Clear the Portal session after success; an active demoted member can log in again as Developer.

Team mutations are limited to 5/minute per actor, merchant, and operation, alongside the API limit. 429 RATE_LIMITED includes retry headers. GET/PATCH/DELETE responses use Cache-Control: no-store, private.

7.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/team/members

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; the list validator does not reject extra fields, which do not add filters. Merchant context still applies.

Validation: PATCH /api/v1/portal/team/members/{member}/role

Field Location Requirement Validation rules
member 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. User ID from the member directory.
role JSON body Required String; exactly administrator or developer. Numeric roles, alternate case, null and empty values are invalid.

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. Demoting the last active Administrator returns 409 LAST_ADMINISTRATOR; actual role changes revoke the target’s user tokens.

Validation: DELETE /api/v1/portal/team/members/{member}

Field Location Requirement Validation rules
member 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.

Send no request body. Any supplied body/query fields are rejected with 422. Administrator only; removing the last active Administrator returns 409 LAST_ADMINISTRATOR.

Validation: GET /api/v1/portal/team/invitations

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.
status Query Optional; required if supplied Exactly pending, accepted, expired or revoked; null/empty is invalid.

Send no request body. Administrator only. The list validator does not reject extra fields; they do not add filters.

Validation: POST /api/v1/portal/team/invitations

Field Location Requirement Validation rules
email JSON body Required RFC-valid email string, maximum 255 characters; normalized lowercase; cannot conflict with existing membership or another pending invitation for that merchant/email.
role JSON body Required String; exactly administrator or developer.

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. Membership/identity conflicts return the documented 409 codes; no invitation proof is returned in the response.

Validation: POST /api/v1/portal/team/invitations/{invitation}/resend

Field Location Requirement Validation rules
invitation Path Required UUID route identifier from the returned resource. Invalid syntax, missing or foreign resources return 404 NOT_FOUND.

Send no request body. Any supplied body/query fields are rejected with 422. Administrator only. Pending/expired-pending invitations may be resent; accepted/revoked invitations return 410; another pending invitation for the same merchant/email returns 409.

Validation: DELETE /api/v1/portal/team/invitations/{invitation}

Field Location Requirement Validation rules
invitation Path Required UUID route identifier from the returned resource. Invalid syntax, missing or foreign resources return 404 NOT_FOUND.

Send no request body. Any supplied body/query fields are rejected with 422. Administrator only. Accepted invitation returns 410; repeated revocation returns 204.

Validation: POST /api/v1/portal/team/invitations/accept

Field Location Requirement Validation rules
token JSON body Required in both flows String of exactly 64 lowercase hexadecimal characters; pattern [a-f0-9]{64}. Must match the latest unconsumed emailed proof, valid for 48 hours.
first_name JSON body Required for new account only Nonempty string, maximum 50 characters. Prohibited for an existing detached account.
last_name JSON body Required for new account only Nonempty string, maximum 50 characters. Prohibited for an existing detached account.
password JSON body Required for new account only String, 12–128 characters; letters, uppercase/lowercase, number, symbol; must match confirmation. Prohibited for an existing detached account.
password_confirmation JSON body Required for new account only Nonempty string, maximum 128 characters; exact match to password. Prohibited for an existing detached account.
current_password JSON body Required for existing detached account only Nonempty string, maximum 128 characters; must match that existing account. Prohibited for a new account.

Unlisted payload/query fields are rejected with 422 VALIDATION_ERROR. A conflicting merchant selector may instead be rejected earlier with 403 MERCHANT_SCOPE_MISMATCH. The server selects the flow from the invitation email’s existing account, not a caller flag. Do not combine both bodies or send email, role or merchant/user selectors. Basic Portal application authentication is required. Inviter and merchant must remain active/eligible. Valid-shape unknown token returns 404; expired/revoked/consumed proof returns 410. Existing membership/identity conflicts return 409.

7.5 Complete team 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/team/members

Request (no body):

GET /api/v1/portal/team/members?page=1&per_page=25
Accept: application/json
Authorization: Bearer <user_pat>

Response: 200.

{
  "success": true,
  "message": "Success",
  "data": [
    {
      "id": 43,
      "first_name": "Example",
      "last_name": "Member",
      "email": "developer@example.com",
      "role": "developer",
      "is_active": true,
      "joined_at": "2026-10-03T09:00:00+08:00"
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 25,
    "total": 1,
    "last_page": 1
  }
}

PATCH /portal/team/members/43/role

Request:

PATCH /api/v1/portal/team/members/43/role
Accept: application/json
Authorization: Bearer <user_pat>
Content-Type: application/json

{
  "role": "administrator"
}

Response: 200.

{
  "success": true,
  "message": "Member role updated.",
  "data": {
    "id": 43,
    "first_name": "Example",
    "last_name": "Member",
    "email": "developer@example.com",
    "role": "administrator",
    "is_active": true,
    "joined_at": "2026-10-03T09:00:00+08:00"
  }
}

DELETE /portal/team/members/43

Request (no body):

DELETE /api/v1/portal/team/members/43
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/team/invitations

Request (no body):

GET /api/v1/portal/team/invitations?page=1&per_page=25&status=pending
Accept: application/json
Authorization: Bearer <user_pat>

Response: 200.

{
  "success": true,
  "message": "Success",
  "data": [
    {
      "id": "9b94f9dc-5482-45a5-bc65-8b360d858fc7",
      "email": "developer@example.com",
      "role": "developer",
      "status": "pending",
      "invited_by": 42,
      "created_at": "2026-10-01T09:00:00Z",
      "expires_at": "2026-10-03T09:00:00Z"
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 25,
    "total": 1,
    "last_page": 1
  }
}

POST /portal/team/invitations

Request:

POST /api/v1/portal/team/invitations
Accept: application/json
Authorization: Bearer <user_pat>
Content-Type: application/json

{
  "email": "developer@example.com",
  "role": "developer"
}

Response: 201.

{
  "success": true,
  "message": "Invitation sent.",
  "data": {
    "id": "9b94f9dc-5482-45a5-bc65-8b360d858fc7",
    "email": "developer@example.com",
    "role": "developer",
    "status": "pending",
    "invited_by": 42,
    "created_at": "2026-10-01T09:00:00Z",
    "expires_at": "2026-10-03T09:00:00Z",
    "sent": true
  }
}

POST /portal/team/invitations/9b94f9dc-5482-45a5-bc65-8b360d858fc7/resend

Request (no body):

POST /api/v1/portal/team/invitations/9b94f9dc-5482-45a5-bc65-8b360d858fc7/resend
Accept: application/json
Authorization: Bearer <user_pat>

Response: 200.

{
  "success": true,
  "message": "Invitation sent.",
  "data": {
    "id": "9b94f9dc-5482-45a5-bc65-8b360d858fc7",
    "email": "developer@example.com",
    "role": "developer",
    "status": "pending",
    "invited_by": 42,
    "created_at": "2026-10-01T09:00:00Z",
    "expires_at": "2026-10-05T09:00:00+08:00",
    "sent": true
  }
}

DELETE /portal/team/invitations/9b94f9dc-5482-45a5-bc65-8b360d858fc7

Request (no body):

DELETE /api/v1/portal/team/invitations/9b94f9dc-5482-45a5-bc65-8b360d858fc7
Accept: application/json
Authorization: Bearer <user_pat>

Response: 204 No Content — empty body; do not parse JSON.

HTTP/1.1 204 No Content

Acceptance examples are alternatives using a valid, unrevoked invitation; do not use the invitation deleted above. Replace the token with the 64-character proof from the latest email.

POST /portal/team/invitations/accept

Request:

POST /api/v1/portal/team/invitations/accept
Accept: application/json
Authorization: Basic <base64(ecosystem_client_id:ecosystem_client_secret)>
X-DLT-Application: merchant_portal
Content-Type: application/json

{
  "token": "<proof_from_invitation_email>",
  "first_name": "Example",
  "last_name": "Member",
  "password": "<new_password>",
  "password_confirmation": "<new_password>"
}

Response: 200.

{
  "success": true,
  "message": "Invitation accepted. Log in to continue.",
  "data": {
    "id": 43,
    "first_name": "Example",
    "last_name": "Member",
    "email": "developer@example.com",
    "role": "developer",
    "is_active": true,
    "joined_at": "2026-10-03T09:00:00+08:00"
  }
}

For an eligible existing detached account, use this body instead. The response has the same member envelope, retaining that account's existing profile and account creation timestamp.

POST /portal/team/invitations/accept

Request:

POST /api/v1/portal/team/invitations/accept
Accept: application/json
Authorization: Basic <base64(ecosystem_client_id:ecosystem_client_secret)>
X-DLT-Application: merchant_portal
Content-Type: application/json

{
  "token": "<proof_from_invitation_email>",
  "current_password": "<existing_account_password>"
}

Response: 200.

{
  "success": true,
  "message": "Invitation accepted. Log in to continue.",
  "data": {
    "id": 43,
    "first_name": "Example",
    "last_name": "Member",
    "email": "developer@example.com",
    "role": "developer",
    "is_active": true,
    "joined_at": "2026-09-01T00:00:00+08:00"
  }
}
DLT Developer Documentation · Updated October 2, 2026