Client Service Process Flows¶
This page explains how client-service processes requests and handles failures. See the API reference for request and response formats.
Login, authorization and logout¶
sequenceDiagram
participant U as Client app
participant G as Gateway
participant C as Client service
participant DB as PostgreSQL
participant R as Redis
U->>G: POST /auth/login
G->>C: username + password
C->>DB: load user and client
C->>C: verify password and require ACTIVE status
C->>R: save access session and refresh-token hash
C-->>U: access and refresh tokens
U->>G: GET /api/me/whoami + Bearer JWT
G->>G: verify JWT and session
G->>C: trusted identity headers
C-->>U: session context
Login returns the same Invalid username or password error when the user is missing, the password is wrong, or the user is inactive. This prevents callers from discovering valid usernames. Login does not check the parent client status.
Refresh removes the stored refresh-token hash before reloading the user. If two requests use the same token, only one succeeds and the other receives 401. After the token is removed, it cannot be retried. If token creation then fails, the client must log in again.
Logout can remove two sessions: the access token from the Authorization header and the refresh token from the optional JSON body. It returns success even when either session is already missing. Clients should send both tokens for a complete logout.
Client onboarding¶
flowchart TD
A[Validate client and contact details] --> B{Email already used?}
B -- yes --> X[409 duplicate]
B -- no --> C{Payment type}
C -- PREPAID --> D[Credit limit is null]
C -- POSTPAID --> E{Credit limit supplied?}
E -- no --> V[400 validation]
E -- yes --> F[Create ACTIVE client]
D --> F
F --> G[Create balance with initial balance or zero]
G --> H[Save and return client]
Creating a client does not create a login user. An operator must create the user through the client-user API. The email must be unique and cannot be changed later. The current update API also cannot change payment type, credit limit, or client status.
User creation and allocation¶
Creating a user hashes the password with BCrypt and sets the initial status to ACTIVE. If the request includes allocatedBalance, client-service locks the client balance and users, then checks:
Updating an allocation applies the same rule after removing the user's old allocation from the calculation. clearAllocatedBalance=true moves the user to the shared balance. If allocation fields are missing, the existing allocation stays unchanged. A password update saves a new hash but does not end active sessions.
SMS debit from the router¶
sequenceDiagram
participant R as SMS router
participant C as Client service
participant DB as PostgreSQL
R->>C: GET /internal/clients/{id}/config
C-->>R: payment type, balance, credit, and DLR setting
R->>R: choose routes and calculate total cost
R->>C: POST balance deduction with idempotency key
C->>DB: lock balance and all client users
C->>DB: find deduction by client and key
alt existing deduction not reversed
C-->>R: original remaining balance
else allocated user
C->>DB: subtract allocation and available balance
C->>DB: save deduction record
C-->>R: remaining balance
else shared user
C->>DB: validate shared pool + postpaid credit
C->>DB: subtract balance and save deduction record
C-->>R: remaining balance
end
Router uses the request idempotency key for the balance deduction. If the deduction succeeds but router cannot save the SMS request, router calls the reversal operation. If the SMS later fails with a refundable result, the responsible service calls the separate refund operation.
Retry rules¶
| Operation | Safe retry? | Rule |
|---|---|---|
| Deduction with a key | Yes, with the same key and request | A key belongs to one client and one deduction |
| Deduction without a key | No after an unclear response | It has no deduction record |
| Refund | Yes, with the same key | A repeat returns alreadyApplied:true |
| Reversal | Yes | A missing or already reversed deduction returns reversed:false |
| Admin top-up | No after an unclear response | It has no idempotency key or permanent top-up record |
reversed:false with a null balance is not an error. It means there was nothing to reverse. Never reuse a reversed deduction key for a new charge.
Report event processing¶
flowchart TD
K[Kafka sms.reporting] --> P{Parse envelope}
P -->|invalid or missing type| DLT[sms.reporting.dlt]
P -->|unknown type| Skip[Log and skip]
P -->|known type| T{Event type}
T -->|MESSAGE_SUBMITTED| S[Save recipient submission data]
T -->|MESSAGE_FINALIZED| F[Save final data if newer]
T -->|CAMPAIGN_UPSERT| J[Save campaign if version is current]
S --> A[Check for a final failure]
F --> A
A --> Done[Commit offset]
J --> Done
T -. temporary database error .-> Retry[Retry with increasing delay, then DLT]
MESSAGE_FINALIZED may arrive before MESSAGE_SUBMITTED. In that case, client-service creates a partial row. The submitted event later fills the missing fields. An older final event cannot replace a newer result. These rules make repeated or out-of-order events safe.
There is no automatic DLT consumer. Operators must inspect and correct failed records before replaying them. A replay must keep the original event identity and order.
Report queries and exports¶
All report APIs require the admin key. clientId is optional. Without it, the result can include every client, so only platform operators should make that request.
- CDR date filters include both the first and last calendar day.
- Request page numbers start at
0, but responsecurrentPagestarts at1. - A size of
0or less uses50. The maximum is200. - CDR and recipient exports read rows in batches of 2,000. Job exports use offset pages.
- Exports ignore UI page settings and include every matching row.
- CSV files start with a UTF-8 BOM, escape values using RFC 4180, and add an apostrophe before values that spreadsheet software may treat as formulas.
excludeColumnscan be repeated and ignores letter case. Unknown column names are ignored.
CSV endpoints stream the response without keeping the full file in memory. The admin-key check also runs during asynchronous processing so the export is not cut off.
Channel failure threshold and delivery¶
For each final failure, a Redis script removes old entries, adds the recipient ID only once, sets an expiry, and returns the recent failure count for that client and channel. A repeated event for the same recipient does not increase the count.
When the count reaches the configured threshold:
- Start a database transaction.
- Lock alerts for the client and channel.
- Do nothing if a recent alert is still in cooldown.
- Save
channel_failure_alertandoutbox_eventtogether. - A scheduler claims pending events without blocking other workers.
- Call the configured webhook with an optional API key, or publish to the alert Kafka topic.
- Mark the event as published, retry it, or mark it failed after five attempts.
- Retry eligible failed events after five minutes for up to three recovery rounds. Operators must handle events that still fail.
If Redis is unavailable, alert counting is skipped for that event, but the report is still saved. Report availability therefore takes priority over guaranteed alert counting.
Failure envelope behavior¶
Most JSON responses contain responseCode, responseMessage, and data. Validation returns HTTP 400, missing data returns 404, duplicates return 409, insufficient balance returns 422, and authentication failure returns 401. The admin-key filter returns 403 directly.
Some application and unexpected errors return HTTP 200 with a failed responseCode, such as SK500. Integrations must check both the HTTP status and the response body.