Skip to content

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
Hold "Ctrl" to enable pan & zoom

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]
Hold "Ctrl" to enable pan & zoom

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:

sum(existing non-null allocations) + requested allocation <= available balance

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
Hold "Ctrl" to enable pan & zoom

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]
Hold "Ctrl" to enable pan & zoom

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 response currentPage starts at 1.
  • A size of 0 or less uses 50. The maximum is 200.
  • 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.
  • excludeColumns can 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:

  1. Start a database transaction.
  2. Lock alerts for the client and channel.
  3. Do nothing if a recent alert is still in cooldown.
  4. Save channel_failure_alert and outbox_event together.
  5. A scheduler claims pending events without blocking other workers.
  6. Call the configured webhook with an optional API key, or publish to the alert Kafka topic.
  7. Mark the event as published, retry it, or mark it failed after five attempts.
  8. 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.