Sindh IT Portal — Facilitation DeskSpecification documents
Englishاردوسنڌي
← All documents

API Contract

The authoritative REST API contract for the Sindh IT Portal — Facilitation Desk (SITP): base URL, versioning, authentication, rate limiting, pagination, envelope, errors, idempotency, an OpenAPI 3.1 skeleton, the full resource catalog, the outbound webhook contract, SDK/codegen plan, deprecation, sandbox, and observability.

Field Value
Doc ID 12
Status Draft
Owner S&ITD / MAAHIR
Languages EN (master) · UR · SD
API style REST (NestJS), JSON, OpenAPI 3.1
Base URL https://sindhitportal.maahir.io/api/v1
Related docs /specs/en/08-integrations-spec/ · /specs/en/05-data-model/ · /specs/en/04-roles-permissions/ · /specs/en/11-security-compliance/ · 03-non-functional-requirements/en.md · /specs/en/15-tech-architecture/

1. Scope & How to Read This Document

This document is the engineering contract for SITP's public REST API. It is consumed by:

This document is the source of truth for the shape of the API. It expands §8 of /specs/en/08-integrations-spec/ (the Public API) into a full contract. The integrations spec remains the source of truth for individual integration adapters (NADRA, SECP, Mailjet, etc.); this document covers only the surface SITP itself exposes. Where the two disagree on a public-API detail, this document wins.

The data model behind every resource is defined in /specs/en/05-data-model/; the API field names are camelCase projections of the snake_case columns documented there.


2. Base URL, Versioning & Media Types

2.1 Base URL

Environment Base URL
Production https://sindhitportal.maahir.io/api/v1
Sandbox https://sandbox.sindhitportal.maahir.io/api/v1 (see §15)
OpenAPI document GET /api/v1/openapi.json
Interactive docs (Swagger UI) https://sindhitportal.maahir.io/docs/api

All paths in this document are relative to the base URL unless written in full.

2.2 Versioning strategy

SITP uses URL-segment major versioning (/api/v1, /api/v2). The rules:

What counts as breaking is documented in §14.2 and reflected in the changelog served at /api/v1/openapi.json (OpenAPI info.version follows SemVer within a major).

2.3 Media types

Concern Rule
Request body Content-Type: application/json (UTF-8). Multipart is used only for direct file upload to the SITP-owned endpoint when a partner cannot use the presigned-URL flow (see §10 Attachments).
Response body application/json for all resources. Errors are application/problem+json (§7).
Locale Accept-Language: en | ur | sd controls the language of human-readable fields (title, displayName, message). Unknown values fall back to en.
Calendar Prefer: calendar=hijri returns dual Gregorian+Hijri dates in date-bearing fields (per _context.md §2).
Compression gzip and br supported via Accept-Encoding.

2.4 Common headers (every request)

Header Required Purpose
Authorization Yes (except public + token-exchange endpoints) Bearer <access_token> — OAuth2 or OIDC (§3).
Accept-Language No (default en) Locale of human-readable fields.
X-Request-Id No Client-supplied correlation id; echoed back in response (§16). If absent, SITP mints one.
Idempotency-Key Required on writes (§8) UUID v4/v7; dedupes within 24 h.
User-Agent Recommended Partner identifier for diagnostics.

3. Authentication & Authorization

SITP exposes two authentication modes, both terminating at the self-hosted Keycloak instance (per /specs/en/08-integrations-spec/ §7):

  1. OAuth 2.0 client-credentials — for partner apps (machine-to-machine). No end-user in the loop.
  2. OIDC bearer (Authorization Code + PKCE) — for interactive users (company reps, government staff) who log in through the SITP UI or a federated government IdP.

Both yield short-lived bearer access tokens (JWTs) sent in the Authorization: Bearer <token> header. Tokens are never passed as query parameters.

3.1 OAuth 2.0 client-credentials (partner apps)

Partner apps are registered in the SITP partner console and receive a client_id and client_secret. Each partner is bound to one or more companyId values it is authorized to act for (the company grants the partner via OAuth-style consent in the portal). The flow:

Token request

POST /api/v1/partners/tokens
Content-Type: application/json

{
  "grant_type": "client_credentials",
  "client_id": "partner_acmeintegrator",
  "client_secret": "••••••••••••••••",
  "scope": "tickets:write tickets:read kb:read"
}

Token response

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "tickets:write tickets:read kb:read",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token"
}

Token revocation

POST /api/v1/partners/tokens/revoke
Authorization: Bearer <partner admin token>
Content-Type: application/json

{ "token": "eyJhbGciOi...", "token_type_hint": "access_token" }

Returns 204 No Content. A denylist is mirrored to Redis so revocation takes effect within seconds.

3.2 OIDC bearer (interactive users)

Company representatives and government staff authenticate through the SITP UI, which performs the OIDC Authorization Code flow with PKCE against Keycloak. The resulting access token is sent as a bearer to the API exactly like a partner token. Claims include sub, email, realm (gov-staff | company), roles[], deptCode, and 2fa_verified (per /specs/en/08-integrations-spec/ §7). Government staff tokens additionally carry a SITP role (Staff, POC, DG, Secretary, SuperAdmin) consumed by the RolesGuard/PermissionsGuard.

For SITP's own frontend, this is transparent — the SPA holds the token in a secure session. For third-party UIs that federate to the same Keycloak realm, the same mechanism applies.

3.3 Scopes

Scopes are the unit of authorization for partner apps. Interactive users are authorized by role + capability (per /specs/en/04-roles-permissions/), not by scope; their token still must satisfy the endpoint's required capability.

Scope Grants
tickets:read Read tickets, messages, attachments for authorized companies.
tickets:write Create tickets, append messages, accept/close, upload attachments.
orgs:read Read organization + representative records for authorized companies.
orgs:write Register organization, invite/update representatives.
reps:write Manage representatives (subset of orgs:write).
kb:read Read published KB articles and SOPs.
stats:read Read non-public analytics slices (the public stats endpoint needs no scope).
files:read Download attachment content via presigned URL.
files:write Initiate uploads / attach files.
webhooks:manage Create, list, delete, replay webhook subscriptions.
* Super-scope; reserved for first-party SITP clients only.

An endpoint's required scope is listed in the resource catalog (§10) and in the OpenAPI security block (§9). Missing scope → SITP-AUTH-004 (§7.3).

3.4 Public (no-auth) endpoints

A small set of read-only endpoints are public — no token required — so that citizens and researchers can consume them without registration:

Public endpoints are still subject to rate limiting (§4) at a stricter default tier.


4. Rate Limiting

Rate limits protect the platform from abusive or runaway clients. Limits are per partner (identified by client_id/token) for authenticated calls and per source IP for public calls. Limits are configurable in the int_ratelimit table (per /specs/en/05-data-model/ §3.10) and differ by partner tier and operation class.

4.1 Tier model

Tier Read budget Write budget (ticket create) Who
public 120 req/min per IP n/a (no writes) Anonymous callers of public endpoints.
standard 600 req/min 60 req/min Default for registered partners.
premium 3 000 req/min 300 req/min Contracted high-volume partners (commercial tier — TBD, see §18).
first-party 10 000 req/min 1 000 req/min SITP's own frontend, mobile, and internal services.

Write limits are separate per operation class (ticket-create, message-append, upload) so a burst of reads cannot exhaust write capacity. Bursty clients are throttled (HTTP 429), never silently banned without prior notice.

4.2 Rate-limit headers

Every response — including successful ones — carries these headers so clients can self-regulate:

Header Meaning
X-RateLimit-Limit Budget for the current window (e.g., 600).
X-RateLimit-Remaining Requests remaining in the current window.
X-RateLimit-Reset Unix timestamp at which the window resets.
X-RateLimit-Tier The tier that applied (public/standard/premium/first-party).

When exceeded:

HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 28
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1752752522

{
  "type": "https://sindhitportal.maahir.io/docs/errors/rate-limited",
  "title": "Rate limit exceeded",
  "status": 429,
  "detail": "You have exceeded 600 requests per minute. Retry after the Retry-After window.",
  "instance": "/api/v1/tickets",
  "code": "SITP-RATELIMIT-001",
  "retryAfterSeconds": 28
}

Retry-After is always present on a 429 and is given in seconds.


5. Pagination, Filtering, Sorting & Sparse Fieldsets

5.1 Pagination (cursor + page)

SITP supports two pagination styles for list endpoints. Cursor is preferred for high-volume, ordered lists; page is supported for convenience and admin UIs.

Style Parameters When to use
Cursor (default) ?cursor=<opaque>&limit=50 Default for /tickets, /tickets/{id}/messages, /kb/articles. Stable across inserts/deletes; max limit=100, default 50.
Page ?page=1&pageSize=50 Allowed on /departments, /service-catalog, /stats/public. Easier for simple UIs; page is 1-indexed; max pageSize=100.

Cursor tokens are opaque to the client (a base64url-encoded signed string); clients must treat them as black boxes and just pass them back. The response always tells the client how to continue via the links block (§6).

5.2 Filtering

Filters are whitelist-validated query parameters per endpoint. Unknown filters are rejected with SITP-VALIDATION-005. Common filters:

Filter Example Notes
Status ?status=assigned,in_progress Comma-separated multi-value; OR within.
Department ?dept=SRB Department code.
Category ?category=SRB-REFUND Category code.
Date range ?since=2026-07-01T00:00:00Z&until=2026-07-31T23:59:59Z ISO-8601, UTC.
Search ?q=refund Server delegates to Meilisearch; multilingual (per /specs/en/05-data-model/ §2.7).
Full-text on KB ?q=tax&locale=ur KB search honors locale.

5.3 Sorting

?sort=<field> ascending; ?sort=-<field> descending (leading -). Multi-sort: ?sort=-createdAt,priority. Sortable fields are listed per endpoint in OpenAPI; sorting by an unsortable field returns SITP-VALIDATION-006.

5.4 Sparse fieldsets

Clients may request only the fields they need to reduce payload size, useful for mobile and metered networks:

GET /api/v1/tickets/SITP-2026-SRB-000128?fields=ticketId,status,sla
Authorization: Bearer <token>

Returns only the requested top-level fields inside data. Unknown or non-permitted fields are silently dropped (not an error) to keep clients forward-compatible. Nested sparse selection uses dot-paths: ?fields=ticketId,sla.responseBy.


6. Response Envelope

Every successful response uses the same envelope: { data, meta, links }. Single-resource responses put the resource in data and omit meta/links when empty; list responses populate all three.

Field Always present? Meaning
data Yes The resource (object) or resources (array). null only for 204 responses.
meta No Out-of-band metadata: requestId, pagination counts, rate-limit echo, server clock, locale used.
links No Pagination/navigation links: self, next, prev, first, last.

Envelope example (list with cursor pagination)

HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: req_01HZX9F8K7P4N2Q3R6STV8WXAB
X-SITP-API-Version: v1

{
  "data": [
    { "ticketId": "SITP-2026-SRB-000127", "status": "assigned" },
    { "ticketId": "SITP-2026-SRB-000128", "status": "new" }
  ],
  "meta": {
    "requestId": "req_01HZX9F8K7P4N2Q3R6STV8WXAB",
    "locale": "en",
    "serverTime": "2026-07-17T09:18:42.117Z",
    "page": { "limit": 50, "returned": 2, "hasMore": true }
  },
  "links": {
    "self":  "/api/v1/tickets?cursor=eyJpZCI6MTI3fQ&limit=50",
    "next":  "/api/v1/tickets?cursor=eyJpZCI6MTIyOH0&limit=50",
    "prev":  null,
    "first": "/api/v1/tickets?limit=50"
  }
}

Envelope example (single resource)

{
  "data": {
    "ticketId": "SITP-2026-SRB-000128",
    "status": "new"
  },
  "meta": { "requestId": "req_01HZX9F8K7P4N2Q3R6STV8WXAB" }
}

Errors do not use this envelope — they use RFC 7807 (§7).


7. Errors (RFC 7807 Problem Details)

All errors use RFC 7807 (application/problem+json). Every error carries a SITP-specific code (stable, machine-readable) in addition to the standard fields. Clients should branch on code, never on detail (which is human-readable and localized).

7.1 Standard fields

Field Type Meaning
type URI A doc page for the error category (dereferenceable).
title string Short, stable, English summary.
status int HTTP status (echoed).
detail string Human-readable explanation, localized via Accept-Language.
instance URI The request path that failed.
code string SITP error code, e.g., SITP-VALIDATION-042.
errors[] array (Validation only) Per-field breakdown with field and code.
requestId string Correlation id (§16), always present.

7.2 Validation error example

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
X-Request-Id: req_01HZX9F8K7P4N2Q3R6STV8WXAC

{
  "type": "https://sindhitportal.maahir.io/docs/errors/validation",
  "title": "Validation failed",
  "status": 422,
  "detail": "companyId does not match the partner's authorized scope.",
  "instance": "/api/v1/tickets",
  "code": "SITP-VALIDATION-042",
  "requestId": "req_01HZX9F8K7P4N2Q3R6STV8WXAC",
  "errors": [
    { "field": "companyId", "code": "SCOPE_MISMATCH" },
    { "field": "attachments[0].size", "code": "MAX_SIZE_EXCEEDED" }
  ]
}

7.3 Error code catalog

Status codes follow HTTP. SITP error codes are grouped by family. The catalog is served live at https://sindhitportal.maahir.io/docs/errors and versioned in the OpenAPI document.

code HTTP Family Meaning
SITP-AUTH-001 401 Auth Missing Authorization header.
SITP-AUTH-002 401 Auth Token expired or malformed.
SITP-AUTH-003 400 Auth Requested scope not granted to client.
SITP-AUTH-004 403 Auth Token valid but missing required scope for this endpoint.
SITP-AUTH-005 403 Auth 2FA required for this action (sensitive/VIP).
SITP-AUTH-006 403 Auth Company rep not certified (Module O gate).
SITP-RATELIMIT-001 429 Rate limit Rate limit exceeded (see §4).
SITP-VALIDATION-001 400 Validation Malformed JSON body.
SITP-VALIDATION-002 400 Validation Missing required field.
SITP-VALIDATION-003 400 Validation Invalid field value (format/range).
SITP-VALIDATION-004 422 Validation Business-rule violation (e.g., proof gate not satisfied).
SITP-VALIDATION-005 400 Validation Unknown filter parameter.
SITP-VALIDATION-006 400 Validation Unsortable field.
SITP-VALIDATION-042 422 Validation Scope mismatch on companyId.
SITP-NOTFOUND-001 404 Not found Resource does not exist.
SITP-NOTFOUND-002 404 Not found Resource exists but caller lacks visibility (same shape as 001 to avoid leaking existence).
SITP-CONFLICT-001 409 Conflict State transition not allowed from current status.
SITP-CONFLICT-002 409 Conflict Duplicate idempotency key with different payload.
SITP-CONFLICT-003 409 Conflict Ticket already merged/closed.
SITP-UPLOAD-001 413 Upload Attachment exceeds per-file size limit.
SITP-UPLOAD-002 415 Upload Unsupported MIME type.
SITP-UPLOAD-003 422 Upload AV scan flagged the file as infected.
SITP-DEPENDENCY-001 502 Dependency Upstream integration (NADRA/SECP/…) unavailable; retry later.
SITP-DEPENDENCY-002 503 Dependency Circuit breaker open for a required adapter.
SITP-INTERNAL-001 500 Internal Unhandled server error; correlation id in requestId.

4xx errors (other than 429) are never retried by clients. 5xx and 429 may be retried with exponential backoff (see §8 and /specs/en/08-integrations-spec/ §11.2).


8. Idempotency

Every write (POST, PUT, PATCH, DELETE that mutates state) accepts the Idempotency-Key header. The header is a client-generated UUID v4 or v7. Within the 24-hour idempotency window (per NFR-INT-004 in /specs/en/08-integrations-spec/ §11):

Missing Idempotency-Key on a write returns SITP-VALIDATION-001 with a pointer at the header. The header is also surfaced in audit_logs.request_id correlation (per /specs/en/05-data-model/ §3.10) so a write and any resulting integration calls share a thread.

POST /api/v1/tickets
Authorization: Bearer <token>
Idempotency-Key: 9a2c4e6b-1f3d-4a2c-9e8b-7d6c5b4a3f2e
Content-Type: application/json

{ "companyId": "cmp_01HZX7K4P9N2Q3R6STV8WXYPJ", "title": "..." }

Replaying the identical request within 24 h returns the original 201 Created with the same ticketId, not a duplicate ticket.


9. OpenAPI 3.1 Skeleton

The full OpenAPI 3.1 document is generated from NestJS controllers via @nestjs/swagger and served at /api/v1/openapi.json, then rendered at /docs/api. The skeleton below shows the top-level document shape and three representative operations (POST /partners/tokens, POST /tickets, GET /tickets/{ticketId}). It is illustrative, not exhaustive.

openapi: 3.1.0
info:
  title: Sindh IT Portal — Facilitation Desk Public API
  version: 1.4.0
  description: >
    REST API for partner apps and interactive clients of the Sindh IT Portal.
    Base URL: https://sindhitportal.maahir.io/api/v1
  contact:
    name: SITP API Support
    email: api-support@sindhitportal.maahir.io
  license:
    name: Proprietary — Government of Sindh
servers:
  - url: https://sindhitportal.maahir.io/api/v1
    description: Production
  - url: https://sandbox.sindhitportal.maahir.io/api/v1
    description: Sandbox
tags:
  - name: Auth
  - name: Tickets
  - name: TicketMessages
  - name: Attachments
  - name: Organizations
  - name: Representatives
  - name: Departments
  - name: ServiceCatalog
  - name: KnowledgeBase
  - name: PublicStats
  - name: Webhooks
security:
  - bearerAuth: []
  - oauth2: [tickets:read]
paths:
  /partners/tokens:
    post:
      tags: [Auth]
      summary: Exchange client credentials for a bearer token (OAuth2 client-credentials)
      operationId: createPartnerToken
      security: []            # public endpoint
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TokenRequest' }
      responses:
        '200':
          description: Token issued
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TokenResponse' }
        '400': { $ref: '#/components/responses/Problem' }
        '401': { $ref: '#/components/responses/Problem' }
  /tickets:
    post:
      tags: [Tickets]
      summary: Create a ticket on behalf of a company
      operationId: createTicket
      security:
        - bearerAuth: []
        - oauth2: [tickets:write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TicketCreateRequest' }
      responses:
        '201':
          description: Ticket created
          headers:
            Location: { schema: { type: string } }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TicketEnvelope' }
        '400': { $ref: '#/components/responses/Problem' }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '422': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
    get:
      tags: [Tickets]
      summary: List tickets for the authorized company (cursor pagination)
      operationId: listTickets
      security:
        - bearerAuth: []
        - oauth2: [tickets:read]
      parameters:
        - in: query
          name: cursor
          schema: { type: string }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - in: query
          name: status
          schema: { type: array, items: { type: string } }
        - in: query
          name: dept
          schema: { type: string }
        - in: query
          name: since
          schema: { type: string, format: date-time }
      responses:
        '200':
          description: A page of tickets
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TicketListEnvelope' }
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
  /tickets/{ticketId}:
    get:
      tags: [Tickets]
      summary: Get a single ticket by tracking id
      operationId: getTicket
      security:
        - bearerAuth: []
        - oauth2: [tickets:read]
      parameters:
        - in: path
          name: ticketId
          required: true
          schema: { type: string, example: SITP-2026-SRB-000128 }
        - in: query
          name: fields
          description: Sparse fieldset
          schema: { type: string, example: ticketId,status,sla }
      responses:
        '200':
          description: The ticket
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TicketEnvelope' }
        '404': { $ref: '#/components/responses/Problem' }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    oauth2:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: /api/v1/partners/tokens
          scopes:
            tickets:read:  Read tickets, messages, attachments
            tickets:write: Create and mutate tickets
            orgs:read:     Read organization records
            orgs:write:    Register and manage organizations
            reps:write:    Manage representatives
            kb:read:       Read published KB articles
            stats:read:    Read non-public analytics slices
            files:read:    Download attachments
            files:write:   Upload and attach files
            webhooks:manage: Manage webhook subscriptions
  parameters:
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: true
      schema: { type: string, format: uuid }
    AcceptLanguage:
      in: header
      name: Accept-Language
      required: false
      schema: { type: string, enum: [en, ur, sd], default: en }
  responses:
    Problem:
      description: An RFC 7807 problem document
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
  schemas:
    Problem:
      type: object
      required: [type, title, status, code]
      properties:
        type:     { type: string, format: uri }
        title:    { type: string }
        status:   { type: integer }
        detail:   { type: string }
        instance: { type: string }
        code:     { type: string, example: SITP-VALIDATION-042 }
        requestId:{ type: string }
        errors:
          type: array
          items:
            type: object
            properties:
              field: { type: string }
              code:  { type: string }

The complete document repeats this pattern for every operation in §10, including request-body schemas, response envelopes, problem-details for every documented status, and the webhooks: extension (OpenAPI 3.1 supports native webhooks) for the events in §12.


10. Resource Catalog

The catalog below lists every endpoint in v1. Field names are stable; full schemas are in /api/v1/openapi.json. Each row carries: HTTP method, path, purpose, required auth scope (or public), and whether the write is idempotent (accepts Idempotency-Key).

Conventions: GET/HEAD are never idempotent-keyed. {ticketId} is the user-facing tracking id SITP-YYYY-DEPT-NNNNNN. All {orgId}/{repId} are opaque SITP ids.

10.1 Auth

Method Path Purpose Scope Idempotent?
POST /partners/tokens Exchange client credentials for a bearer token. — (public) Yes
POST /partners/tokens/revoke Revoke a token. * (partner admin) Yes

10.2 Tickets

Method Path Purpose Scope Idempotent?
POST /tickets Create a ticket on behalf of a company. tickets:write Yes
GET /tickets List tickets for the authorized company. tickets:read No
GET /tickets/{ticketId} Get a single ticket with status, SLA, history. tickets:read No
PATCH /tickets/{ticketId} Update editable ticket fields (priority, category). tickets:write Yes
POST /tickets/{ticketId}/accept Company accepts the resolution → triggers auto-close. tickets:write Yes

10.3 TicketMessages

Method Path Purpose Scope Idempotent?
GET /tickets/{ticketId}/messages List public-thread messages. tickets:read No
POST /tickets/{ticketId}/messages Append a public message. tickets:write Yes

10.4 Attachments

Method Path Purpose Scope Idempotent?
POST /uploads Initiate an upload; returns a presigned URL + uploadId. files:write Yes
GET /tickets/{ticketId}/attachments List attachments on a ticket. tickets:read No
GET /attachments/{attachmentId} Get attachment metadata + short-lived download URL. files:read No

The recommended partner flow is presigned-URL upload to MinIO (the partner PUTs the bytes directly to object storage; SITP never proxies the bytes). After the PUT completes, the partner references the uploadId in POST /tickets (see §11.3 of /specs/en/08-integrations-spec/). Every upload is AV-scanned by ClamAV before it is attachable (SITP-UPLOAD-003 if infected — per /specs/en/05-data-model/ §4.4 media_library.av_status).

10.5 Organizations

Method Path Purpose Scope Idempotent?
POST /organizations Register a new organization (file-first → provisional). orgs:write Yes
GET /organizations/{orgId} Get organization record + verification status. orgs:read No
GET /organizations/me Get the caller's own organization (from token). orgs:read No

10.6 Representatives

Method Path Purpose Scope Idempotent?
GET /organizations/{orgId}/representatives List representatives. orgs:read No
POST /organizations/{orgId}/representatives Invite a representative. reps:write Yes
PATCH /representatives/{repId} Update a representative (role, contact, status). reps:write Yes

10.7 Departments

Method Path Purpose Scope Idempotent?
GET /departments List departments (tree). public No
GET /departments/{deptId} Get a department with sections, hours, holidays. public No

10.8 ServiceCatalog

Method Path Purpose Scope Idempotent?
GET /service-catalog List services offered with categories and SLAs. public No
GET /service-catalog/{serviceId} Get a service with required fields and default SLA. public No

10.9 KnowledgeBase

Method Path Purpose Scope Idempotent?
GET /kb/articles List/search published KB articles (multilingual). public (subset) / kb:read (full) No
GET /kb/articles/{articleId} Get a KB article in the requested locale. public / kb:read No

10.10 PublicStats

Method Path Purpose Scope Idempotent?
GET /stats/public Public transparency dashboard aggregates (anonymized). public No
GET /stats/public/summary Headline KPIs (totals, by dept, by status). public No

The public-stats payloads are anonymized aggregates derived from the analytics model in /specs/en/17-analytics-kpis/. No individual ticket or company is identifiable.

10.11 Webhooks

Method Path Purpose Scope Idempotent?
GET /webhooks/subscriptions List the caller's webhook subscriptions. webhooks:manage No
POST /webhooks/subscriptions Create a webhook subscription (URL + events + secret). webhooks:manage Yes
DELETE /webhooks/subscriptions/{subId} Delete a subscription. webhooks:manage Yes
POST /webhooks/subscriptions/{subId}/replay Replay a window of past events. webhooks:manage Yes

Total v1 endpoints: 30.


11. Sample Request/Response Flows

Three end-to-end flows that exercise the envelope, idempotency, errors, and pagination. Field names mirror /specs/en/05-data-model/ (camelCased for the API).

11.1 Create a ticket (partner, on behalf of a company)

A partner app files a ticket on behalf of an authorized company, attaching a previously uploaded file. The call is idempotent; replaying the same Idempotency-Key within 24 h returns the identical 201 response (no duplicate ticket).

Request

POST /api/v1/tickets
Host: sindhitportal.maahir.io
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIs...
Idempotency-Key: 9a2c4e6b-1f3d-4a2c-9e8b-7d6c5b4a3f2e
X-Request-Id: req_01HZX9F8K7P4N2Q3R6STV8WXAB
Accept-Language: en
Content-Type: application/json

{
  "companyId": "cmp_01HZX7K4P9N2Q3R6STV8WXYPJ",
  "title": "Pending sales tax refund for Q4 2025",
  "description": "Our company filed the Q4 2025 SRB refund on 15 Jan 2026; no acknowledgment received.",
  "category": "SRB-REFUND",
  "targetDept": "SRB",
  "priority": "normal",
  "language": "en",
  "attachments": [
    {
      "filename": "SRB-Q4-2025-filing.pdf",
      "mimeType": "application/pdf",
      "size": 482310,
      "uploadId": "fil_upl_01HZX9F8K7P4N2Q3R6STV8WXY"
    }
  ],
  "requestedBy": { "repId": "rep_01HZX7K4P9N2Q3R6STV8WXYPJ" }
}

Response

HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/v1/tickets/SITP-2026-SRB-000128
X-Request-Id: req_01HZX9F8K7P4N2Q3R6STV8WXAB
X-SITP-API-Version: v1
X-RateLimit-Tier: standard
X-RateLimit-Remaining: 599

{
  "data": {
    "ticketId": "SITP-2026-SRB-000128",
    "status": "new",
    "state": "triage-pending",
    "category": "SRB-REFUND",
    "targetDept": "SRB",
    "priority": "normal",
    "sla": {
      "responseBy": "2026-07-19T09:00:00Z",
      "resolveBy": "2026-07-27T09:00:00Z",
      "paused": false
    },
    "createdAt": "2026-07-17T09:18:42.117Z",
    "portalUrl": "https://sindhitportal.maahir.io/tickets/SITP-2026-SRB-000128",
    "attachments": [
      {
        "filename": "SRB-Q4-2025-filing.pdf",
        "attachmentId": "fil_att_01HZX9F8K7P4N2Q3R6STV8WXZ",
        "size": 482310,
        "isEvidence": false
      }
    ]
  },
  "meta": {
    "requestId": "req_01HZX9F8K7P4N2Q3R6STV8WXAB",
    "locale": "en",
    "serverTime": "2026-07-17T09:18:42.117Z"
  }
}

A duplicate POST with the same Idempotency-Key and an identical body returns the same 201 (with the same ticketId) instead of 409/duplicate; a different body with the same key returns 409 SITP-CONFLICT-002 (§8).

11.2 Get ticket status (read, sparse fieldset)

A partner polls a single ticket and asks only for status + SLA fields to keep the payload minimal on a metered mobile link. Uses the sparse-fieldset query (§5.4).

Request

GET /api/v1/tickets/SITP-2026-SRB-000128?fields=ticketId,status,sla,assignedTo
Host: sindhitportal.maahir.io
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIs...
Accept-Language: en
X-Request-Id: req_01HZX9F8K7P4N2Q3R6STV8WXAE

Response

HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: req_01HZX9F8K7P4N2Q3R6STV8WXAE
X-SITP-API-Version: v1
X-RateLimit-Remaining: 587

{
  "data": {
    "ticketId": "SITP-2026-SRB-000128",
    "status": "assigned",
    "assignedTo": { "dept": "SRB", "section": "Refunds" },
    "sla": {
      "responseBy": "2026-07-19T09:00:00Z",
      "resolveBy": "2026-07-27T09:00:00Z",
      "paused": false,
      "firstResponseAt": null
    }
  },
  "meta": {
    "requestId": "req_01HZX9F8K7P4N2Q3R6STV8WXAE",
    "locale": "en",
    "serverTime": "2026-07-17T10:02:13.004Z",
    "requestedFields": ["ticketId", "status", "sla", "assignedTo"]
  }
}

Fields not requested (title, description, attachments, createdAt, …) are omitted. If the ticket does not exist or the caller lacks visibility, the response is 404 SITP-NOTFOUND-001 (§7.3) — same shape for both, so existence is not leaked.

11.3 List public stats (anonymous, no auth)

A citizen or researcher fetches the anonymized public-transparency aggregates. No token required; subject to the public rate-limit tier (§4.1).

Request

GET /api/v1/stats/public?since=2026-07-01T00:00:00Z&until=2026-07-31T23:59:59Z&limit=20
Host: sindhitportal.maahir.io
Accept-Language: en
X-Request-Id: req_01HZX9F8K7P4N2Q3R6STV8WXAF

Response

HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: req_01HZX9F8K7P4N2Q3R6STV8WXAF
X-SITP-API-Version: v1
X-RateLimit-Tier: public
X-RateLimit-Remaining: 118

{
  "data": {
    "window": { "since": "2026-07-01T00:00:00Z", "until": "2026-07-31T23:59:59Z" },
    "totals": {
      "ticketsReceived": 4821,
      "ticketsResolved": 3104,
      "ticketsClosed": 2877,
      "avgResolutionHours": 53.2,
      "medianResolutionHours": 38.0,
      "csatAverage": 4.2,
      "withinSlaPercent": 87.4
    },
    "byDepartment": [
      { "dept": "SRB",  "received": 1210, "resolved": 902, "withinSlaPercent": 91.1 },
      { "dept": "SITD", "received": 988,  "resolved": 741, "withinSlaPercent": 89.0 },
      { "dept": "LBR",  "received": 612,  "resolved": 388, "withinSlaPercent": 81.3 },
      { "dept": "FIN",  "received": 457,  "resolved": 290, "withinSlaPercent": 84.6 }
    ],
    "byStatus": {
      "new": 612, "triaged": 233, "assigned": 410,
      "in_progress": 374, "resolved": 227, "closed": 2877, "reopened": 88
    },
    "generatedAt": "2026-07-17T10:05:00.000Z"
  },
  "meta": {
    "requestId": "req_01HZX9F8K7P4N2Q3R6STV8WXAF",
    "locale": "en",
    "serverTime": "2026-07-17T10:05:11.882Z",
    "anonymized": true,
    "page": { "limit": 20, "returned": 4, "hasMore": false }
  },
  "links": {
    "self": "/api/v1/stats/public?since=2026-07-01T00:00:00Z&until=2026-07-31T23:59:59Z&limit=20"
  }
}

The byDepartment and byStatus breakdowns are pre-aggregated nightly (per /specs/en/17-analytics-kpis/) and cached; the meta.anonymized: true flag is always present on public-stats responses. No individual ticket, company, or rep is identifiable in any public-stats payload.


12. Webhook Contract

Webhooks let partner apps react to SITP events in real time without polling. This section is the partner-facing contract; the receiver-side mechanics (signature verify, replay protect, enqueue) are described in /specs/en/08-integrations-spec/ §9.

12.1 Events

Event Trigger Typical consumer
ticket.created New ticket filed by/for a partner's company. Partner CRM.
ticket.assigned Ticket assigned to a department/section. Partner.
ticket.status_changed Any status transition (triaged, in_progress, resolved, closed, reopened, appealed). Partner.
ticket.escalated Escalation-ladder tier fired. Partner + oversight dashboard.
ticket.resolved Resolution-proof gate satisfied. Partner.
ticket.closed Auto-close or company-accept. Partner.
ticket.message_appended Public message appended (by company or dept). Partner.
mom.published MoM published on a ticket. Partner.
organization.verified Background checks passed → Verified badge. Partner.
organization.on_hold Background check failed → on hold + appeal. Partner.

12.2 Delivery contract

12.3 HMAC-signed payload sample

POST https://partner.example.org/sitp/webhook
Content-Type: application/json
X-SITP-Event: ticket.status_changed
X-SITP-Signature: t=1752752322,v1=5b1c3729d4f8a2e6b0c1d9e7f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2
X-SITP-Delivery: dlv_01HZX9F8K7P4N2Q3R6STV8WXZ
X-Request-Id: req_01HZX9F8K7P4N2Q3R6STV8WXAD

{
  "eventId": "evt_01HZX9F8K7P4N2Q3R6STV8WXAB",
  "eventType": "ticket.status_changed",
  "occurredAt": "2026-07-17T09:38:42.000Z",
  "version": "1",
  "data": {
    "ticketId": "SITP-2026-SRB-000128",
    "companyId": "cmp_01HZX7K4P9N2Q3R6STV8WXYPJ",
    "previousStatus": "new",
    "status": "assigned",
    "assignedTo": { "dept": "SRB", "section": "Refunds" },
    "sla": { "responseBy": "2026-07-19T09:00:00Z" },
    "portalUrl": "https://sindhitportal.maahir.io/tickets/SITP-2026-SRB-000128"
  },
  "delivery": { "deliveryId": "dlv_01HZX9F8K7P4N2Q3R6STV8WXZ", "attempt": 1 }
}

Verification pseudocode (partner side):

t, v1 = parse(X-SITP-Signature)            # "1752752322", "5b1c..."
if abs(now() - t) > 300: reject            # replay window
expected = HMAC_SHA256(secret, t + "." + raw_request_body)
if not constant_time_eq(expected, v1): reject
event = JSON.parse(raw_request_body)
if seen(event.eventId): ignore              # idempotency
handle(event)

The signature is computed by SITP as HMAC_SHA256(secret, "<t>.<rawRequestBody>") and hex-encoded as v1.


13. SDK & Codegen Plan

SITP's OpenAPI 3.1 document is the single input for client SDK generation. The plan:

Channel Generator Output Status
TypeScript / browser openapi-typescript + openapi-fetch Type-safe fetch client used by the Next.js portal. Phase 1.
TypeScript / Node @hey-api/openapi-ts (or openapi-generator-cli typescript-axios) Partner SDK published to a private npm scope. Phase 1.
Python openapi-generator-cli python Partner SDK (PyPI) — favored by research/data partners. Phase 2.
React Native reuses the TS client. Phase 4.
Postman / Bruno collection auto-generated from OpenAPI. Partner onboarding pack. Phase 1.
Mock server prism (Stoplight) from the OpenAPI doc. Sandbox contract testing (§15). Phase 1.

Conventions:


14. Deprecation Policy

14.1 Lifecycle

Every API element (endpoint, field, parameter, enum value, event) moves through:

experimental → current → deprecated → sunset → removed

14.2 What counts as breaking (forces a new major)

14.3 What is non-breaking (ships in-version)

14.4 Timelines


15. Sandbox & Test Environment

A dedicated Sandbox environment lets partners build and test without touching production data.

Aspect Sandbox Production
Base URL https://sandbox.sindhitportal.maahir.io/api/v1 https://sindhitportal.maahir.io/api/v1
Data Isolated database; seeded with synthetic companies, tickets, departments. Real.
Integrations Mocked. NADRA/SECP/FBR/SRB/PSEB return canned responses (per adapter mock mode). Mailjet/SMS/WhatsApp diverted to an internal mailbox — no real emails/SMS leave the sandbox. Live.
Tokens Self-service partner console; client secrets rotated freely. Vetted.
Rate limits Same tiers, lower ceilings (tunable). Per §4.1.
Webhooks Partner endpoints receive events from the sandbox; partners may also use the mock server (prism) for offline contract testing. Live.
OpenAPI /api/v1/openapi.json on the sandbox host. /api/v1/openapi.json.

15.1 Test data

15.2 Contract testing


16. Observability — Correlation IDs

Every request — success or error — is traceable end-to-end via a correlation id.

Header Direction Purpose
X-Request-Id In (optional) / Out (always) Client-supplied or SITP-minted; echoed on the response and threaded through every log line, OTel span, and audit_logs.request_id (per /specs/en/05-data-model/ §3.10).
X-Correlation-Id In (optional) / Out (if provided) Carries an upstream id when SITP is itself called by another system (e.g., a parent government portal).
Traceparent (W3C) In/Out Standard W3C trace context; SITP is an OTel participant and propagates the trace to integration adapters (per NFR-INT-008 in /specs/en/08-integrations-spec/ §11).

Behavior:


17. Request Lifecycle (end-to-end)

The diagram shows the full lifecycle of a single authenticated write — POST /tickets — from the partner's HTTP call through every cross-cutting concern to the final response.

sequenceDiagram autonumber participant P as Partner app participant GW as nginx / API gateway participant API as NestJS API (controller) participant MW as Cross-cutting middleware<br/>(cors · ratelimit · auth · idempotency · audit · otel) participant DOM as Domain module<br/>(TicketsService) participant INT as Integration layer<br/>(adapters · resilience · DLQ) participant DB as MariaDB participant Q as BullMQ participant WH as Webhook dispatcher P->>GW: POST /api/v1/tickets\nAuthorization, Idempotency-Key, X-Request-Id GW->>API: forward (+ mints traceparent if absent) API->>MW: enter middleware chain MW->>MW: ratelimit check (429 → problem+json) MW->>MW: verify bearer token + scope (401/403 → problem+json) MW->>MW: idempotency lookup (cache hit → replay original response) MW->>MW: validate body (422 → problem+json) MW->>DOM: createTicket(dto, actor) DOM->>DB: BEGIN; insert ticket (tracking_id generated) DOM->>INT: route/notification calls (queued, idempotent) INT->>Q: enqueue (SLA timer, notifications, e-Office push) DOM->>DB: COMMIT; write audit_logs + ticket_history DOM-->>API: Ticket entity API->>WH: enqueue ticket.created event API-->>MW: build envelope {data, meta, links} MW-->>GW: 201 Created (+ X-Request-Id, rate-limit headers) GW-->>P: 201 Created, Location header Note over WH,INT: Async fan-out (after response):<br/>notifications, webhooks, SLA clock, registry checks WH->>P: POST <partner webhook URL> (signed, retried)

Written description. A partner issues POST /tickets with an Authorization bearer, an Idempotency-Key, and (optionally) an X-Request-Id. The nginx gateway forwards the call, minting a W3C traceparent if none is present. Inside NestJS, the cross-cutting middleware chain runs in a fixed order: CORS → rate limit → authentication → authorization (scope/role) → idempotency lookup → request validation → audit/OTel span open. Any failure short-circuits with an RFC 7807 problem document carrying the original X-Request-Id. If the idempotency key was seen within 24 h and the payload matches, the original response is replayed verbatim and no domain work runs. On a cache miss, the controller hands the validated DTO to the TicketsService, which opens a MariaDB transaction, generates the SITP-YYYY-DEPT-NNNNNN tracking id, inserts the ticket, writes a ticket_history row and an audit_logs row, and enqueues side-effects (SLA clock start, notifications, optional e-Office file push, registry verification jobs) to BullMQ — these run after the transaction commits so the response is never blocked by external latency. The transaction commits, the controller wraps the new ticket in the {data, meta, links} envelope, the middleware attaches the rate-limit and version headers, and the gateway returns 201 Created with a Location header pointing at the new resource. Meanwhile, the BullMQ workers fan out: notifications go out via the communications adapters, the ticket.created webhook is HMAC-signed and delivered (and retried per §12.2), and the SLA clock starts ticking within the department's business hours. Every step — gateway, middleware, domain, integration, queue — emits an OTel span tagged with the same X-Request-Id, so a partner reporting the id lets ops reconstruct the entire trace in Grafana/Loki.


18. Open Questions / TBD

# Item Status
1 Premium partner tier ceilings and commercial terms. TBD (procurement).
2 Whether sandbox should expose synthetic NADRA biometric tokens for testing high-trust flows. TBD with NADRA mock spec.
3 Final SDK languages beyond TS and Python (Go? Java?). TBD (partner demand).
4 GraphQL facet for analytics-heavy partners, or REST only. TBD (Phase 2 evaluation).
5 Public read API key program (lightweight alternative to OAuth for read-only public data consumers). TBD.
6 Whether POST /sandbox/reset is exposed to all partners or gated. TBD.
7 gRPC / protobuf internal contract for the NestJS ↔ AI microservice boundary (out of scope for this public REST doc). See /specs/en/15-tech-architecture/.

End of document.