Policy callback API is provided by the partner and is the way Qoala shares the latest status of the transaction. Through this callback API, Qoala will share policy information based on each transaction, such as policy_number, policy_status, and a link to the e-certificate. Qoala's system supports several methods of authorization: API Key, HMAC_SHA256, MD5, Bearer (JWT or static token), and Basic Auth. Qoala can support a new method of authorization if needed.
Callback URL
Each partner can configure a callback URL for policy creation webhook.
POST {partner-url-path}
partner-url-path = URL from partner to receive webhook notificationThe configuration can be done through at the time of onboarding.
Authentication
Qoala authenticates every policy status notification using credentials and header names agreed during onboarding.
The examples below show the methods currently supported. Header names, message format, and encoding are partner-specific and will be confirmed during onboarding.
API Key
The shared API key is sent in a request header. Partner can verify the request by matching this value with the key exchanged during onboarding.
x-api-key: 6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39
Content-Type: application/json
HMAC_SHA256
Qoala signs the request with HMAC-SHA256 using a shared secret. A typical configuration generates a timestamp, serializes the mapped JSON body, then signs:
message = {apikey}|{timestamp}|{raw_json_body}
signature = Base64(HMAC-SHA256(secret, message))
The signature is computed after the request body is built, so it covers the exact JSON payload that is sent.
Example mapping
| Placeholder | Resolved value |
|---|---|
apikey | INS_QL_12345678 (sent as X-API-Key) |
secret | Shared HMAC secret (used only to sign, never sent) |
timestamp | Current UTC time in ISO 8601, for example 2026-07-16T10:00:00Z |
body_string | Raw JSON body after field mapping |
signature | Base64 HMAC-SHA256 digest |
Body fields in this example are mapped as follows: status is sent as actual_status. Other fields keep the same name (policy_number, partner_transaction_number, quotation_number, product_code, start_protection_at, end_protection_at).
Example request
POST /api/notify/your/endpoint/callback
Content-Type: application/json
X-API-Key: INS_QL_12345678
X-Timestamp: 2026-07-16T10:00:00Z
X-Signature: YNlgRaQK5bh6aRpcMmPq33xBhq0uREkhQmE1yKDLF8I=
{
"actual_status": "POLICY_ACTIVE",
"end_protection_at": "2028-07-15T16:59:59+07:00",
"partner_transaction_number": "TX-20260716-001",
"policy_number": "G-ADLD-PARTNER-003-20260716-INQ4M",
"product_code": "G-ADLD-PARTNER-003",
"quotation_number": "QS-GADGET-20260716-ABC12",
"start_protection_at": "2026-07-15T17:00:00+07:00"
}The sample X-Signature above is produced with secret partner_hmac_secret over this concatenated message:
INS_QL_00000000|2026-07-16T10:00:00Z|{"actual_status":"POLICY_ACTIVE","end_protection_at":"2028-07-15T16:59:59+07:00","partner_transaction_number":"TX-20260716-001","policy_number":"G-ADLD-PARTNER-003-20260716-INQ4M","product_code":"G-ADLD-PARTNER-003","quotation_number":"QS-GADGET-20260716-ABC12","start_protection_at":"2026-07-15T17:00:00+07:00"}
How to verify
- Read
X-API-Key,X-Timestamp, and the raw request body (do not re-serialize the JSON). - Build
message = X-API-Key + "|" + X-Timestamp + "|" + raw_body. - Compute
Base64(HMAC-SHA256(shared_secret, message)). - Compare the result with
X-Signatureusing a constant-time comparison.
MD5
Qoala can send an MD5 hex digest of a concatenated message. The digest may be placed in a header or in a body field (commonly Sign), depending on the partner contract. The digest is always a 32-character lowercase hex string.
signature = hex(MD5(format(params...)))
Example
If the agreed message is {secret}{start_date}{partner_transaction_number}:
secret = partner_md5_secret
message = partner_md5_secret2026-07-16TX-20260716-001
Sign = f6656a796f452b4630ce9362ccafa59c
{
"partner_transaction_number": "TX-20260716-001",
"start_protection_at": "2026-07-16",
"status": "POLICY_ACTIVE",
"Sign": "f6656a796f452b4630ce9362ccafa59c"
}Verify by rebuilding the same concatenated string from the received body (and shared secret) and comparing the MD5 hex digest.
Bearer
Qoala sends an Authorization: Bearer <token> header. The token can be:
- A static token exchanged during onboarding
- A JWT signed by Qoala (
HS256,HS384, orHS512) - A token fetched from the partner's auth endpoint (OAuth / REST) immediately before the callback
Static token
Authorization: Bearer partner_static_token
Content-Type: application/json
JWT
Qoala signs a JWT with the shared secret. Default claims:
| Claim | Value |
|---|---|
iss | QOALA |
exp | Token expiry (default 5 minutes from issuance, configurable) |
Additional custom claims can be included when required.
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
Verify the JWT using the shared secret, signing method, and expected issuer QOALA.
Token from partner auth endpoint
If the partner requires Qoala to obtain a token first, Qoala calls the partner auth URL, reads the token from the response (for example access_token), prefixes it with Bearer , and sends it on the policy callback:
Authorization: Bearer {access_token_from_auth_response}
Content-Type: application/json
Basic Auth
Qoala sends HTTP Basic Authentication. Username and password are concatenated, Base64-encoded, and prefixed with Basic .
credential = Base64("USERNAME:{password}")
Authorization: Basic {credential}
Example
username:password = qoala:partner_password
Authorization: Basic cW9hbGE6cGFydG5lcl9wYXNzd29yZA==
Content-Type: application/json
Webhook Request
Each time a status is updated then a request with the following details will be made against the specified Callback URL [partner-url-path]
Headers
The following are the details of the request headers of the webhook. Authentication headers depend on the method configured during onboarding. See Authentication.
| Header | Expected Value |
|---|---|
Content-Type | application/json |
x-api-key / X-API-Key | API key, when API Key or HMAC_SHA256 is used |
X-Signature | HMAC-SHA256 signature, when HMAC_SHA256 is used |
X-Timestamp | Request timestamp, when HMAC_SHA256 is used |
Authorization | Bearer <token> or Basic <credential>, when Bearer or Basic Auth is used |
Body
The following are the details of the request body of the webhook.
| Object Fields | Type | Required | Description |
|---|---|---|---|
policy_number | string | Yes | Policy number generated by Qoala |
partner_transaction_number | string | No | The Transaction ID serves as a unique identifier for each transaction initiated through the API. Partners are responsible for generating this ID and ensuring its uniqueness across their transactions. |
transaction_number | string | No | Transaction number generated by Qoala or the partner, depending on the integration flow |
quotation_number | string | No | Unique quotation number generated by Qoala system |
product_code | string | No | Product code defined by Qoala |
insurance_number | string | No | Insurance policy number assigned by the insurer, if available |
status | string | Yes | Last policy status. Enum: POLICY_ACTIVE, POLICY_ISSUING, POLICY_CANCELLED |
start_protection_at | string | No | Start date and time of the policy protection period (ISO 8601) |
end_protection_at | string | No | End date and time of the policy protection period (ISO 8601) |
purchased_at | string | No | Timestamp when the policy was purchased (ISO 8601) |
activated_at | string | No | Timestamp when the policy was activated (ISO 8601) |
currency_code | string | No | Currency code of the policy |
payer_type | string | No | Entity that pays for the policy |
calculation | object | No | Premium and sum insured calculation. See calculation table below |
policy_holder | object | No | Policy holder information. See policy_holder table below |
insureds | array | No | List of insured parties. Structure differs by type — see insureds tables below |
product | object | No | Product details. See product table below |
documents | array | No | Policy documents such as e-certificate and policy wording. See documents table below |
policies | array | No | Policy list used in simplified async flows (non-Gadget). Each item contains number, status, start_protection_at, end_protection_at, premium_amount, and documents |
calculation
| Field | Type | Required | Description |
|---|---|---|---|
gwp | number | No | Gross written premium |
total_sum_insured | number | No | Total sum insured amount |
policy_holder
| Field | Type | Required | Description |
|---|---|---|---|
full_name | string | No | Full name of the policy holder |
email | string | No | Email address |
phone_number | string | No | Phone number in international format |
insureds
Each item in the insureds array represents an insured party. The detail object structure depends on type.
| Field | Type | Required | Description |
|---|---|---|---|
uuid | string | No | Unique identifier for the insured record |
type | string | No | Insured type. See supported values below |
detail | object | No | Insured details. Structure varies by type — see tables below |
documents | array | No | Supporting documents for the insured |
Supported type values: USER, AGENT, GADGET, TRAVEL, FLIGHT, VEHICLE, GOODS, PET, LOAN, SHIPPING, PACKAGE, EXPERIENCE, HOSPITALITY, USER_LENDER, USER_BORROWER, BENEFICIARY, OWNER
insureds[].detail (type = USER / USER_LENDER / USER_BORROWER / BENEFICIARY / OWNER)
| Field | Type |
|---|---|
identity_type | string |
identity_number | string |
full_name | string |
email | string |
phone_number | string |
dial_code | string |
birth_date | string |
gender | string |
relationship | string |
address | string |
occupation | string |
city | string |
country | string |
place_of_birth | string |
age | number |
sum_insured | number |
additional_info | object |
insureds[].detail (type = AGENT)
| Field | Type |
|---|---|
identity_type | string |
identity_number | string |
full_name | string |
email | string |
phone_number | string |
store_location | string |
store_name | string |
insureds[].detail (type = GADGET)
| Field | Type |
|---|---|
identity_type | string |
identity_number | string |
sum_insured | number |
manufacturer | string |
brand | string |
series | string |
model | string |
device_category | string |
price | number |
covered_type_unit | string |
covered_type_max | number |
activation_number | string |
external_url | string |
upload_activation_at | string |
insureds[].detail (type = VEHICLE)
| Field | Type |
|---|---|
plate_code | string |
plate_number | string |
category | string |
manufacturer | string |
brand | string |
series | string |
year | number |
price | number |
accessories_price | number |
accessories_note | string |
usage_type | string |
condition | string |
protection | string |
chassis_number | string |
engine_number | string |
registration_number | string |
insureds[].detail (type = TRAVEL / FLIGHT)
| Field | Type |
|---|---|
identity_type | string |
identity_number | string |
booking_time | string |
name | string |
number | string |
departure | string |
departure_time | string |
departure_time_offset | number |
arrival | string |
arrival_time | string |
arrival_time_offset | number |
class | string |
sub_class | string |
city_origin | string |
city_destination | string |
country_origin | string |
country_destination | string |
timezone_origin | string |
timezone_destination | string |
total_price_ticket | number |
sub_total_price_ticket | number |
trip_type | string |
trip_order | number |
number_of_room | number |
total_covered | number |
additional_info | object |
insureds[].detail (type = GOODS)
| Field | Type |
|---|---|
identity_type | string |
identity_number | string |
sum_insured | number |
category | string |
brand | string |
model | string |
price | number |
address | string |
additional_info | object |
insureds[].detail (type = PET)
| Field | Type |
|---|---|
identity_type | string |
identity_number | string |
full_name | string |
birth_date | string |
gender | string |
breed | string |
pure_breed | boolean |
color | string |
category | string |
usage_type | string |
vaccination | array |
condition | string |
pre_existing_condition | string |
number_of_pets_in_household | number |
number_of_pets_with_same_insurance | number |
insureds[].detail (type = LOAN)
| Field | Type |
|---|---|
identity_type | string |
identity_number | string |
loan_amount | number |
sum_insured | number |
loan_total_principal | number |
loan_total_principal_percentage | number |
tenure_value | number |
tenure_period | string |
interest_rate | number |
interest_period | string |
additional_info | object |
insureds[].detail (type = SENDER / RECIPIENT)
| Field | Type |
|---|---|
identity_type | string |
identity_number | string |
full_name | string |
email | string |
phone_number | string |
birth_date | string |
gender | string |
address | string |
occupation | string |
additional_data | object |
additional_data fields: city, country, zipcode, subject
insureds[].detail (type = SHIPPING)
| Field | Type |
|---|---|
carrier | string |
identity_type | string |
identity_number | string |
service | string |
total_package_price | number |
cost | number |
estimated_delivery_time | string |
input_package_at | string |
estimated_delivery_time_offset | number |
input_package_at_offset | number |
insureds[].detail (type = PACKAGE)
| Field | Type |
|---|---|
name | string |
identity_type | string |
identity_number | string |
price | number |
sum_insured | number |
insureds[].documents[]
| Field | Type |
|---|---|
filename | string |
type | string |
url | string |
product
| Field | Type | Required | Description |
|---|---|---|---|
country | string | No | Product country |
parent_category | string | No | Parent product category |
product_category | string | No | Product sub-category |
product_summary | string | No | Short product summary |
code | string | No | Product code |
name | string | No | Product name |
language | string | No | Product language |
category_name | string | No | Category display name |
insurance_name | string | No | Insurance provider name |
partner_name | string | No | Partner name |
product_code | string | No | Product code (duplicate of code in some payloads) |
product_name | string | No | Product display name |
product_description | string | No | HTML product description |
product_terms | string | No | HTML product terms |
product_wording | string | No | URL to product wording document |
claim_description | string | No | Claim description text |
timezone | string | No | Product timezone |
terms | string | No | Additional terms |
riplay_document | string | No | RIPLAY document reference |
benefits | array | No | List of product benefits. See product.benefits[] table below |
product.benefits[]
| Field | Type | Description |
|---|---|---|
code | string | Benefit code |
name | string | Benefit name |
description | string | Benefit description |
term.inclusion | array | List of inclusion terms |
term.exclusion | array | List of exclusion terms |
documents
| Field | Type | Required | Description |
|---|---|---|---|
type | string | No | Document type. Examples: POLICY_CERTIFICATE, POLICY_WORDING |
filename | string | No | Document filename |
alias | string | No | Human-readable document alias |
url | string | No | URL to download the document |
Example Request Body
Purpose (simplified async flow)
{
"partner_transaction_number": "TX-20260716-001",
"transaction_number": "QS-GADGET-20260716-ABC12",
"product_code": "G-ADLD-PARTNER-003",
"policies": [
{
"number": "G-ADLD-PARTNER-003-20260716-INQ4M",
"status": "POLICY_ACTIVE",
"start_protection_at": "2026-07-15T17:00:00+07:00",
"end_protection_at": "2028-07-15T16:59:59+07:00",
"premium_amount": 3270000,
"documents": [
{
"type": "POLICY_CERTIFICATE",
"filename": "G-ADLD-PARTNER-003-20260716-INQ4M.pdf",
"alias": "policy_certificate.pdf",
"url": "https://api.uat.qoala.app/api/v2/policies/documents/policy_certificate.pdf"
},
{
"type": "POLICY_WORDING",
"filename": "Qoala-Gadget-Protection-Terms-Conditions.pdf",
"alias": "policy_wording.pdf",
"url": "https://api.uat.qoala.app/api/v2/policies/documents/policy_wording.pdf"
}
]
}
]
}Webhook Response
HTTP response status code from partner will be tracked and
- HTTP status code 200 will be acknowledged by Qoala as notification successfully received. No retry will be done.
- Other HTTP status code will be marked as failed and will be retried. Request will be retried up to 8 times with exponential backoff interval.