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:
- Partner developers — other government portals, integration partners, and large company tenants that file and track tickets programmatically.
- Internal frontend/mobile teams — the Next.js portal and the future React Native app call the same API.
- QA & test engineers — to author contract tests against §7 (errors) and §10 (resource catalog).
- Security review — §3 (auth), §4 (rate limits), and §16 (observability) cross-reference
/specs/en/11-security-compliance/.
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:
- Patch/minor changes (additive, non-breaking) ship inside the current major version. Examples: a new optional field, a new endpoint, a new enum value that clients can ignore, a new query parameter.
- Breaking changes require a new major version. Examples: removing a field, changing a field's type, changing default behavior, narrowing an enum, changing a status code.
- When a new major version ships, the previous version is supported in parallel for at least 12 months (see §14 Deprecation). Both versions run on the same deployment; routing is by URL segment.
- No
Accept-header versioning — the URL segment is the only version selector. This keeps client code, proxies, and debugging simple. - Every response carries an
X-SITP-API-Version: v1header so clients and logs can confirm which version served the request.
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):
- OAuth 2.0 client-credentials — for partner apps (machine-to-machine). No end-user in the loop.
- 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 TTL: 1 hour (3600 s). Partners must refresh before expiry; SITP never extends a token, it issues a new one.
client_secretis shown once at registration; rotation is a documented runbook (see/specs/en/11-security-compliance/).- Tokens are scope-limited (§3.3). Requesting an ungranted scope returns a
SITP-AUTH-003error (§7.3).
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:
GET /departments,GET /departments/{deptId}GET /service-catalog,GET /service-catalog/{serviceId}GET /kb/articles,GET /kb/articles/{articleId}(published subset)GET /stats/public,GET /stats/public/summary
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):
- The first request with a given key is executed and its full response (status, body, headers) is cached.
- A repeat request with the same key returns the cached response, even if the original failed validationally — clients therefore must mint a fresh key when they change the payload.
- A repeat request with the same key but a different payload body returns
SITP-CONFLICT-002(409), protecting against accidental reuse of a key for a different intent. GETrequests ignore the header.
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/HEADare never idempotent-keyed.{ticketId}is the user-facing tracking idSITP-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
- Transport: HTTPS
POSTto the partner's registered endpoint, JSON body,Content-Type: application/json. - Signing: every payload is HMAC-SHA256 signed with the subscription's shared secret. The signature is sent in the
X-SITP-Signatureheader ast=<unix-ts>,v1=<hex-signature>over the string<ts>.<raw-body>. Partners verify by recomputing the HMAC. - Replay protection: the timestamp
tis checked; requests older than 5 minutes must be rejected by the partner. Each delivery carrieseventIdanddeliveryId; SITP records both in thewebhookdelivery log (per/specs/en/05-data-model/§3.10 and/specs/en/08-integrations-spec/§9.1). - Retry: non-2xx responses are retried with exponential backoff —
10s, 30s, 2m, 10m, 1h, 6h, 24h(7 attempts). After exhaustion the delivery is flaggedfailedand surfaced in the partner dashboard for manual replay viaPOST /webhooks/subscriptions/{subId}/replay. - Ordering: events for the same
ticketIdare delivered in order via a per-endpoint queue. Cross-ticket ordering is not guaranteed. - Idempotency: partners must deduplicate on
eventId; SITP may redeliver. - Versioning: the
datablock carries aversionfield; a major shape change ships as a newversionwith parallel delivery until partners migrate.
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:
- Generated, never hand-written. SDKs are regenerated from
openapi.jsonon every contract change; manual edits are forbidden. - Stable method names.
operationIdin the OpenAPI doc is treated as a public symbol and changed only across a major version. - Versioning. SDK packages are versioned independently and pin a major API version (
sitp-sdk-ts@^1↔ APIv1). - Bundled models. Request/response types are bundled so partners get compile-time safety.
- Retry & idempotency built in. The official SDKs auto-mint
Idempotency-Keyfor writes and retry idempotently on 5xx/429 with exponential backoff (capped) — partners do not implement this themselves.
14. Deprecation Policy
14.1 Lifecycle
Every API element (endpoint, field, parameter, enum value, event) moves through:
experimental → current → deprecated → sunset → removed
- experimental: behind a feature flag, may change without notice; documented as such in OpenAPI (
x-sitp-experimental: true). Partners should not depend on these. - current: stable, covered by the versioning guarantee.
- deprecated: still functional; announces a removal date. Adds the
DeprecationandSunsetHTTP response headers (RFC 8594 / RFC 9745) on every affected response, and the OpenAPI doc marks itdeprecated: true. - sunset: returns the standard
Sunsetheader with the removal date; calls succeed but are logged for telemetry. - removed: returns
SITP-NOTFOUND-001(404) on an endpoint, or the field is absent from responses.
14.2 What counts as breaking (forces a new major)
- Removing or renaming an endpoint, field, parameter, or enum value.
- Changing a field's type, nullability, or semantics.
- Changing a default behavior clients depend on.
- Changing a status code or error
code. - Narrowing a paginated
limitmaximum.
14.3 What is non-breaking (ships in-version)
- Adding a new endpoint, field, parameter, or enum value (clients must ignore unknowns).
- Adding a new optional request field.
- Loosening validation (e.g., raising a max length).
- Reordering fields in a JSON object (clients must not depend on key order).
14.4 Timelines
- A deprecated element is supported for at least 12 months from the deprecation announcement before it can be removed (and only in a new major version).
- The previous major version is supported for at least 12 months of parallel run after the next major ships.
- Deprecation announcements are emailed to registered partners, posted on
/docs/api/changelog, and surfaced in the partner dashboard.
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
- Synthetic organizations with known
companyIds (cmp_test_*) documented in the sandbox onboarding pack. - Synthetic CNICs/NTNs that return predictable mock results (e.g.,
active,not-found,cancelled) so partners can exercise every verification branch. - A reset endpoint (
POST /sandbox/reset) restores the seed dataset for repeatable tests. Production has no such endpoint.
15.2 Contract testing
- Every release runs the OpenAPI document against the live sandbox using
prismto verify the implementation matches the contract. - Partner-facing examples in this document are validated by a CI job that replays them against the sandbox on every change.
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:
- If the client sends
X-Request-Id, SITP validates it (UUID orreq_<ulid>form) and reuses it; otherwise SITP mints one (req_<ulid>). - The id appears in: HTTP response header, the
meta.requestIdfield of the envelope, the RFC 7807requestIdfield on errors, every structured log line, every OTel span, theaudit_logs.request_idcolumn, and everyint_callrow for integration calls triggered by the request. - Partners quoting the id in a support ticket lets SITP retrieve the full request trace in Grafana/Loki in seconds.
- PII is never placed in the id or in log tags; only opaque identifiers and outcome codes are logged (per
/specs/en/11-security-compliance/).
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.
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.