هوية — Huwiyya
هويةHuwiyya
Partner Portal
Partner API · v1

Partner API Reference

Complete reference for building partner integrations with the Huwiyya Identity Verification Platform. Covers authentication, verifications, webhooks, reports, and more.

Overview

Base URL

https://{your-domain}/api/partner/v1

API Version

v1

Content-Type

application/json

Auth Scheme

OAuth 2.0 mTLS (RFC 8705)

All partner API endpoints are strictly scoped to the authenticated partner's tenant. Your OAuth 2.0 access token is bound to your tenant ID and client certificate, which are enforced on every request — you can only access verification cases, webhook configurations, and resources belonging to your organisation.

All responses use camelCase JSON property names. Enum values are serialised as strings (e.g. "Approved", not integers). All timestamps are ISO 8601 with UTC offset, for example 2026-03-23T14:00:00+00:00.

Every request and response is correlated by a X-Correlation-ID header. If you supply one on your requests it will be echoed back and appear in all server-side logs and audit records, making cross-system debugging straightforward.

Authentication

All Partner API requests are authenticated using OAuth 2.0 with Mutual TLS (mTLS) as defined in RFC 8705. Partners authenticate with a registered X.509 client certificate at the TLS layer and obtain short-lived, certificate-bound access tokens via the client_credentials grant. Access tokens are scoped to the partner's tenant and bound to the certificate thumbprint (cnf.x5t#S256).

Authentication Flow

1. Register Client + Certificate2. mTLS Handshake3. POST /oauth/token4. Receive Bound Token5. Use Bearer Token + mTLS
OAuth clients are registered through the Partner Portal by users with the PartnerAdmin role. Each client must have a valid X.509 certificate. Up to 5 active OAuth clients are allowed per tenant. Access tokens expire after 1 hour (3600 seconds).

OAuth 2.0 mTLS Overview

The platform implements OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens per RFC 8705. The external TLS-terminating reverse proxy validates the client certificate and forwards the SHA-256 certificate thumbprint via the X-Client-Cert-Thumbprint header. The platform verifies that the thumbprint matches the registered OAuth client and the issued access token.

Token format

text
hwy_at_<64 lowercase hex characters>

Example: hwy_at_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2

Example authenticated API request

http
GET /api/partner/v1/verifications HTTP/1.1
Host: huwiyya.syria-cloud.sy
Authorization: Bearer hwy_at_a1b2c3d4e5f6...your-token...
Content-Type: application/json

cURL with mTLS

bash
# Obtain access token using client certificate
curl -X POST https://huwiyya.syria-cloud.sy/api/partner/v1/oauth/token \
  --cert client.pem --key client-key.pem \
  -d "grant_type=client_credentials&client_id=hwy_a1b2c3d4..."

# Use the access token with mTLS
curl -X GET https://huwiyya.syria-cloud.sy/api/partner/v1/verifications \
  --cert client.pem --key client-key.pem \
  -H "Authorization: Bearer hwy_at_..." \
  -H "Content-Type: application/json"
200Authenticated401Missing/invalid token or certificate403Client revoked or tenant disabled
Security: Access tokens are certificate-bound — they can only be used with the same client certificate that was presented during token issuance. Store client certificates and private keys securely in a hardware security module (HSM) or a secrets manager. Never embed private keys in source control, CI logs, or mobile applications.

Register OAuth Client

Register a new OAuth client by uploading the client's X.509 certificate in PEM format. The platform extracts the SHA-256 thumbprint, subject DN, and validity period. A unique client_id is generated and returned. Requires PartnerAdmin role via the Partner Portal.

POST
/api/partner/v1/oauth/clients

Register a new OAuth client with a client certificate. Returns the generated client_id.

ParameterTypeDescription
clientNamerequiredstringHuman-readable name for this OAuth client (e.g. "Production mTLS Client").
certificatePemrequiredstringX.509 client certificate in PEM format (-----BEGIN CERTIFICATE----- block).
allowedScopesstringSpace-delimited scopes. Default: "verifications webhooks evidence reports".

Response 201

json
{
  "client": {
    "id": "c1d2e3f4-a5b6-7890-c1d2-e3f4a5b67890",
    "clientId": "hwy_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
    "clientName": "Production mTLS Client",
    "certificateThumbprint": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
    "certificateSubjectDN": "CN=partner.example.com, O=Partner Corp",
    "certificateNotBefore": "2026-01-01T00:00:00+00:00",
    "certificateNotAfter": "2027-01-01T00:00:00+00:00",
    "allowedScopes": "verifications webhooks evidence reports",
    "isActive": true,
    "createdAt": "2026-03-23T14:00:00+00:00"
  },
  "clientId": "hwy_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"
}
201Client registered400Invalid certificate or expired409Duplicate cert or max 5 clients

Obtain Access Token

Obtain a certificate-bound access token using the OAuth 2.0 client_credentials grant. The request must be made over mTLS — the platform verifies that the client certificate thumbprint matches the registered OAuth client. The token endpoint uses application/x-www-form-urlencoded content type as required by RFC 6749.

POST
/api/partner/v1/oauth/token

Issue a certificate-bound access token. Requires mTLS with a registered client certificate.

ParameterTypeDescription
grant_typerequiredstringMust be "client_credentials".
client_idrequiredstringThe client_id returned during registration (hwy_... format).
scopestringSpace-delimited scopes. Defaults to client's allowed scopes if omitted.

cURL example

bash
curl -X POST https://huwiyya.syria-cloud.sy/api/partner/v1/oauth/token \
  --cert client.pem --key client-key.pem \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&client_id=hwy_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"

Response 200

json
{
  "access_token": "hwy_at_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "verifications webhooks evidence reports",
  "cnf": {
    "x5t#S256": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
  }
}
The cnf claim in the response confirms the certificate binding per RFC 8705 §3.1. The x5t#S256 value is the SHA-256 thumbprint of the bound client certificate. Subsequent API requests must present the same certificate.
200Token issued400Invalid grant type401Invalid client or certificate mismatch

Use Access Token

After obtaining an access token, include it in the Authorization header as a Bearer token on all Partner API requests. The mTLS connection must still present the same client certificate — the platform validates that the certificate thumbprint matches the token's bound thumbprint.

Request headers

ParameterTypeDescription
AuthorizationrequiredstringBearer hwy_at_... — the access token from the token endpoint.
X-Client-Cert-ThumbprintrequiredstringSHA-256 thumbprint of the client certificate (forwarded by TLS proxy).
Content-Typestringapplication/json for most endpoints.
Idempotency-KeystringUnique key for safe retries on create operations.

Example request

bash
curl -X GET https://huwiyya.syria-cloud.sy/api/partner/v1/verifications \
  --cert client.pem --key client-key.pem \
  -H "Authorization: Bearer hwy_at_a1b2c3d4e5f6..." \
  -H "Content-Type: application/json"

Introspect Token

Inspect the metadata and validity of an access token per RFC 7662. Requires mTLS with a registered client certificate.

POST
/api/partner/v1/oauth/introspect

Introspect an access token. Returns metadata including active status and certificate binding.

ParameterTypeDescription
tokenrequiredstringThe access token to introspect.

Response 200 (active)

json
{
  "active": true,
  "client_id": "hwy_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "scope": "verifications webhooks evidence reports",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "exp": 1711209600,
  "iat": 1711206000,
  "token_type": "Bearer",
  "cnf": {
    "x5t#S256": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
  }
}

Response 200 (inactive)

json
{
  "active": false
}

Revoke Token

Revoke an access token per RFC 7009. Requires mTLS with a registered client certificate. The endpoint always returns 200 OK regardless of whether the token existed.

POST
/api/partner/v1/oauth/revoke

Revoke an access token. Always returns 200 OK per RFC 7009.

ParameterTypeDescription
tokenrequiredstringThe access token to revoke.
200Token revoked (or not found)401Missing client certificate

List OAuth Clients

List all registered OAuth clients for your tenant. Returns metadata including certificate thumbprints and activity status. Requires Partner Portal session or a valid OAuth access token.

GET
/api/partner/v1/oauth/clients

List all OAuth clients for your tenant. Metadata only.

Response 200

json
[
  {
    "id": "c1d2e3f4-a5b6-7890-c1d2-e3f4a5b67890",
    "clientId": "hwy_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
    "clientName": "Production mTLS Client",
    "certificateThumbprint": "b94d27b993...efcde9",
    "certificateSubjectDN": "CN=partner.example.com",
    "certificateNotAfter": "2027-01-01T00:00:00+00:00",
    "isActive": true,
    "createdAt": "2026-01-10T09:00:00+00:00",
    "lastUsedAt": "2026-03-23T13:45:00+00:00",
    "revokedAt": null
  }
]

Revoke OAuth Client

Permanently revoke an OAuth client. All active access tokens issued to this client are immediately revoked. This action is irreversible. Requires PartnerAdmin role via the Partner Portal.

DELETE
/api/partner/v1/oauth/clients/{clientEntityId}

Permanently revoke an OAuth client and all its tokens. Cannot be undone.

204Client revoked404Client not found400Already revoked

Verifications

Every identity check is a Verification Case — a durable record that advances through a defined state machine from Requested to a terminal state.

Verification State Machine

RequestedSessionIssuedCaptureStartedEvidenceUploadedAutomatedChecksRunningPendingManualReviewApprovedRejectedExpiredCancelledFailed

Create Verification

Creates a new verification case and issues a one-time session token for the end user to begin the capture flow. Fully idempotent when the Idempotency-Key header is supplied.

POST
/api/partner/v1/verifications

Create a new verification case. Returns the case ID and a one-time session token for the end user.

Request headers

ParameterTypeDescription
AuthorizationrequiredstringBearer hwy_at_... — your OAuth 2.0 access token
Idempotency-KeystringUnique key for safe retries. Auto-generated if omitted. UUID or any opaque string (max 128 chars).
X-Correlation-IDstringTrace ID propagated through all logs and audit records. Recommended for production integrations.

Request body

ParameterTypeDescription
documentTyperequiredenumSyrianNationalId | SyrianPassport — the type of document the user will present.
externalReferencestringYour internal identifier for this user (max 200 chars). Stored for correlation, never shown to the end user.
json
{
  "documentType": "SyrianNationalId",
  "externalReference": "user-abc-123"
}

Response 201

json
{
  "caseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "sessionToken": "stok_Kj8mNqP3rL9vWxYzA1bCdE2fGhIjKl",
  "status": "SessionIssued",
  "expiresAt": "2026-03-23T16:00:00+00:00"
}
One-time token: The sessionToken is returned exactly once and stored only as a hash server-side. Deliver it to the end user immediately via SMS, deep link, or QR code. It expires in 2 hours.
201Case created409CASE_EXISTS — idempotency key already used400Validation error401Unauthorized

Get Verification Details

Returns the full detail of a verification case including all AI check results (OCR, face match, liveness), evidence metadata (SHA-256 hash, size, type), fraud signals, and reviewer decisions. Tenant-scoped: only returns cases belonging to your organisation.

GET
/api/partner/v1/verifications/{caseId}

Get full verification case detail including AI results, evidence metadata, fraud signals, and review decisions.

Response 200

json
{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "externalReference": "user-abc-123",
  "status": "Approved",
  "documentType": "SyrianNationalId",
  "compositeRiskScore": 0.12,
  "decisionReason": "All automated checks passed.",
  "createdAt":   "2026-03-23T14:00:00+00:00",
  "updatedAt":   "2026-03-23T14:08:31+00:00",
  "completedAt": "2026-03-23T14:08:31+00:00",
  "session": {
    "id": "a2b3c4d5-e6f7-...",
    "expiresAt": "2026-03-23T16:00:00+00:00",
    "isUsed": true,
    "usedAt": "2026-03-23T14:02:10+00:00"
  },
  "documentEvidence": [
    {
      "id": "e1f2a3b4-...",
      "evidenceType": "DocumentFront",
      "contentType": "image/jpeg",
      "fileSizeBytes": 524288,
      "originalFileName": "front.jpg",
      "sha256Hash": "a1b2c3d4e5f6...",
      "uploadedAt": "2026-03-23T14:03:50+00:00"
    }
  ],
  "selfieEvidence": [ "..." ],
  "videoEvidence":  [ "..." ],
  "ocrResult": {
    "modelName": "huwiyya-ocr",
    "modelVersion": "1.0.0",
    "confidenceScore": 0.96,
    "extractedFieldsJson": "{"fullName":"Ahmad Al-Souri","idNumber":"12345678"}",
    "mrzIsValid": null,
    "mrzRawText": null,
    "hasManipulationIndicators": false,
    "inferenceTimestamp": "2026-03-23T14:06:10+00:00",
    "inferenceLatencyMs": 834
  },
  "faceMatch": {
    "modelName": "huwiyya-facematch",
    "modelVersion": "1.0.0",
    "similarityScore": 0.94,
    "threshold": 0.85,
    "isMatch": true,
    "inferenceTimestamp": "2026-03-23T14:06:11+00:00",
    "inferenceLatencyMs": 421
  },
  "liveness": {
    "modelName": "huwiyya-liveness",
    "modelVersion": "1.0.0",
    "livenessScore": 0.98,
    "threshold": 0.75,
    "isLive": true,
    "presentationAttackDetected": false,
    "attackType": null,
    "inferenceTimestamp": "2026-03-23T14:06:13+00:00",
    "inferenceLatencyMs": 612
  },
  "fraudSignals": [],
  "reviewDecisions": []
}
200OK404Case not found

List Verifications

GET
/api/partner/v1/verifications

Paginated list of verification cases for your tenant. Filter by status.

Query parameters

ParameterTypeDescription
statusenumFilter by status. Values: Requested · SessionIssued · CaptureStarted · EvidenceUploaded · AutomatedChecksRunning · PendingManualReview · Approved · Rejected · Expired · Cancelled · Failed
pageintegerPage number, 1-based (default: 1).
pageSizeintegerItems per page, max 100 (default: 50).

Response 200

json
{
  "items": [
    {
      "id": "3fa85f64-...",
      "externalReference": "user-abc-123",
      "status": "Approved",
      "documentType": "SyrianNationalId",
      "compositeRiskScore": 0.12,
      "createdAt":   "2026-03-23T14:00:00+00:00",
      "completedAt": "2026-03-23T14:08:31+00:00"
    }
  ],
  "page": 1,
  "pageSize": 50,
  "totalCount": 147,
  "totalPages": 3
}

Cancel Verification

Cancels an active case. Only non-terminal states can be cancelled: Requested, SessionIssued, CaptureStarted, and PendingManualReview. The reason is stored in the audit trail.

POST
/api/partner/v1/verifications/{caseId}/cancel

Cancel a verification case. Provide a human-readable reason for audit purposes.

json
{
  "reason": "User withdrew consent."
}
204Cancelled400Already in terminal state404Case not found

Update OCR Fields

Correct or amend AI-extracted OCR fields on a verification case. This is useful when automated extraction produced inaccurate results and the partner needs to supply corrected values. Every update creates a full audit trail entry with before/after comparison. Only non-terminal cases or cases in PendingManualReview can be updated.

PATCH
/api/partner/v1/verifications/{caseId}/ocr-fields

Update AI-extracted OCR fields. Creates an audit trail with before/after comparison.

ParameterTypeDescription
fieldsrequiredobjectKey-value map of field names to corrected values. Keys must match existing extracted field names.
notesstringOptional free-text note explaining the reason for the correction. Stored in audit trail.
json
{
  "fields": {
    "fullName": "أحمد السوري",
    "idNumber": "12345678"
  },
  "notes": "Corrected name transliteration from OCR output."
}

Response 200

json
{
  "modelName": "huwiyya-ocr",
  "modelVersion": "1.0.0",
  "confidenceScore": 0.96,
  "extractedFieldsJson": "{\"fullName\":\"أحمد السوري\",\"idNumber\":\"12345678\"}",
  "mrzIsValid": null,
  "mrzRawText": null,
  "hasManipulationIndicators": false,
  "inferenceTimestamp": "2026-03-23T14:06:10+00:00",
  "inferenceLatencyMs": 834
}
200Fields updated400Validation error404Case or OCR result not found

Evidence Access

Download evidence files (document images, selfie photos, liveness video) associated with verification cases belonging to your tenant. Files are streamed directly from secure object storage with proper content-type headers. Access is tenant-scoped and logged in the audit trail.

Download Evidence File

Stream an evidence file by its ID. The response content-type matches the original upload (e.g. image/jpeg, video/mp4). Evidence IDs are returned in the verification detail response.

GET
/api/partner/v1/evidence/{evidenceId}/file

Stream an evidence file from object storage. Returns the file with its original content-type.

Evidence file access requires the evidenceId from the verification detail response. The file is streamed directly — no JSON envelope. Use the Content-Type response header to determine the file type.
200File stream404Evidence not found403Tenant mismatch

Webhook Endpoints

Configure outbound webhook endpoints to receive real-time event notifications when verification cases change state. Each endpoint has an HMAC-SHA256 signing secret so you can verify payload authenticity before processing.

List Endpoints

GET
/api/partner/v1/webhooks

List all webhook endpoints configured for your tenant.

json
[
  {
    "id": "b1c2d3e4-5678-4abc-d9ef-012345678901",
    "url": "https://yourbank.com/events/huwiyya",
    "eventTypes": ["verification.approved", "verification.rejected"],
    "isActive": true,
    "createdAt": "2026-01-15T10:00:00+00:00"
  }
]

Get Endpoint

GET
/api/partner/v1/webhooks/{id}

Get a single webhook endpoint by ID.

Create Endpoint

Creates a webhook endpoint and returns the HMAC signing secret. The plain-text secret is returned exactly once — store it in your secret manager immediately. It cannot be retrieved again; use rotate-secret if you lose it.

POST
/api/partner/v1/webhooks

Create a new webhook endpoint. The signing secret is returned once in the response.

Request body

ParameterTypeDescription
urlrequiredstringHTTPS endpoint URL. Must be publicly reachable. Max 500 chars.
eventTypesrequiredstring[]Array of event type strings to subscribe to. See Event Types reference.
json
{
  "url": "https://yourbank.com/events/huwiyya",
  "eventTypes": [
    "verification.approved",
    "verification.rejected",
    "verification.failed",
    "verification.pending_manual_review"
  ]
}

Response 201

json
{
  "endpoint": {
    "id": "b1c2d3e4-5678-4abc-d9ef-012345678901",
    "url": "https://yourbank.com/events/huwiyya",
    "eventTypes": ["verification.approved", "verification.rejected"],
    "isActive": true,
    "createdAt": "2026-03-23T14:00:00+00:00"
  },
  "signingSecret": "whsec_Kj8mNqP3rL9vWxYzA1bCdE2fGhIjKlMnOpQrStUvWxYz"
}
201Created400Invalid URL or event types

Update Endpoint

PUT
/api/partner/v1/webhooks/{id}

Update the target URL and/or subscribed event types for a webhook endpoint.

json
{
  "url": "https://yourbank.com/events/huwiyya-v2",
  "eventTypes": ["verification.approved", "verification.failed"]
}

Activate / Deactivate

POST
/api/partner/v1/webhooks/{id}/deactivate

Pause delivery to this endpoint without deleting it.

POST
/api/partner/v1/webhooks/{id}/activate

Resume delivery to a previously deactivated endpoint.

204Success404Endpoint not found

Rotate Signing Secret

Generates a new HMAC signing secret for the endpoint. The new plain-text secret is returned once. Rotate secrets periodically or immediately after a potential compromise. Update your receiver code with the new secret before rotating to avoid verification failures during the transition.

POST
/api/partner/v1/webhooks/{id}/rotate-secret

Generate a new HMAC signing secret. The previous secret is invalidated immediately.

json
{
  "newSigningSecret": "whsec_NewRotatedSecretReturnedOnce..."
}

Delivery History

GET
/api/partner/v1/webhooks/{id}/deliveries

Paginated delivery history for a webhook endpoint, including HTTP status codes and retry counts.

Query parameters

ParameterTypeDescription
pageintegerPage number (default: 1).
pageSizeintegerItems per page (default: 20).

Response 200

json
{
  "deliveries": [
    {
      "id": "d1e2f3a4-...",
      "eventType": "verification.approved",
      "caseId": "3fa85f64-...",
      "httpStatus": 200,
      "attemptCount": 1,
      "lastAttemptAt": "2026-03-23T14:09:00+00:00",
      "succeeded": true
    }
  ],
  "page": 1,
  "pageSize": 20,
  "totalCount": 1,
  "totalPages": 1
}

Reports

Summary Statistics

Returns verification case counts grouped by status for a specified date range. Useful for dashboard KPIs, SLA tracking, and monitoring automation rates.

GET
/api/partner/v1/reports/summary

Verification case counts grouped by status for a date range, plus derived automation rate.

Query parameters

ParameterTypeDescription
fromISO 8601Start of range (inclusive). Defaults to 30 days ago.
toISO 8601End of range (inclusive). Defaults to now.

Response 200

json
{
  "from": "2026-02-23T00:00:00+00:00",
  "to":   "2026-03-23T23:59:59+00:00",
  "totalCases": 847,
  "byStatus": {
    "Approved":           612,
    "Rejected":            91,
    "PendingManualReview": 23,
    "Failed":               8,
    "Expired":            113
  },
  "automationRate": 0.93
}

Export CSV

GET
/api/partner/v1/reports/export

Export all verification cases for a date range as a UTF-8 CSV file. Maximum 5,000 rows per export.

ParameterTypeDescription
fromISO 8601Start of range.
toISO 8601End of range.

The response has Content-Type: text/csv and a filename of the form verifications_YYYYMMDD.csv.

CSV columns

csv
CaseId,Status,DocumentType,ExternalReference,CreatedAt,CompletedAt,CompositeRiskScore,DecisionReason

Webhook Events

Event Types

Subscribe to one or more event types when creating a webhook endpoint. Only subscribed event types are delivered. We recommend subscribing to the states most relevant to your workflow rather than all events.

Event TypeWhen fired
verification.session_issuedCase created and session token issued to partner
verification.capture_startedEnd user opened the session in the mobile app
verification.evidence_uploadedAll required evidence uploaded; AI checks enqueued
verification.automated_checks_runningAI pipeline started processing the case
verification.pending_manual_reviewAutomated checks flagged the case; awaiting human review
verification.approvedCase approved (automated or after manual review)
verification.rejectedCase rejected (automated or after manual review)
verification.expiredSession or case expired before completion
verification.cancelledCase cancelled by your system or an administrator
verification.failedUnrecoverable system error during AI processing

Payload Structure

Each delivery is a POST request to your endpoint URL with a JSON body containing the event type, a unique event ID, a timestamp, and the verification case snapshot. Your endpoint must return any 2xx status code within 30 seconds to acknowledge receipt. Failed deliveries are retried with exponential backoff up to 5 attempts.

json
{
  "eventType":  "verification.approved",
  "eventId":    "evt_3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "timestamp":  "2026-03-23T14:09:00+00:00",
  "apiVersion": "v1",
  "tenantId":   "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "data": {
    "caseId":            "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "externalReference": "user-abc-123",
    "status":            "Approved",
    "documentType":      "SyrianNationalId",
    "compositeRiskScore": 0.12,
    "decisionReason":    "All automated checks passed.",
    "completedAt":       "2026-03-23T14:08:31+00:00"
  }
}

Signature Verification

Every delivery includes HTTP headers for verifying payload authenticity. Always verify the signature before processing any webhook payload.

HeaderValue
X-Huwiyya-Signaturesha256=<hex> — HMAC-SHA256 of the signed payload using your endpoint's secret
X-Huwiyya-TimestampUnix timestamp (seconds). Reject events outside ±5 minutes to prevent replay attacks.
X-Huwiyya-EventThe event type string (e.g. verification.approved)

Signed payload: The HMAC is computed over {timestamp}.{rawBodyBytes} where timestamp is the value of X-Huwiyya-Timestamp.

Node.js / Express example

javascript
const crypto = require('crypto');

function verifyHuwiyyaSignature(req, signingSecret) {
  const signature = req.headers['x-huwiyya-signature']; // "sha256=abc123..."
  const timestamp  = req.headers['x-huwiyya-timestamp']; // Unix epoch seconds

  if (!signature || !timestamp) return false;

  // Reject replays older than 5 minutes
  const ageSeconds = Math.abs(Date.now() / 1000 - parseInt(timestamp, 10));
  if (ageSeconds > 300) return false;

  // req.rawBody must be the raw Buffer — do NOT parse before verifying
  const computed = 'sha256=' + crypto
    .createHmac('sha256', signingSecret)
    .update(timestamp + '.' + req.rawBody)
    .digest('hex');

  // Constant-time comparison prevents timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(computed, 'utf8'),
    Buffer.from(signature, 'utf8'),
  );
}

// Express route — use express.raw() to preserve the raw body
app.post(
  '/events/huwiyya',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    if (!verifyHuwiyyaSignature(req, process.env.HUWIYYA_WEBHOOK_SECRET)) {
      return res.status(401).send('Invalid signature');
    }
    const event = JSON.parse(req.body);
    // Handle event.eventType, event.data ...
    res.sendStatus(200);
  },
);

Python example

python
import hashlib, hmac, time

def verify_huwiyya_signature(
    headers: dict,
    raw_body: bytes,
    signing_secret: str,
) -> bool:
    signature = headers.get('X-Huwiyya-Signature', '')  # "sha256=abc123..."
    timestamp  = headers.get('X-Huwiyya-Timestamp', '')

    if not signature or not timestamp:
        return False

    # Reject replays older than 5 minutes
    if abs(time.time() - int(timestamp)) > 300:
        return False

    payload = timestamp.encode() + b'.' + raw_body
    expected = 'sha256=' + hmac.new(
        signing_secret.encode(),
        payload,
        hashlib.sha256,
    ).hexdigest()

    # Constant-time comparison to prevent timing attacks
    return hmac.compare_digest(expected, signature)

Error Codes

All error responses follow the RFC 7807 Problem Details format with an additional code field for programmatic handling.

json
{
  "type":   "https://tools.ietf.org/html/rfc7807",
  "title":  "Conflict",
  "status": 409,
  "detail": "A verification case with this idempotency key already exists.",
  "code":   "CASE_EXISTS"
}
HTTPError CodeDescription
400VALIDATION_ERRORRequest body or query parameter validation failed
401UNAUTHENTICATEDMissing, expired, or invalid authorization token
403FORBIDDENAuthenticated user lacks the required role or tenant scope
403ACCOUNT_DISABLEDPartner account disabled by an administrator
404NOT_FOUNDResource does not exist or belongs to a different tenant
409CASE_EXISTSIdempotency key already used — original case ID in response detail
429RATE_LIMITEDExceeded 120 requests/minute. Check Retry-After header.
429ACCOUNT_LOCKEDAccount temporarily locked after repeated failed logins
500INTERNAL_ERRORUnhandled server error. Contact the platform operator.

Rate Limiting

The API enforces a limit of 120 requests per IP address per minute using a fixed-window counter. When exceeded, the API returns HTTP 429 with a Retry-After header indicating the seconds until the window resets.

http
HTTP/1.1 429 Too Many Requests
Retry-After: 42
Content-Type: application/problem+json

{
  "title":  "Too Many Requests",
  "status": 429,
  "detail": "Rate limit exceeded. Retry after the indicated interval.",
  "code":   "RATE_LIMITED"
}

Machine-to-machine integrations should implement exponential backoff when receiving 429 responses. The most common cause of rate-limit hits is polling the list or status endpoints too aggressively — prefer webhooks for real-time state change notifications.

Idempotency

All POST endpoints that create resources accept an Idempotency-Key request header. Supplying the same key on a retry returns the original successful response without creating a duplicate resource.

http
POST /api/partner/v1/verifications HTTP/1.1
Authorization: Bearer hwy_at_a1b2c3d4e5f6...your-token...
Idempotency-Key: 7f9c8a1b-3d2e-4f5a-b6c7-d8e9f0a1b2c3
Content-Type: application/json

{
  "documentType": "SyrianNationalId",
  "externalReference": "user-abc-123"
}
  • The same key on a successful retry returns the original 201 response — no duplicate case created.
  • If a case already exists with that key, the API returns 409 CASE_EXISTS with the original case ID in the response detail.
  • Keys should be unique UUIDs generated per real user action, not per HTTP retry attempt.
  • If omitted, a random key is auto-generated and no idempotency protection is applied for retries.

Ready to integrate?

Contact us to get your partner credentials and start verifying identities at banking grade.