← All documents
Per-integration contracts and the platform integration layer (API, webhooks, SSO) that connect the Sindh IT Portal — Facilitation Desk (SITP) to government registries, communication providers, video platforms, and partner consumers.
This document is the authoritative contract for every external system SITP talks to and every external surface it exposes. It covers:
- The integration layer itself — the architectural pattern every adapter obeys (§2).
- The integration template every adapter is described with (§3).
- Government identity & registry integrations — NADRA, SECP, FBR, SRB, PSEB, NITB e-Office (§4).
- Communications integrations — Mailjet, SMS gateway, WhatsApp Business API (§5).
- Video conferencing — Zoom, Google Meet, Microsoft Teams (§6).
- Identity & SSO — OIDC via Keycloak (§7).
- Public API for partner consumers (§8).
- Webhooks — outbound and inbound (§9).
- Data mapping & master data (§10).
- Non-functional requirements specific to integrations (§11).
- Integration roadmap and dependencies (§12).
The technology stack and high-level architecture are locked in _context.md §3 and detailed in /specs/en/15-tech-architecture/ §13. This document expands §13 of the tech architecture into per-integration contracts. Where this document and the tech architecture disagree, the tech architecture is the source of truth for the platform and this document is the source of truth for individual integration contracts.
Every external system — government registry, communication provider, video platform, partner consumer — is reached only through an adapter inside the NestJS Integrations module. No module outside Integrations imports a vendor SDK, calls a vendor HTTP endpoint directly, or reads a vendor credential. This is the anti-corruption boundary of the system: it isolates SITP's domain language and data model from each external system's quirks, schema changes, and outages.
| # |
Invariant |
Enforcement |
| 1 |
Adapter pattern |
Every external system has exactly one adapter module implementing a stable internal interface. All call sites program to the interface, never the SDK. |
| 2 |
Anti-corruption layer |
Adapters translate external payloads into SITP domain objects and back. External field names never leak past the adapter. |
| 3 |
Secrets in the vault |
Credentials, API keys, signing secrets, and OAuth client secrets live in the secrets vault (see /specs/en/15-tech-architecture/ §16), fetched at startup, rotated per §11.7. No secret is ever committed to the repo or read from a plain env file. |
| 4 |
Idempotency keys |
Every outbound state-changing call carries an idempotency key (Idempotency-Key header or vendor equivalent). The adapter and the remote system honor it so retries never double-write. |
| 5 |
Retries with exponential backoff + jitter |
Transient failures retry with capped exponential backoff and random jitter; max attempts and the dead-letter queue catch permanently-failed calls. |
| 6 |
Circuit breakers per adapter |
Each adapter has its own breaker (e.g., Opossum). A tripped breaker fails fast instead of piling up connections during an external outage. |
| 7 |
Dead-letter queue (DLQ) |
Calls that exhaust retries land in a per-adapter DLQ in BullMQ/Redis; ops triages and replays from there. |
| 8 |
Audit every call |
Every outbound call and every inbound webhook payload is recorded (sensitive fields redacted) in the int_call table with direction, adapter, target, latency, status, and a redacted payload excerpt. |
| 9 |
Queued for slow/external work |
Any call estimated > 250 ms or that crosses the trust boundary is enqueued to BullMQ, never run inline in a request. |
| 10 |
Feature-flagged |
Each adapter sits behind a feature flag so it can be disabled per environment/department without redeploy. |
The diagram below shows the runtime flow of a single outbound call from a domain module through the integration layer to an external system, and the corresponding inbound path for webhook deliveries from external systems back into SITP.
flowchart TB
subgraph Domain["NestJS domain modules"]
TKT["Tickets"]
ORG["Org / RBAC"]
NOT["Notifications"]
MTG["Meetings / TRI / MoM"]
REG["Registration"]
end
subgraph IntLayer["Integration layer (anti-corruption)"]
IFACE["Stable adapter interfaces<br/>(RegistryAdapter · CommsAdapter · VideoAdapter · ... )"]
ADP["Concrete adapters<br/>(NADRA · SECP · FBR · SRB · PSEB · e-Office · Mailjet · SMS · WA · Zoom/Meet/Teams)"]
RES["Resilience wrapper<br/>(idempotency · retry+backoff · circuit breaker · DLQ)"]
AUD["Audit recorder → int_call"]
SEC["Secrets resolver (vault)"]
MAP["Data mapping / master-data lookup"]
end
subgraph Q["BullMQ queues"]
OQ["Outbound queue (per adapter)"]
DLQ["Dead-letter queue (per adapter)"]
IQ["Inbound webhook queue"]
end
subgraph Ext["External systems"]
NADRA["NADRA Verisys"]
GOV["SECP · FBR · SRB · PSEB · NITB e-Office"]
MJ["Mailjet"]
SMS["SMS gateway"]
WA["WhatsApp Cloud API"]
VID["Zoom / Meet / Teams"]
end
subgraph Inbound["Inbound path"]
HOOK["Webhook receiver<br/>(signature verify · replay protect)"]
end
TKT --> IFACE
ORG --> IFACE
NOT --> IFACE
MTG --> IFACE
REG --> IFACE
IFACE --> ADP
SEC --> ADP
MAP --> ADP
ADP --> RES
RES --> AUD
RES --> OQ
OQ --> NADRA
OQ --> GOV
OQ --> MJ
OQ --> SMS
OQ --> WA
OQ --> VID
RES -- "exhausted" --> DLQ
MJ -. "delivery/bounce webhook" .-> HOOK
WA -. "inbound reply webhook" .-> HOOK
GOV -. "e-Office status callback" .-> HOOK
HOOK --> IQ
IQ --> Domain
The integration layer sits between the domain modules (which never know which vendor is plugged in) and the external systems. A domain module calls a stable interface such as RegistryAdapter.verifyCnic(...); the Integrations module resolves the concrete adapter (NADRA), pulls credentials from the vault, applies master-data mapping, wraps the call in the resilience layer (idempotency key → retry → circuit breaker → DLQ), records an audit row, and dispatches the actual HTTP/SDK call from a BullMQ worker. The same physical layer also accepts inbound webhooks: the webhook receiver verifies the signature, rejects replays, enqueues a normalized event, and a worker dispatches it back into the appropriate domain module. The DLQ captures any call that cannot complete within the retry budget so ops can triage and replay.
Every adapter family implements a small, stable interface. New vendors are added by writing a new concrete adapter behind an existing interface — the domain modules are not touched.
// Illustrative TypeScript contract — the shape every adapter obeys.
interface RegistryAdapter {
verifyCnic(req: CnicVerifyRequest): Promise<CnicVerifyResponse>;
lookupCompany(req: CompanyLookupRequest): Promise<CompanyLookupResponse>;
}
interface CommsAdapter {
sendEmail(req: EmailRequest): Promise<MessageSendResult>;
sendSms(req: SmsRequest): Promise<MessageSendResult>;
sendWhatsApp(req: WhatsAppRequest): Promise<MessageSendResult>;
}
interface VideoAdapter {
createMeeting(req: MeetingCreateRequest): Promise<MeetingCreateResult>;
getRecording(meetingId: string): Promise<RecordingResult>;
}
Every integration in §4–§7 is described using the same template so contracts are comparable and reviewable. The template fields:
| Field |
Meaning |
| Purpose |
What SITP uses this integration for. |
| Direction |
Inbound (external → SITP), Outbound (SITP → external), or Bidirectional. |
| Auth method |
How SITP authenticates to the external system, and (for inbound) how the external system authenticates to SITP. |
| Key operations |
The discrete calls/messages the integration supports. |
| Data contract |
Request and response field lists (and sample JSON where helpful). |
| Error handling & SLA |
Status codes handled, retry policy, circuit-breaker threshold, target latency/availability. |
| Assumptions / dependencies |
MoUs, registrations, sender-ID approvals, template approvals, network access (IP whitelisting), etc. |
| Status |
planned (no contract yet) or contracted (MoU/access in place). |
These integrations back the file-first, verify-in-parallel registration model (see _context.md §5): a company rep registers, gets a Provisional account, files immediately, and SITP verifies identity and entity standing in the background across NADRA, SECP, FBR, SRB, and PSEB. The same adapters serve ad-hoc verification during ticket handling.
Cross-cutting dependency. Every adapter in this section requires an MoU and/or API registration with the respective authority before it can go live. These are governance dependencies, tracked in §12, not engineering tasks. Until an MoU is signed, the adapter is shipped dark behind a feature flag and lookups return "verification pending — manual review" so the registration flow is unblocked.
| Field |
Value |
| Purpose |
Verify a representative's identity from their CNIC; confirm name, date of birth, family tree, and CNIC status (active/cancelled). Optional biometric verification for high-trust actions (Primary Authorized Rep transfer, sensitive-ticket closure). |
| Direction |
Outbound. |
| Auth method |
IP whitelisting at the NADRA endpoint + username/password (or mutual TLS / API key, per the executed MoU). Credentials in the secrets vault. Source IP is SITP's egress IP, registered with NADRA. |
| Key operations |
verifyCnic(cnic) → identity record; verifyBiometric(cnic, biometricToken) → match score (optional, gated by feature flag). |
| Data contract — request |
cnic (13 digits, validated), purpose ("registration" | "rep-transfer" | "sensitive-action"), optional biometricToken. |
| Data contract — response |
cnic, nameEn, nameUr (if available), fatherOrHusbandName, dob, gender, familyTreeId, presentAddress, status (active | cancelled | not-found), verificationRef (NADRA transaction id), verifiedAt. |
| Error handling & SLA |
200 → success; 404 → CNIC not found (returned as not-found, not an error); 401/403 → credential/IP failure (alert ops, no retry); 408/5xx → retry per §11.2 then DLQ. Target p95 ≤ 8 s; breaker opens after 5 consecutive failures. |
| Assumptions / dependencies |
MoU with NADRA for Verisys API access. IP whitelisting requires a static egress IP. Sovereign data: this adapter is allowed only via on-prem or NADRA-approved channel; payloads are Restricted data class, stored in int_call with heavy redaction (only verificationRef + status retained, not the full identity record). |
| Status |
planned — MoU required. |
| Field |
Value |
| Purpose |
Validate a registered company by its incorporation/registration number; fetch legal name, status (active/dormant/struck-off), registered office, and directors. Drives the Verified badge during registration and the directors list for rep authorization. |
| Direction |
Outbound. |
| Auth method |
API key + (likely) IP whitelisting, per the SECP data-access MoU. |
| Key operations |
lookupCompany(incorporationNo), lookupCompanyByName(name) (fuzzy, for search-assist during filing). |
| Data contract — request |
incorporationNo (e.g., 0012345) or name; jurisdiction (federal). |
| Data contract — response |
incorporationNo, name, status, registrationDate, registeredOfficeAddress, businessActivity, directors[] (name, cnic (if disclosed), designation), lookupRef. |
| Error handling & SLA |
As §4.1; target p95 ≤ 6 s. Mismatches between the user-entered name and the SECP record flag the registration for manual review rather than auto-reject. |
| Assumptions / dependencies |
MoU / API registration with SECP. SECP also exposes a public-name-search website that can be used as a fallback for lookups while the API MoU is in progress (manual verification). |
| Status |
planned — MoU required. |
| Field |
Value |
| Purpose |
Verify a company's National Tax Number (NTN), active-filer status, and tax profile. Contributes to the Verified badge and is a signal in the company trust score. |
| Direction |
Outbound. |
| Auth method |
API key + IP whitelisting, per FBR access agreement. FBR also publishes a public active-taxpayer list (ATL) refreshed periodically, used as a read-only fallback. |
| Key operations |
lookupNtin(ntn), filerStatus(ntnOrCnic). |
| Data contract — request |
ntn (7–8 digits) and/or cnic. |
| Data contract — response |
ntin, name, status (active | inactive), filerStatus (active-filer | non-filer), taxOffice, businessActivity, asOfTaxYear, lookupRef. |
| Error handling & SLA |
As §4.1; target p95 ≤ 6 s. Filer status is point-in-time per tax year — the response records the asOfTaxYear so stale data is detectable. |
| Assumptions / dependencies |
MoU / API access with FBR. Filer status changes yearly; ATL refresh lag is acceptable. |
| Status |
planned — MoU required. |
| Field |
Value |
| Purpose |
Validate a company's Sales Tax Registration Number (STRN) issued by the Sindh Revenue Board — particularly relevant for IT/services companies operating in Sindh. |
| Direction |
Outbound. |
| Auth method |
API key + IP whitelisting, per SRB access agreement. |
| Key operations |
validateStrn(strn). |
| Data contract — request |
strn (format-validated). |
| Data contract — response |
strn, legalName, status (active | suspended | cancelled), registrationDate, taxAuthority (SRB), lookupRef. |
| Error handling & SLA |
As §4.1; target p95 ≤ 5 s. |
| Assumptions / dependencies |
MoU / API access with SRB. SRB is a Sindh-provincial body, so this is the highest-priority provincial integration. |
| Status |
planned — MoU required (provincial priority). |
| Field |
Value |
| Purpose |
Validate membership with the Pakistan Software Export Board — both company membership and freelancer/individual membership. PSEB members receive a trust signal in the Verified badge and may be eligible for priority routing on IT-sector tickets. |
| Direction |
Outbound. |
| Auth method |
API key, per PSEB data-sharing agreement. |
| Key operations |
validateMembership({ memberType, membershipNo }) for memberType ∈ { company, freelancer }. |
| Data contract — request |
memberType, membershipNo (or cnic for freelancers), companyName. |
| Data contract — response |
membershipNo, memberType, name, status (active | expired | not-found), validUntil, lookupRef. |
| Error handling & SLA |
As §4.1; target p95 ≤ 5 s. An expired membership does not block registration — it only removes the trust signal. |
| Assumptions / dependencies |
Data-sharing agreement with PSEB. PSEB membership data lags renewals by days; an "expired" result that is < 30 days past validUntil is treated as "pending renewal". |
| Status |
planned — agreement required. |
| Field |
Value |
| Purpose |
Push a SITP ticket, MoM, or resolution certificate into the government's e-Office system as an official file movement so the receiving department can process it through their statutory file workflow; pull status back so SITP reflects the official-file state on the ticket. |
| Direction |
Bidirectional. Outbound (create file movement, attach document); Inbound (status callbacks from e-Office). |
| Auth method |
Service account in e-Office (managed by NITB) + API key; inbound callbacks signed with a shared HMAC secret. |
| Key operations |
createFileMovement(ticketOrMom), attachDocument(fileId, docRef), getStatus(fileId) (poll fallback), inbound onStatusChange(fileId, status). |
| Data contract — push request |
sourceRef (SITP-2026-ITD-000045), type (ticket | mom | resolution), title, summary, originatingDept (S&ITD), targetDept, priority, documents[] (fil_attachment refs → presigned URLs the e-Office side fetches), requestedBy. |
| Data contract — push response |
fileId (e-Office's file number), status (submitted), acceptedAt. |
| Data contract — status callback |
fileId, sourceRef, status (submitted | under-process | approved | returned | rejected), currentDesk, updatedAt, note. |
| Error handling & SLA |
2xx → success; 409 → file already exists (treat as idempotent success using the idempotency key); 5xx → retry per §11.2 then DLQ; missing callback → poll getStatus every 30 min as fallback. Target p95 ≤ 10 s for push; status freshness ≤ 30 min. |
| Assumptions / dependencies |
MoU with NITB and a service account in the e-Office instance. Document exchange requires e-Office to fetch from presigned URLs (or alternatively receive base64 payloads — adapter supports both). |
| Status |
planned — MoU required. High value for inter-departmental legitimacy. |
The communications adapters back the notifications pipeline (see /specs/en/15-tech-architecture/ §12). They are outbound by default with inbound paths for two-way replies (email reply, WhatsApp reply) that append to a ticket thread. Every outbound send carries an Idempotency-Key so duplicate queue jobs never send twice.
| Field |
Value |
| Purpose |
Transactional notifications (ticket created/updated/resolved, MoM published, escalation, digests), multilingual templated email, and inbound reply parsing (a reply to a notification email is appended to the originating ticket). |
| Direction |
Bidirectional (outbound send; inbound delivery/bounce/spam webhooks + inbound parse of replies). |
| Auth method |
SMTP (for legacy send paths) and REST API with API key + secret in the vault. Sender domain authentication: SPF, DKIM, and DMARC configured for the sender domains maahir.io and sindhitportal.maahir.io; Mailjet's verified sender domain record in place. Inbound webhook signed with Mailjet's signature header, verified by SITP. |
| Key operations |
sendEmail(...), sendTemplate(...), inbound onDelivery, onBounce, onSpam, onInboundReply. |
| Data contract — send request |
to[], cc[], bcc[], from (no-reply@sindhitportal.maahir.io), replyTo (per-ticket address that routes inbound back to SITP), templateId (Mailjet template ID), variables (locale, ticketId, names, dates — both Gregorian and Hijri pre-rendered server-side), idempotencyKey, tags[] (ticketId, dept, notificationType). |
| Data contract — send response |
messageId (Mailjet id), status (sent | queued), acceptedAt. |
| Data contract — delivery webhook |
event (sent | delivered | bounce | blocked | spam | open | click), messageId, email, time, reason, ticketId (from tags). |
| Error handling & SLA |
2xx → ok; 422 (validation) → no retry, log; 5xx/429 → retry per §11.2. Bounces and blocks update the recipient's email_deliverable flag and trigger fallback to SMS/in-app per the notifications pipeline. Target p95 ≤ 4 s send ack; delivery event freshness ≤ 5 min. |
| Assumptions / dependencies |
Mailjet account provisioned; sender domains verified; reply-to routing configured (Mailjet routes a per-ticket inbound address to SITP's inbound webhook). Transactional templates pre-approved (no marketing content). |
| Status |
contracted (Mailjet is the locked SMTP/email provider per _context.md §3). |
POST /api/v1/integrations/mailjet/send
Authorization: Bearer <service JWT>
Idempotency-Key: 7f3c1a2e-9b44-4d21-8e6a-2c9b1f4d0a55
Content-Type: application/json
{
"to": [{ "email": "primary.rep@acme.com.pk", "name": "Ayesha Khan" }],
"from": { "email": "no-reply@sindhitportal.maahir.io", "name": "Sindh IT Portal" },
"replyTo": { "email": "ticket-SITP-2026-ITD-000045@inbound.sindhitportal.maahir.io" },
"templateId": 4821103,
"variables": {
"locale": "en",
"ticketId": "SITP-2026-ITD-000045",
"subject": "Your ticket has been assigned",
"bodyMarkdown": "Ticket SITP-2026-ITD-000045 has been assigned to the IT Department...",
"gregorianDate": "17 July 2026",
"hijriDate": "2 Muharram 1448",
"dept": "S&ITD",
"portalUrl": "https://sindhitportal.maahir.io/tickets/SITP-2026-ITD-000045"
},
"tags": ["ticket:SITP-2026-ITD-000045", "type:assigned", "dept:ITD"],
"channel": "email"
}
HTTP/1.1 202 Accepted
Content-Type: application/json
{
"messageId": "1948234751920384",
"status": "queued",
"acceptedAt": "2026-07-17T09:14:22.481Z",
"adapter": "mailjet",
"intCallId": "int_call_01HZX9F8K7P4N2Q3R6STV8WXY"
}
| Field |
Value |
| Purpose |
OTP delivery (2FA fallback for company reps), short ticket notifications (created, assigned, escalated, resolved), and escalation alerts to staff. |
| Direction |
Bidirectional (outbound send; inbound delivery reports). |
| Auth method |
API key + sender ID; credentials in the vault. DLT / PEPRA / Pakistan regulatory compliance: sender ID pre-approved; transactional vs promotional traffic separated; opt-out respected for any promotional class (SITP sends transactional only). |
| Key operations |
sendSms(...), inbound onDeliveryReport. |
| Data contract — send request |
to (E.164 MSISDN, validated), from (approved sender ID, e.g., SITP), body (concatenation-aware, locale-correct, no Urdu-in-SMS-7-bit pitfalls — Sindhi/Urdu trigger UCS-2 with the 70-char segment limit), idempotencyKey, tags[]. |
| Data contract — send response |
messageId (gateway id), segmentCount, status (accepted | rejected). |
| Data contract — delivery report |
messageId, msisdn, status (delivered | failed | pending), errorCode, deliveredAt. |
| Error handling & SLA |
2xx → accepted; 4xx (invalid number, sender-ID rejected) → no retry; 5xx → retry per §11.2. Delivery failures update the recipient's sms_deliverable flag and trigger fallback to email/in-app. Target p95 ≤ 3 s send ack; delivery report freshness ≤ 10 min. Failover between Jazz and Telenor is configurable per message class. |
| Assumptions / dependencies |
Bulk-SMS account with Jazz and/or Telenor; sender ID approved by the PTA-registered aggregator; UCS-2 support confirmed for Sindhi. |
| Status |
contracted (stack-locked SMS providers). |
| Field |
Value |
| Purpose |
Two-way chat with company reps; templated notifications (ticket created/assigned/resolved, MoM published); inbound replies append to a ticket thread; opt-in/opt-out management. |
| Direction |
Bidirectional (outbound template + session messages; inbound reply webhook). |
| Auth method |
WhatsApp Business Cloud API — System-User access token (graph API) with the whatsapp_business_messaging permission; phone-number ID and WABA ID in the vault. App secret used to verify the inbound webhook X-Hub-Signature-256 (HMAC-SHA256). |
| Key operations |
sendTemplate(...), sendText(...) (within 24-hour customer-service window), markMessageRead, inbound onMessage (text/media/voice), onMessageStatus, optIn/optOut. |
| Data contract — send template request |
to (E.164), templateName (pre-approved, e.g., ticket_assigned_en), language (en | ur | sd), components[] (header/body parameters: ticketId, dept, status, portalUrl), idempotencyKey. |
| Data contract — send response |
messageId (WA id), status (queued). |
| Data contract — inbound webhook |
from, messageId, type (text | image | document | voice), text / media ref, timestamp, replyContext (the ticket ref parsed from the most-recent outbound message). |
| Error handling & SLA |
2xx → ok; 4xx (template not approved, recipient opted out, outside 24h window for non-template) → no retry, log + notify dispatcher to fall back; 5xx/429 → retry per §11.2. Recipient opt-out is honored immediately and propagated to the preference center. Target p95 ≤ 5 s send ack; inbound freshness ≤ 30 s. |
| Assumptions / dependencies |
WhatsApp Business Account approved; templates pre-approved by Meta for EN/UR/SD; display phone number verified; explicit opt-in recorded per recipient before any template send (compliance). |
| Status |
planned — WABA + template approvals required. |
| Field |
Value |
| Purpose |
Create a virtual meeting per TRI/hearing (Company + S&ITD + Department) when a physical-only meeting is not required; generate a join link for participants; pull the recording and transcript after the meeting for the MoM extraction pipeline. |
| Direction |
Outbound (create meeting, fetch recording/transcript); Inbound (optional recording-ready webhook). |
| Auth method |
OAuth 2.0 with each provider (Zoom OAuth + Server-to-Server; Google Workspace domain-wide delegation; Microsoft Teams application permissions). OAuth tokens + refresh tokens in the vault; refreshed by a scheduled job. |
| Key operations |
createMeeting(...), getJoinLink(meetingId), getRecording(meetingId), getTranscript(meetingId), inbound onRecordingReady. |
| Data contract — create request |
provider (zoom | meet | teams), topic (e.g., TRI — SITP-2026-ITD-000045), startTime (UTC), durationMinutes, agenda (pre-filled by AI from ticket history), participants[] (emails — invited via the provider), hostEmail (the S&ITD facilitator), record (boolean), transcribe (boolean, gated by feature flag). |
| Data contract — create response |
provider, meetingId, joinUrl, hostUrl, password (if any), calendarEventId. |
| Data contract — recording-ready webhook |
meetingId, provider, downloadUrl (presigned, short-lived), transcriptUrl, durationSeconds, sourceRef. |
| Error handling & SLA |
2xx → ok; 401 → token refresh then retry once; 4xx → no retry; 5xx → retry per §11.2. Provider selection is configurable per meeting (a S&ITD facilitator may pick Zoom today and Teams tomorrow). Recordings are pulled (SITP fetches) not pushed, so a missing recording triggers a poll fallback. Target p95 ≤ 6 s for createMeeting. |
| Assumptions / dependencies |
OAuth apps registered with Zoom, Google, and Microsoft; recording and transcription features enabled per account; recordings downloaded and stored in MinIO (then deleted from the provider if the data-classification policy requires). Transcript text is fed to the MoM action-item extractor (AI capability #11). |
| Status |
planned — OAuth app registration required for each provider. |
| Field |
Value |
| Purpose |
Government staff federated login via OIDC SSO so S&ITD and department staff sign in with their existing government identity (federated IdP) instead of a new SITP-local password; map group/role claims from the IdP into SITP roles; enforce 2FA for staff. Company representatives / citizens log in via the SITP-local Keycloak realm (email/password + 2FA optional). |
| Direction |
Bidirectional (SITP ↔ Keycloak; Keycloak ↔ government IdP). |
| Auth method |
OIDC Authorization Code flow with PKCE; short-lived access tokens (minutes) + refresh tokens (days), revocable via a denylist mirrored to Redis. Client secrets in the vault. |
| Key operations |
authorize, token, userInfo, refresh, logout, introspect. |
| Data contract — token claims |
sub, email, name, locale, realm (gov-staff | company), groups[] (from IdP, e.g., SITD-Staff, LBR-Section), roles[] (mapped from groups via Keycloak role-claim mapper → SITP roles: Staff, POC, DG, Secretary, SuperAdmin), deptCode, 2fa_verified (boolean). |
| Data contract — RBAC mapping |
A Keycloak mapper translates IdP groups into SITP roles; SITP's RolesGuard/PermissionsGuard consumes the role claims; granular per-permission overrides live in SITP (not Keycloak). |
| Error handling & SLA |
401 → prompt re-auth; 403 → role/permission denied (logged); token-introspect failures fail closed (deny). Keycloak is treated as a critical dependency — health-checked continuously. |
| Assumptions / dependencies |
Keycloak self-hosted (stack-locked); government IdP federation requires coordination with the government identity authority (e.g., NADRA FBR-style staff IdP or departmental AD). Until federation is in place, staff use Keycloak-local accounts with mandatory 2FA. |
| Status |
contracted (Keycloak stack-locked); federation path planned pending IdP agreement. |
The SITP exposes a versioned REST API at /api/v1 for authorized partner consumers — other government portals, integration partners, and large company tenants that want to file and track tickets programmatically. The public API is documented as OpenAPI 3.1, served at /api/v1/openapi.json and rendered at /docs/api.
| Concern |
Standard |
| Versioning |
URL-segment versioning (/api/v1, /api/v2). Breaking changes require a new major version; old version supported in parallel for ≥ 12 months. |
| Auth |
OAuth 2.0 client-credentials flow for machine-to-machine partners (client_id + client_secret → bearer access token, ≤ 1 hour TTL); optional partner API keys for read-only public resources. Tokens are scope-limited (tickets:write, tickets:read, kb:read, stats:read). |
| Rate limits |
Per-state (configurable in int_ratelimit): default 600 req/min per partner for read; 60 req/min for ticket creation. 429 returned with Retry-After. Bursty partners are throttled, never banned without notice. |
| Pagination |
Cursor-based (?cursor=...&limit=50, max limit=100) for list endpoints; total counts returned in a meta block where computable cheaply. |
| Filtering |
Query-string filters (?status=...&dept=...&since=...); filters whitelist-validated server-side. |
| Errors |
RFC 7807 application/problem+json with type, title, status, detail, instance, plus a SITP-specific code (e.g., SITP-VALIDATION-001). |
| Idempotency |
All POST/PUT accept Idempotency-Key; the same key within the 24-hour window returns the original response. |
| Localization |
`Accept-Language: en |
| Resource |
Methods |
Scope |
Notes |
/partners/tokens |
POST |
— |
Exchange client credentials for a bearer token. |
/tickets |
GET, POST |
tickets:read, tickets:write |
Authorized partners create/track tickets on behalf of a company (with that company's companyId). |
/tickets/{ticketId} |
GET |
tickets:read |
Single ticket with status, SLA, history (role-filtered). |
/tickets/{ticketId}/messages |
GET, POST |
tickets:write |
Append a public message; list messages. |
/kb/articles |
GET |
kb:read |
Public KB articles (no auth required for public subset). |
/stats/public |
GET |
stats:read or public |
Public transparency dashboard aggregates (anonymized). |
/departments |
GET |
public |
Department directory + service catalog. |
/service-catalog |
GET |
public |
Services offered with categories, SLAs, and required fields. |
/webhooks/subscriptions |
GET, POST, DELETE |
webhooks:manage |
Manage outbound webhook subscriptions (see §9). |
POST /api/v1/tickets
Authorization: Bearer <partner bearer token>
Idempotency-Key: 9a2c4e6b-1f3d-4a2c-9e8b-7d6c5b4a3f2e
Content-Type: application/json
Accept-Language: en
{
"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" }
}
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/v1/tickets/SITP-2026-SRB-000128
{
"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" },
"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" }
]
}
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"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",
"errors": [{ "field": "companyId", "code": "SCOPE_MISMATCH" }]
}
Webhooks let partners react to SITP events in real time (outbound) and let external systems push events to SITP (inbound).
| Event |
Trigger |
Typical consumer |
ticket.created |
New ticket filed by/for a partner's company |
Partner CRM |
ticket.assigned |
Ticket assigned to a department |
Partner |
ticket.status_changed |
Any status transition (triaged, in-progress, resolved, closed, reopened, appealed) |
Partner |
ticket.escalated |
Escalation ladder step fired |
Partner + oversight dashboard |
mom.published |
MoM published on a ticket |
Partner |
ticket.resolved |
Resolution-proof gate satisfied |
Partner |
ticket.closed |
Auto-close or company-accept |
Partner |
Delivery contract:
- Transport: HTTPS POST to 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-Signature header as t=<unix-ts>,v1=<hex-signature> over the string <ts>.<body>. Partners verify by recomputing the HMAC.
- Replay protection: the timestamp in the header is checked; requests more than 5 minutes old are rejected by the partner. The webhook also carries an
eventId and deliveryId; SITP records both in int_webhook_delivery.
- Retry: non-2xx responses are retried with exponential backoff (10s, 30s, 2m, 10m, 1h, 6h, 24h — 7 attempts); after exhaustion the delivery is flagged
failed and surfaced in the partner dashboard for manual replay.
- Ordering: events for the same
ticketId are delivered in order via a per-endpoint queue; cross-ticket ordering is not guaranteed.
- Idempotency: partners must deduplicate on
eventId; SITP may redeliver.
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
{
"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 }
}
The signature is computed as HMAC_SHA256(secret, "1752752322." + rawBody) and sent hex-encoded as v1. Partners read t from the header, reject if abs(now - t) > 300, then recompute the HMAC over t + "." + rawRequestBody and compare. The data block is versioned (version: "1") so the shape can evolve without breaking existing parsers; a major change ships as version: "2" with a parallel delivery until partners migrate.
Inbound webhooks are how external systems push events to SITP. The webhook receiver is the single front door for all of them: it verifies signatures, rejects replays, normalizes into an internal event, and enqueues to a BullMQ queue for a worker to dispatch into the right domain module.
| Source |
Event |
Domain effect |
| Mailjet |
delivered, bounce, blocked, spam |
Update recipient deliverability flags. |
| Mailjet (inbound parse) |
Reply email |
Append to the ticket parsed from the reply-to address; notify watchers. |
| WhatsApp Cloud API |
Inbound message / status |
Append reply to ticket; update delivery status. |
| SMS gateway |
Delivery report |
Update sms_deliverable flag. |
| NITB e-Office |
onStatusChange |
Update ticket's e-Office file status; show "under official process". |
| Video provider |
Recording ready |
Trigger recording fetch → MoM pipeline. |
| Partner (push-back) |
Acknowledgment, custom status |
Update partner integration record. |
Every inbound payload is recorded (sensitive fields redacted) in int_call with direction inbound, source, signature-verification result, and the dispatch outcome.
Each external system speaks its own field names and code sets; SITP keeps one canonical model and translates at the adapter boundary.
| Mapping |
Source |
SITP canonical |
Notes |
| Department code |
External (e-Office, IdP group names) |
dept_code (e.g., ITD, LBR, SRB) |
Maintained in int_dept_map; one direction = lookup, the other = inverse-lookup. |
| Service/category code |
Service catalog (internal) ↔ external dept categories |
category_code (e.g., SRB-REFUND) |
Drives auto-routing. |
| Status codes |
Each adapter's status vocabulary |
SITP ticket lifecycle enum |
e-Office statuses (submitted, under-process, approved, returned, rejected) map to ticket state extensions, not replacements. |
| Locale |
ISO 639-1 (en, ur, sd) |
Same |
Used in templates and AI translation. |
| Calendar |
Gregorian (storage) ↔ Hijri (display) |
Both |
Computed at presentation; never stored twice. |
- Idempotency keys are SITP-generated UUIDs for outbound calls and
int_call row IDs for inbound webhooks; the same key always returns the original outcome within the 24-hour window.
- Reconciliation: a scheduled job compares each integration's recent outbound calls against expected outcomes (e.g., "every registered company should have a NADRA + SECP + FBR verification record within 24 hours of filing"). Gaps surface in the ops dashboard for manual reconciliation.
- Master-data sync: department/category mappings are versioned; changes are audited in
int_map_change so historic tickets keep their routing context even after a mapping is renamed.
These NFRs apply to every adapter and are the operational contract for the integration layer. Authoritative NFRs for the whole system are in 03-non-functional-requirements/en.md; the items here are integration-specific refinements.
| ID |
Concern |
Target |
NFR-INT-001 |
Timeout (default) |
10 s connect + response per call, configurable per adapter (e.g., NADRA 8 s, Mailjet 4 s, SMS 3 s, e-Office push 10 s). |
NFR-INT-002 |
Retries |
Max 5 attempts with exponential backoff + jitter: 1s, 4s, 16s, 60s, 240s (capped). 4xx (non-401/429) are not retried. |
NFR-INT-003 |
Circuit-breaker thresholds |
Open after 5 consecutive failures or > 50% failure rate over 30 s; half-open after 60 s; close after 5 successful half-open calls. Per adapter. |
NFR-INT-004 |
Idempotency window |
24 hours; keyed by SITP-generated UUID or vendor idempotency token; responses cached and replayed on key match. |
NFR-INT-005 |
DLQ |
Per-adapter dead-letter queue; alert fires on any DLQ entry; ops dashboard + replay UI; max retention 30 days then exported. |
NFR-INT-006 |
Availability |
Integration layer ≥ 99.9% measured from inside SITP; per-adapter availability depends on the vendor and is tracked separately in the status page. |
NFR-INT-007 |
Secrets rotation |
Provider credentials rotated every 90 days (or per vendor policy, whichever is shorter); rotation is a documented runbook; OAuth tokens refreshed automatically; rotation events audited. |
NFR-INT-008 |
Observability per integration |
Each call emits an OTel span tagged with adapter, operation, outcome; p50/p95/p99 latency, error rate, and DLQ depth dashboards per adapter in Grafana; alerts on latency and error anomalies. |
NFR-INT-009 |
Audit retention |
int_call rows retained per the data-classification policy (default 2 years hot, archived thereafter); sensitive fields redacted at write time. |
NFR-INT-010 |
Data residency |
Sovereign-data adapters (NADRA, CNIC-bearing) are restricted to on-prem or vendor-approved channels; payloads are Restricted data class. |
Integrations are sequenced by dependency (MoU/access) and value (what unblocks the file-first registration flow and the ticket lifecycle). Phase mapping aligns with /specs/en/14-roadmap-release/.
| Integration |
Why Phase 1 |
Dependency |
| Mailjet (email) |
Primary notification channel; transactional flow depends on it. |
Mailjet account (✅ stack-locked); sender-domain verification. |
| SMS gateway (Jazz/Telenor) |
OTP + critical notifications. |
Bulk-SMS account; sender-ID approval. |
| Keycloak OIDC (local + staff 2FA) |
Authentication backbone. |
Self-hosted (✅ stack-locked). |
| Public REST API v1 (tickets + KB + stats) |
Partner onboarding and transparency. |
None external. |
| Integration |
Why Phase 2 |
Dependency |
| NADRA Verisys |
Rep identity verification — backs Verified badge. |
MoU with NADRA; IP whitelisting. |
| SECP |
Company lookup — backs Verified badge. |
MoU / API access with SECP. |
| FBR (NTN + filer) |
Tax profile — backs Verified badge. |
MoU / API access with FBR. |
| SRB (STRN) |
Provincial priority for Sindh IT companies. |
MoU / API access with SRB. |
| PSEB |
Membership trust signal. |
Data-sharing agreement with PSEB. |
| WhatsApp Business API |
Two-way chat + templated notifications. |
WABA approval + template approvals. |
| Integration |
Why Phase 3 |
Dependency |
| NITB e-Office |
Official file movement — inter-departmental legitimacy. |
MoU with NITB; e-Office service account. |
| Video providers (Zoom/Meet/Teams) |
Hybrid TRI/hearing virtual meetings. |
OAuth app registration per provider. |
| Outbound webhooks |
Partner event delivery. |
None external (consumer-driven). |
| OIDC federation to government IdP |
Staff single sign-on. |
Government IdP agreement. |
- Inbound IVR / toll-free voice integration.
- Direct integration with the existing CM Complaint Cell (
istd.sindh.gov.pk/complains) — only if data-sharing is agreed; out of scope for V1.
- Additional video providers or PSTN-dialout for hybrid hearings.
- Public mobile-app push via FCM/APNS (when the React Native app ships).
| Dependency |
Type |
Blocks |
| NADRA Verisys MoU |
Governance |
Rep identity verification. |
| SECP API access |
Governance |
Company verification. |
| FBR API access |
Governance |
Tax verification. |
| SRB API access |
Governance |
STRN verification (provincial). |
| PSEB data-sharing |
Governance |
Membership validation. |
| NITB e-Office MoU + service account |
Governance + technical |
Official file movement. |
| Mailjet sender-domain verification |
Technical |
Outbound email reputation. |
| SMS sender-ID (PTA) approval |
Regulatory |
SMS delivery. |
| WhatsApp WABA + templates |
Vendor |
WhatsApp notifications + two-way chat. |
| Government IdP federation |
Governance |
Staff SSO. |
| Zoom/Google/Microsoft OAuth apps |
Vendor |
TRI/hearing video. |
Until each dependency closes, the corresponding adapter ships dark behind a feature flag and the system degrades gracefully (e.g., verification returns "pending manual review"; notifications fall back to a working channel).
| # |
Item |
Status |
| 1 |
Exact API contract per government registry — most authorities publish only informal docs; formal specs are negotiated per MoU. |
TBD per MoU. |
| 2 |
Whether NADRA Verisys supports biometric tokens over the API or only via dedicated devices. |
TBD with NADRA. |
| 3 |
Choice of primary SMS aggregator (Jazz vs Telenor) and whether both are contracted for failover. |
TBD (procurement). |
| 4 |
Whether e-Office integration fetches documents via presigned URL or requires base64 push (adapter supports both — finalize per NITB capability). |
TBD with NITB. |
| 5 |
Government IdP for staff federation (existing departmental AD, or a new Sindh-wide IdP). |
TBD. |
| 6 |
Public-API rate-limit ceilings per partner tier (default vs premium). |
TBD (commercial). |
| 7 |
Retention period for int_call audit rows per data class. |
TBD with data-classification policy (see /specs/en/11-security-compliance/). |
End of document.