API Reference
Automate UAE motor insurance across three carriers — DNI, Insurance House, and QIC behind one integration. Submit a customer intake, poll for quotes, send a payment link when the broker approves, then retrieve the issued policy. Every endpoint is the same for every carrier — you choose per request with targetInsurers.
POST /api/quotesSubmit the customer intake + documents. You get back a jobId.
GET /api/quotes/:jobIdPoll until settled, then read the premium on each result.
POST /api/quotes/:jobId/sellAfter the broker approves, send the payment link.
GET /api/quotes/:jobId/policyRetrieve the issued policy + PDFs (auto-polled after payment).
Authentication
Every endpoint except /health requires an API key, passed in the x-api-key header. Keep it server-side — it can read customer data and send real payment links.
Carriers
One API, three carriers. Choose per request with targetInsurers — e.g. ["dni"], ["insurancehouse"], ["qic"], or several at once (they run in parallel and return the same response shapes). Everything else in this reference is identical for every carrier — this table is the practical difference.
| DNI | Insurance House | QIC | |
|---|---|---|---|
| targetInsurers value | "dni" | "insurancehouse" | "qic" |
| Products | One per scheme: 65S Comprehensive, 66S TPL, 60A-S1 Comprehensive High Value | Comprehensive + TPL, priced together from one submit | One per scheme: 65S Comprehensive, 66S TPL |
| coverage.schemes to send | Choose ["65S"] and/or ["66S"]. Use ["60A-S1"] for a vehicle DNI values above AED 250,001 (quote-only for now — not sellable yet) | Choose ["65S"] and/or ["66S"] — send both to also get the other product’s price for comparison | Choose ["65S"] and/or ["66S"] |
| Result rows (results[]) | One per scheme you requested | One per requested scheme (2 when you request both) | One per scheme you requested |
| Typical quote time | ~20–30 s | ~30–70 s | ~10–30 s |
| Policy retrieval | Paste the CRS policy number (crsPolNo) | Automatic — no number needed (auto-derived) | Automatic — no number needed (auto-derived) |
| Policy PDFs returned | Schedule + Jacket | Schedule + Tax Invoice | Schedule + Receipt |
| Pay-link recipient | Broker’s central eSanad inbox — never the customer directly | Broker’s central eSanad inbox — never the customer directly | Broker’s central eSanad inbox — never the customer directly |
| Notes | Echoes quote.placeOfRegistration; 65S returns a valuation block (none on 66S/TPL). | One submit prices both products internally, but only the scheme(s) you requested are returned; Comprehensive (IH-COMP) returns a valuation block, TPL has none; no placeOfRegistration echo. | Policy auto-discovered by the saved quote number (no CRS to paste); 65S returns a valuation block, TPL (66S) has none; no placeOfRegistration echo. |
Rate limits
Requests are capped per key. Exceeding a limit returns 429 RATE_LIMITED; wait for the window to reset and retry.
| Scope | Limit | Applies to |
|---|---|---|
| Quote submission | 10 / min | POST /api/quotes |
| Sell / payment link | 5 / min | POST /api/quotes/:jobId/sell |
| Policy refresh | 10 / min | POST /api/quotes/:jobId/refresh-policy |
| Global | 100 / min | All endpoints combined |
Endpoints
The full quote → sell → policy loop. Every endpoint is identical across carriers.
/healthPublicHealth check
The only public endpoint — verify the API and its dependencies are up before you send requests.
Details
Returns the health status of the API and its core dependencies. Use this to verify the system is operational before sending requests.
status has three values: "ok" (database + workflow engine both up), "degraded" (database up but the workflow engine is down — the API still answers and reads work, but new quotes may not start; returned with HTTP 200), and "down" (database down; returned with HTTP 503). If you gate on health, treat both "ok" and "degraded" as reachable — do NOT treat "degraded" as a failure.
Status codes
Request
curl https://api.rpai.vionark.com/health
Response
{"status": "ok","service": "rpa-api","version": "0.0.0","timestamp": "2026-05-26T12:00:00.000Z","database": "up","engine": "up"}
/api/quotesAuth requiredSubmit a quote
Submit a customer intake (vehicle details + documents) and get a jobId to poll. New requests return 202; duplicates return 200 and reuse the existing job.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| Customer | |||
| customer.fullName | string | yes | Full name as on Emirates ID |
| customer.emiratesId | string | yes | Emirates ID (format: 784-YYYY-NNNNNNN-C) |
| customer.emiratesIdExpiry | string | yes | EID expiry date (YYYY-MM-DD) |
| customer.dob | string | yes | Date of birth (YYYY-MM-DD). Used for pricing — must be the real DOB. |
| customer.nationality | string | yes | ISO 3166-1 alpha-2 country code (e.g. "IN", "AE", "PH"). Used for pricing. |
| customer.gender | string | yes | "Male" or "Female" |
| customer.mobileNumber | string | yes | UAE mobile in E.164 (e.g. +971501234567). Stored on the job for your records; the carrier-facing mobile is the broker’s central number, not this one. |
| customer.email | string | yes | Customer email. Kept on the job for your records and used to validate the sell request (must match on file). The DNI quote + payment-link email go to the broker’s central inbox, not this address. |
| customer.nameInArabic | string | no | Customer name in Arabic. Accepted and forwarded to the carrier as-is when provided. Note for DNI: their motor quote record does not currently retain this field — the quote and policy are keyed on the Latin name and the Emirates ID, so do not rely on the Arabic name appearing on carrier-side documents. |
| customer.address.line1 | string | no | Customer street address. Not a rating field. Omit the whole `customer.address` object and a placeholder is used, taking the emirate from `motor.vehicle.registrationEmirate`. |
| customer.address.emirate | string | no | Where the customer LIVES — **must be one of the accepted emirates**, spelled exactly as listed in the "Accepted emirates" table. Anything else is a `400` for the whole request, so send the exact value or omit the address object entirely (the registration emirate is then used). `"Al Ain"` is accepted and is NOT the same declaration as `"Abu Dhabi"`: DNI and QIC carry it as its own place and quote it, Insurance House does not list it and is skipped with `CARRIER_UNSUPPORTED_VALUE` while the others quote normally. |
| Vehicle | |||
| motor.vehicle.chassisNumber | string | yes | 17-character VIN / chassis number |
| motor.vehicle.plateNumber | string | yes | Plate number |
| motor.vehicle.registrationEmirate | string | yes | Place of registration — the emirate printed on the registration card / Mulkiya (e.g. "Dubai", "Abu Dhabi"). **Must be one of the accepted emirates** (see the "Accepted emirates" table); a carrier that does not list the place is skipped with `CARRIER_UNSUPPORTED_VALUE` while the rest quote. Sent to the carrier; on DNI it is echoed back as quote.placeOfRegistration (Insurance House and QIC do not echo it). |
| motor.vehicle.firstRegistrationDate | string | yes | First registration date from the Mulkiya (YYYY-MM-DD) |
| motor.vehicle.plateCode | string | no | Plate code (e.g. "12", "A") |
| motor.vehicle.plateCategory | string | no | Plate registration category from the Mulkiya — e.g. `PRIVATE`, `IMPORT`, `CLASSICAL`, `DUBAI FLAG`. **Used by DNI only** today; other carriers ignore it. Matched case-insensitively against the carrier's own list for that emirate and cover type. **Omit it and `PRIVATE` is declared, exactly as before this field existed.** A value we cannot verify (unrecognised, or an emirate/cover-type combination not yet captured) also declares `PRIVATE` and adds a `quote.warnings` entry — it never rejects the request. This is **independent of `motor.vehicle.usage`**: a privately-used car can sit on an IMPORT or CLASSICAL plate. |
| motor.vehicle.color | string | no | Vehicle colour, as written on the registration card (e.g. "WHITE", "PEARL WHITE", "DARK BLUE", "GRAY AND BLACK"). **Insurance House requires it** and matches it against its own colour list (about 140 colours, including two- and three-colour combinations): case, word order, the separators `/ & , + -` and the word "and" do not matter, and GRAY/GREY, GOLD/GOLDEN and PEARL/PEARLS are treated as the same word. A colour that list does not contain is never guessed. DNI and QIC do not need it. Omit it, or send a colour Insurance House does not list, and only Insurance House is skipped (a `rejected` result with `CARRIER_REQUIREMENT_MISSING` / `CARRIER_UNSUPPORTED_VALUE`) — the other carriers still quote. |
| motor.vehicle.bodyType | string | no | Body type. One of: "sedan", "suv", "hatchback", "coupe", "convertible", "pickup", "van" — anything else is rejected for every carrier, so send one of these or omit the field. It is a HINT, not the source of truth: all three carriers take the body from their own decode of the chassis number and fall back to this only when the decode is silent (on DNI Third-Party it also helps match the carrier’s per-model options). Omitting it is safe and normal. |
| motor.vehicle.engineNumber | string | no | Engine number from the Mulkiya |
| motor.vehicle.make | string | no | Vehicle make. Required ONLY for TPL (66S); for Comprehensive (65S) it is read from the chassis. |
| motor.vehicle.model | string | no | Vehicle model. Required ONLY for TPL (66S). |
| motor.vehicle.year | number | no | Model year. Required ONLY for TPL (66S). |
| motor.vehicle.marketValueAed | number | no | Accepted for reference only — it never changes your price. Every carrier computes the vehicle value itself from the chassis, so sending 1 (or any placeholder) has no effect. Safe to omit. |
| motor.vehicle.numOfPassengers | number | no | Passenger count from the Mulkiya ("Num. of Pass"). Required for TPL (66S). For 65S it is read from the chassis; if that fails you get a CHASSIS_DECODE_INCOMPLETE error asking for it. |
| motor.vehicle.numOfDoors | number | no | Door count. Same rule as numOfPassengers. |
| motor.vehicle.mileageKm | number | no | Odometer reading in kilometres, from the registration card or the customer. Whole number. **Used by DNI only** today; the other carriers read the mileage from their own chassis lookup. Omit it and `0` is declared, exactly as every quote did before this field was documented — so sending the real figure is a declaration improvement, not a behaviour change. It is not a rating input we can see moving a premium; declare it truthfully anyway (it sits on the carrier’s record). |
| motor.vehicle.registrationTcNumber | string | no | Traffic Code (TC) number printed on the registration card / Mulkiya. Used as the carrier’s registration-card TC when tcNumberSource resolves to "registration-card". |
| motor.vehicle.tcNumberSource | string | no | Which document’s Traffic Code to push as the registration-card TC: "registration-card" or "driving-license". Defaults to "registration-card" — uses registrationTcNumber when supplied, otherwise falls back to the licence TC. The licence TC always fills the separate licence-TC field regardless. |
| Licence & driver | |||
| motor.licence.number | string | yes | Driving licence number |
| motor.licence.tcNumber | string | yes | Traffic-code (TC) number printed on the driving licence. Always fills the carrier’s licence-TC field, and is the fallback for the registration-card TC (see motor.vehicle.tcNumberSource). |
| motor.licence.expiryDate | string | yes | Licence expiry date (YYYY-MM-DD) |
| motor.licence.issueDate | string | yes | Licence first-issue date (YYYY-MM-DD). Used for pricing. |
| motor.licence.issueCountry | string | yes | Licence issuing country, ISO alpha-2 (e.g. "AE") |
| motor.licence.yearsLicensed | number | no | Optional (deprecated). No carrier prices on licence tenure, so this field never changes your quote — send motor.licence.claimFreeYears instead, which IS priced. Kept only for back-compatibility; if you do send it, it must be a whole number of years. |
| motor.licence.claimFreeYears | number | yes | Consecutive claim-free INSURED years — how many years in a row the customer has held motor insurance without a claim. Required for DNI, Insurance House, and QIC. Integer ≥ 0: 3 or more → maximum No-Claims Discount; 0 → no discount. This is NOT yearsLicensed (licence tenure) — a long-licensed but newly-insured driver has a low value. A claim ENDS the claim-free run, so count only the years since the last claim — this value is the whole of what the carriers are told about the customer's claims history. |
| motor.licence.hasAccidentsLast3Years | boolean | yes | Whether the driver had an accident in the last 3 years. Recorded on the quote, but NOT priced by any carrier: none of DNI, Insurance House or QIC has an accident field, so the discount comes from claimFreeYears alone (changed 2026-08-18 — this flag used to cancel the discount at DNI and QIC, which overrode your own claimFreeYears answer and over-priced the customer). It MUST still come from a real customer answer; do not hard-code false — the claim it describes is exactly the event that should have shortened claimFreeYears. |
| motor.licence.issueEmirate | string | no | Emirate the licence was issued in (defaults to the registration emirate). One of the accepted emirates — see the "Accepted emirates" table. |
| motor.licence.industry | string | no | The driver’s industry / occupation, e.g. "Banks", "Aviation", "Construction and building". **Used by DNI only** today. Matched against the carrier’s own occupation list — exactly, then by an unambiguous partial match. **Omit it and a generic white-collar office occupation is declared, exactly as before this field was documented.** A value we cannot match unambiguously falls back to that same default AND adds a `quote.warnings` entry, so an unrecognised occupation is visible rather than silent — it never rejects the request and never changes the price band on its own. `motor.licence.occupation` is accepted as an alias for the same field; if you send both, `industry` wins. |
| Coverage & carriers | |||
| motor.previousPolicyExpiry | string | yes | The LAST day the previous policy provides cover — the expiry date printed on it, including the whole of that day (a policy expiring on the 21st covers through the end of the 21st). Required — never assumed. This date also decides whether QIC will quote at all: together with motor.coverage.startDate it sets the previous-insurance declaration we send the carrier. For a seamless renewal set motor.coverage.startDate to the DAY AFTER this date — the vehicle is then declared insured and the quote is sellable. A gap of 2 days or more means the vehicle really was uninsured in between, and QIC freezes that declaration into the quote and then refuses to send a payment link for it, for the life of the quote. **Since 2026-08-17 we do not build that quote at all: QIC is skipped at preflight** and appears in `blockedCarriers` with `CARRIER_UNSUPPORTED_VALUE` (both of its products), while the other insurers you requested quote normally. Omitting this field entirely, without a `renewal` transactionType, skips QIC too — as `CARRIER_REQUIREMENT_MISSING` naming the field, because sending it is likely to make QIC quotable again. Neither skip affects DNI or Insurance House, which decide nothing from prior cover. |
| motor.coverage.startDate | string | no | When the new policy should start (YYYY-MM-DD). Optional — omit it and we use TODAY’S date in the UAE (Asia/Dubai), which is rarely what you want for a renewal quoted in advance. Send it nested under motor as shown in the examples: that is the canonical location. A startDate placed in the TOP-LEVEL coverage object is also accepted, and if you send both, the one under motor.coverage wins. Validation errors refer to the field as coverage.startDate either way. For a renewal, set it to motor.previousPolicyExpiry plus one day — that is the seamless successor with no uninsured day, and at QIC it is what makes the quote sellable. **At QIC it now decides whether the carrier is asked at all**: dates that leave a gap of 2 days or more skip QIC at preflight (`blockedCarriers`, `CARRIER_UNSUPPORTED_VALUE`) rather than producing a priced quote QIC will never pay-link. Avoid re-quoting the same vehicle day after day while letting this default: the start date drifts forward while the previous expiry stays put, so a contiguous renewal slowly turns into a real gap in cover — and, since 2026-08-17, silently drops QIC out of the job. DNI now declares this date as the policy start too — until 2026-08-09 every DNI quote was declared as starting on the day it was quoted, whatever you sent here, so a policy booked to start later was priced and recorded against today. It is also re-declared unchanged when the payment link is sent, so the cover period cannot drift if you sell a quote days after creating it. |
| motor.transactionType | string | no | What this transaction is: "new", "renewal", or "transfer" (lowercase is canonical; any casing is accepted). renewal = the same customer continuing cover, normally on the same vehicle; new = first-time cover for this customer and vehicle; transfer = the vehicle changed owner. Optional, but it must be TRUTHFUL: at QIC the label "renewal" on its own declares to the carrier that the vehicle currently holds valid insurance, so calling a genuinely-lapsed car a renewal misdeclares a regulated record — while calling a real renewal "new" leaves the declaration to the two dates alone. If your "new" only means "new to this insurer", send "renewal" and the real motor.previousPolicyExpiry. Note the label also decides whether QIC is asked at all: without it, and without motor.previousPolicyExpiry, QIC is skipped with `CARRIER_REQUIREMENT_MISSING` (it has no basis to declare the vehicle insured). That is not a reason to label a car a renewal — a false label buys a quote that a real customer cannot lawfully hold. |
| coverage.schemes | string[] | yes | Which product(s) to quote — one or more of: **"65S"** Comprehensive, **"66S"** Third Party (TPL), **"60A-S1"** Comprehensive High Value. Each requested scheme is quoted against each requested carrier, so `["65S","66S"]` with two carriers returns four results. **"60A-S1" is DNI-only, and it is for expensive cars.** DNI splits Comprehensive at AED 250,000: "65S" covers vehicles it values UP TO 250,000, and "60A-S1" covers vehicles it values ABOVE 250,001 (agency repair, windscreen excess waiver). The carrier decides which band a car is in from its own valuation, not from anything you send — so if a "65S" quote comes back `rejected` with `DNI_VALUE_ABOVE_SCHEME_CAP`, re-quote that vehicle with `coverage.schemes: ["60A-S1"]`. The mirror also holds: a normal-value car quoted on "60A-S1" is `rejected` with `DNI_VALUE_BELOW_SCHEME_MIN`, pointing you back to "65S". Neither check reaches the carrier’s quotation system, so a wrong guess costs nothing and leaves nothing open at DNI. There is no auto-switch: you always choose the product. Insurance House and QIC do not sell "60A-S1". Requesting it alongside them does NOT fail the request — DNI quotes it, and each carrier that does not offer it returns its own `rejected` result with `CARRIER_UNSUPPORTED_VALUE`. Requesting it with ONLY those carriers is a 400 (`NO_CARRIER_SUPPORTS_VALUE`). ⚠️ A "60A-S1" quote cannot be sold through this API yet — see `DNI_SCHEME_SELL_NOT_ENABLED` in the Sale Error Codes table. |
| targetInsurers | string[] | no | Insurer IDs to quote, run in parallel. Allowed: "dni", "insurancehouse", "qic". Defaults to ["dni"]. Note: "insurancehouse" prices Comprehensive + Third Party together from ONE submit, but results[] contains ONLY the scheme(s) you requested in coverage.schemes — "65S" returns the "IH-COMP" entry, "66S" returns the "IH-TPL" entry. Request BOTH to also get the other product's price for comparison: you then get two entries — the chosen product with quote.sellable=true, and the other as a price-only comparison entry (sellable=false) — and only in that both-requested mode will IH fall back to completing Third Party when Comprehensive is unavailable. IMPORTANT for "insurancehouse": an IH quote completes the full application into a ready-to-pay quotation, so it (a) REQUIRES all 3 documents — Driving Licence, Registration Card (Mulkiya), and Emirates ID — plus the licence number/dates, engine number, and colour, all validated up front (missing any of them skips Insurance House with `CARRIER_REQUIREMENT_MISSING`; the other carriers still quote, so send them if you want an IH price); and (b) completes ONLY ONE product (the one chosen via coverage.schemes — "66S" alone → TPL, otherwise Comprehensive). Completing the application is per-vehicle and one-time, so send a correct payload the first time. Note: "qic" (Qatar Insurance Company) supports BOTH Comprehensive ("65S") and Third Party ("66S") — each returns premium + VAT + total and is sellable via the pay-link flow. An EV / high-value / odd-risk car with no auto-priced QIC scheme comes back as "referral". Only "dni" sells the "60A-S1" Comprehensive High Value product; the other two return a `rejected` result with `CARRIER_UNSUPPORTED_VALUE` if you request it from them. |
Details
Submit a motor insurance quote request. Accepts multipart/form-data with a JSON intake object and document files. Use combined field names (mulkiya, emirates-id, license) for single-file uploads, or split variants (mulkiya-front, mulkiya-back, emirates-id-front, etc.) if you have separate scans. Other accepted kinds: passport-bio, no-claim-certificate. Returns a jobId you poll for results.
A new or retried submission returns 202 with { jobId, status: "running" }. A duplicate of an in-flight or completed request (same customer + chassis + plate + coverage) returns 200 with { jobId, status, deduped: true } and reuses the existing job — no second quote is run. Either way, poll GET /api/quotes/:jobId with the returned jobId.
`blockedCarriers` — carriers this request will not reach. When a carrier is skipped at preflight (it requires a field you did not send, or its catalogue cannot serve one of your values), the response carries a top-level blockedCarriers array naming the carrier, the product, the exact field, and whether sending that field would fix it (fixable). The other carriers quote normally, and each skipped carrier also appears in results as its own rejected run. The key is absent when nothing was blocked.
A carrier can also be skipped because WE paused it. When an insurer’s portal account is temporarily unavailable to us, we take it offline rather than send you failures. That appears in blockedCarriers as code "CARRIER_PAUSED" with field: null, fixable: false and retryable: true. Unlike CARRIER_UNSUPPORTED_VALUE this is TEMPORARY and has nothing to do with your data: there is nothing to fix, and the insurer returns on its own with no change to your integration. If EVERY insurer you requested is unavailable and at least one of them is paused, the request itself returns 503 `NO_CARRIER_AVAILABLE` with a Retry-After header rather than creating a job.
Check it. A carrier can be blocked on every single request without any of them failing: if fixable is true, add the named field and that carrier quotes next time.
Status codes
Request
curl -X POST https://api.rpai.vionark.com/api/quotes \-H "x-api-key: YOUR_API_KEY" \-F 'intake={"customer":{"fullName":"John Doe","nameInArabic":"جون دو","emiratesId":"784-1990-1234567-1","emiratesIdExpiry":"2027-12-31","dob":"1990-05-20","nationality":"IN","gender":"Male","mobileNumber":"+971501234567","email":"john@example.com"},"motor":{"vehicle":{"chassisNumber":"SJN00000000000001","plateCode":"1","plateNumber":"12345","registrationEmirate":"Abu Dhabi","registrationTcNumber":"12345678","tcNumberSource":"registration-card","color":"WHITE","engineNumber":"ABC12345","firstRegistrationDate":"2020-01-15"},"licence":{"number":"1234567","tcNumber":"12345678","expiryDate":"2027-06-30","issueDate":"2012-03-10","issueCountry":"AE","issueEmirate":"Abu Dhabi","yearsLicensed":13,"claimFreeYears":4,"hasAccidentsLast3Years":false},"previousPolicyExpiry":"2026-08-21","transactionType":"renewal","coverage":{"startDate":"2026-08-22"}},"coverage":{"schemes":["65S"]},"targetInsurers":["dni"]}' \-F 'mulkiya=@/path/to/mulkiya.pdf' \-F 'emirates-id=@/path/to/eid.jpg' \-F 'license=@/path/to/license.jpg'
Response
{"jobId": "7f3a1c2e-9b4d-4e1a-8c2f-1a2b3c4d5e6f","status": "running","blockedCarriers": [{"insurerId": "dni","schemeCode": "65S","code": "CARRIER_REQUIREMENT_MISSING","field": "customer.emiratesIdExpiry","message": "Emirates ID expiry date is required (YYYY-MM-DD).","fixable": true}]}
/api/quotes/:jobIdAuth requiredPoll quote results
Poll a job until job.status is "completed" or "failed", then read each per-insurer/scheme outcome in results[].status. Money is returned as { amount, currency } objects.
Details
Poll the status and results of a quote job. The response is { job, results } — plus blockedCarriers when a carrier was skipped at preflight (see below). Branch on the OVERALL job.status ("queued" / "running" / "completed" / "failed") — poll every 2 seconds until it is "completed" or "failed" — then read each per-insurer/scheme outcome in results[].status.
Each result is a nested object: { jobId, quoteRunId, insurerId, schemeCode, status, quote?, error?, timings }. The quote object is present on settled outcomes that produced data; money is returned as { amount, currency } objects (e.g. quote.totalAmount.amount), NOT bare numbers. On rejected / failed there is usually no quote, only error ({ code, message, stepFailed }).
A success or referral result may also carry an optional quote.makeMismatch = { enteredMake, decodedMake } when the vehicle make you submitted clearly disagrees with the make the carrier decoded from the chassis number (e.g. you sent "BMW" but the chassis decodes as "Toyota"). It is informational only — the quote is still priced for the decoded vehicle — but you should confirm the chassis number is correct before selling.
On DNI, a settled result also echoes quote.placeOfRegistration (the emirate we sent the carrier); Insurance House and QIC do not return this field. For Comprehensive (DNI 65S and 60A-S1, QIC 65S, Insurance House IH-COMP), all three carriers also return quote.valuation = { low, medium, high, chosenSumInsured, currency } — the car-value range the carrier fetched plus the sum insured we used (chosenSumInsured). Display-only; valuation is absent for TPL (66S / IH-TPL).
results[].schemeCode echoes the product this row was quoted for, so a job requesting several products is matched back by that field — it is the same code you sent in coverage.schemes (for Insurance House it is the carrier's own IH-COMP / IH-TPL).
`chosenSumInsured` is the carrier's own recommended value. All three carriers now insure on the carrier's recommended (medium) figure — the value its own form pre-fills for a human broker — so an API quote matches a manual one. Insurance House already worked this way and QIC's recommended value equals its midpoint, so neither changes; DNI used the Low/High midpoint until 2026-08-09, which under-insured and under-priced Comprehensive quotes by roughly 5%. Expect DNI 65S premiums to sit slightly higher than before, for the correct sum insured. DNI falls back to the midpoint only when it returns no usable medium.
All carriers return money split as quote.premium (net of VAT), quote.vatAmount, and quote.totalAmount (the VAT-inclusive total the customer pays) — always use quote.totalAmount as the amount payable. On Insurance House the VAT is the carrier's own figure when its quote page provides it, otherwise the UAE-standard 5%.
Vehicle identity on DNI Third-Party (66S). DNI validates the vehicle model + body type + engine against its own per-model catalogue for TPL — a value it does not recognise causes a "refer to underwriter" outcome. To match what a human broker picks from DNI's own dropdowns, we resolve what you send against DNI's catalogue: the model may be matched to DNI's catalogue spelling (e.g. "SONATA HYBRID" from the registration card → DNI's "SONATA"; "NISSAN ALTIMA" → "ALTIMA"), the body type is taken from DNI's per-model options (so an SUV is no longer assumed to be a sedan when bodyType is omitted), and the engine is read from the carrier's chassis decoder — when the exact engine is not on DNI's allowed list, the NEAREST allowed engine (within 0.4 L) may be declared instead. Every substitution adds a plain-language note to quote.warnings naming both values. When no honest match exists we declare your values as-is and the carrier may return referral — we never misdeclare a vehicle to force a price. No integration change is needed — just surface quote.warnings to your users.
Can this quote be sold — `quote.sellable`, `quote.notSellableReason` and `quote.notSellableCode`. A priced quote is normally sellable and leaves quote.sellable unset. When a quote cannot be sold as-is, the result still carries the real price but sets quote.sellable: false — and, since 2026-08-17, ALWAYS carries both a plain-language quote.notSellableReason you can show the broker as-is and a machine-readable quote.notSellableCode you can branch on. (Quotes produced before that date may carry neither; treat a missing code as "unknown reason".)
The codes, and what each one means for you:
- IH_COMPARISON_ONLY — Insurance House prices BOTH products from one submission but completes the application for only one of them, so when you request both schemes the other comes back as a comparison price. To sell THIS product, request it alone in coverage.schemes.
- IH_APPLICATION_INCOMPLETE — Insurance House’s website hung after pricing. The quote is real and saved on their side under the reference we return, but the application never finished, so no payment link can be sent. Do NOT auto-re-quote this vehicle — surface it to a human broker, who completes the saved quotation in the Insurance House portal. Re-running almost always hangs again or trips Insurance House’s duplicate check, and every attempt uses up a submission for that chassis.
- IH_POLICY_ALREADY_ISSUED — the recovered Insurance House quotation is already an issued policy. Nothing to sell.
- QIC_PREVIOUS_INSURANCE_INVALID — the QIC quote declared the vehicle as NOT previously insured (the previous policy’s expiry does not run up to the policy start date). QIC refuses to pay-link such a quote for its whole life; only a NEW quote with different dates can change it. Since 2026-08-17 you should meet this earlier and cheaper: those dates are visible in your request, so QIC is skipped at preflight and reported in blockedCarriers instead of being quoted. Expect this code on quotes created before that date.
- QIC_SCHEME_NOT_COMMITTED — QIC priced the plan but did not confirm it on their side, so it is an uncommitted draft. Re-quote before selling.
Neither field is a quote.warnings entry — they describe sellability only and never mean the price is unreliable. A POST /sell on a non-sellable quote is rejected immediately with 409 carrying the same explanation: QIC_PREVIOUS_INSURANCE_INVALID for the QIC previous-insurance case, QUOTE_NOT_SELLABLE otherwise.
`blockedCarriers` — carriers that never ran. A carrier is skipped at preflight when it requires a field you did not send (CARRIER_REQUIREMENT_MISSING) or it cannot serve one of your values (CARRIER_UNSUPPORTED_VALUE — usually a catalogue gap, but at QIC also a gap in cover: dates showing 2+ uninsured days produce a quote QIC will never pay-link, so we do not build it). The other carriers quote normally, each skipped carrier appears in results as its own rejected run, and the response also carries a top-level blockedCarriers array summarising them: { insurerId, schemeCode, code, field, message, fixable }. field is the exact path to add (e.g. customer.emiratesIdExpiry) and fixable: true means sending it will let that carrier quote next time. The key is absent when nothing was blocked.
A carrier can also be skipped because WE paused it. When an insurer’s portal account is temporarily unavailable to us, we take it offline rather than send you failures. That appears in blockedCarriers as code "CARRIER_PAUSED" with field: null, fixable: false and retryable: true. Unlike CARRIER_UNSUPPORTED_VALUE this is TEMPORARY and has nothing to do with your data: there is nothing to fix, and the insurer returns on its own with no change to your integration. If EVERY insurer you requested is unavailable and at least one of them is paused, the request itself returns 503 `NO_CARRIER_AVAILABLE` with a Retry-After header rather than creating a job.
Please check it on every job. A carrier can be blocked on 100% of your requests without a single one "failing" — this is the field that tells you.
Recovered ("reclaimed") quotes — Insurance House. If the carrier's website hangs AFTER a quote has priced, we automatically recover the quote from the carrier's saved-quote list instead of failing the run. Such a success result carries quote.reclaimed: true plus a plain-language note in quote.warnings. Check quote.sellable: true means the application also completed on the carrier's side — sell it as normal; false means the price is real but the application did not finish, and the result then carries quote.notSellableCode — usually "IH_APPLICATION_INCOMPLETE", or "IH_POLICY_ALREADY_ISSUED" when the recovered quotation had already become a policy (a POST /sell on either returns 409 QUOTE_NOT_SELLABLE). Do NOT auto-re-quote that vehicle — hand it to a human broker to finish in the Insurance House portal; re-running almost always hangs again or trips the carrier's duplicate check, and each attempt uses up a submission for that chassis. Only when the recovery is impossible (the carrier is truly down) does the run stay failed with error.code: "INSURER_UNAVAILABLE".
Status codes
Request
curl https://api.rpai.vionark.com/api/quotes/7f3a1c2e-9b4d-4e1a-8c2f-1a2b3c4d5e6f \-H "x-api-key: YOUR_API_KEY"
Response
{"job": {"id": "7f3a1c2e-9b4d-4e1a-8c2f-1a2b3c4d5e6f","status": "completed","product": "motor","targetInsurers": ["dni"],"createdAt": "2026-06-19T08:00:00.000Z","updatedAt": "2026-06-19T08:00:28.000Z","customerEmail": "john@example.com"},"results": [{"jobId": "7f3a1c2e-9b4d-4e1a-8c2f-1a2b3c4d5e6f","quoteRunId": "8f3a1c2e-aaaa-4e1a-8c2f-1a2b3c4d5e60","insurerId": "dni","schemeCode": "65S","status": "success","quote": {"insurerQuoteRef": "MT-2026-0000001","premium": {"amount": 1700,"currency": "AED"},"vatAmount": {"amount": 85,"currency": "AED"},"totalAmount": {"amount": 1785,"currency": "AED"},"planName": "Motor Standard","deductible": {"amount": 200,"currency": "AED"},"repairCondition": "Garage","placeOfRegistration": "Dubai","valuation": {"low": 36417,"medium": 42843,"high": 47127,"chosenSumInsured": 42843,"currency": "AED"},"makeMismatch": {"enteredMake": "BMW","decodedMake": "TOYOTA"}},"timings": {"startedAt": "2026-06-19T08:00:01.000Z","endedAt": "2026-06-19T08:00:28.000Z","durationMs": 27000}},{"jobId": "7f3a1c2e-9b4d-4e1a-8c2f-1a2b3c4d5e6f","quoteRunId": "9f3a1c2e-bbbb-4e1a-8c2f-1a2b3c4d5e61","insurerId": "dni","schemeCode": "66S","status": "rejected","error": {"code": "NON_GCC","message": "Vehicle Region='Non-GCC'. DNI scheme 66S requires GCC-spec vehicles.","stepFailed": "decodeVin"},"timings": {"startedAt": "2026-06-19T08:00:01.000Z","endedAt": "2026-06-19T08:00:22.000Z","durationMs": 21000}}]}
/api/quotes/:jobId/sellAuth requiredSend payment link
Trigger the payment-link send for an accepted quote (requires broker consent). Works for all three carriers; auto-polling for the issued policy starts automatically.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| quoteRunId | string | yes | ID of the specific quote run to sell |
| customerEmail | string | yes | Customer email on file — used for validation only (must match the customer on file, returns 400 EMAIL_MISMATCH otherwise). The payment link itself is sent to the broker’s central inbox, not this address. |
| brokerConsentGiven | boolean | yes | Must be true — broker confirms they reviewed the quote |
| brokerConsentAt | string | yes | ISO timestamp of when the broker gave consent. Must be within the last 30 minutes. Your clock may be up to 2 minutes ahead of ours and is still accepted (tolerated as a small clock skew); beyond that the request is rejected with 400 CONSENT_CLOCK_DRIFT — keep your server clock NTP-synced. |
Details
Trigger sending the payment link for an accepted quote. Requires explicit broker consent (checkbox + timestamp). The carrier sends the payment email to the broker’s central inbox (eSanad mediates payment) — not to the customer directly. Works for DNI, Insurance House, and QIC.
202 means accepted, not sent. The response body carries the freshly-claimed sale attempt with status: "pending" — the payment-link send has only been *queued*, and it can still fail. Poll GET /api/quotes/:jobId/sale until the status becomes payment-link-sent (link sent) or failed (see the Sale Error Codes reference for why).
Auto-polling after sell: once the link is ACTUALLY sent (status reaches payment-link-sent), a background workflow starts polling the carrier every 10 minutes for up to 24 hours, watching for the issued policy. You do not need to do anything — once coreStatus === "Approved" appears on GET /policy, the policy is issued and synced to RTA. You can short-circuit the wait by calling POST /refresh-policy manually — for DNI, pass the CRS policy number the customer forwards; for Insurance House and QIC, pass nothing (the policy is auto-derived from the saved quote reference).
All three carriers auto-poll (DNI, Insurance House, QIC). QIC discovers the issued policy by the saved quote number (no policy number to paste) and caches the Policy Schedule + Receipt PDFs to S3.
If issuance takes longer than 24 hours — the carrier can issue the policy hours-to-days after payment — the auto-poll stops, the sale stays at payment-link-sent, and no automatic notification is sent. Retrieve a late-issued policy at ANY time (no time limit) by calling POST /refresh-policy with the CRS number.
Status codes
Request
curl -X POST https://api.rpai.vionark.com/api/quotes/7f3a1c2e-9b4d-4e1a-8c2f-1a2b3c4d5e6f/sell \-H "x-api-key: YOUR_API_KEY" \-H "Content-Type: application/json" \-d '{"quoteRunId":"8f3a1c2e-aaaa-4e1a-8c2f-1a2b3c4d5e60","customerEmail":"john@example.com","brokerConsentGiven":true,"brokerConsentAt":"2026-05-26T12:00:00Z"}'
Response
{"saleAttemptId": "4a1b2c3d-1111-4e1a-8c2f-1a2b3c4d5e70","status": "pending","quotationRef": "MT-2026-0000001"}
/api/quotes/:jobId/saleAuth requiredPoll sale status
Poll a sale attempt for a job (pass ?quoteRunId to track one run). Status progresses pending → payment-link-sent → policy-issued | failed.
Details
Poll the status of a sale attempt (payment link send).
Query param `?quoteRunId=<uuid>` (optional but recommended). Pass the run you sold to get THAT run's attempt. This is REQUIRED to correctly track a multi-product job where you sold more than one run (e.g. a 65S Comprehensive and a 66S TPL sold separately) — each run has its own sale attempt, and without the param the response is just the most recent attempt for the WHOLE job (so one run's poll could read the other run's status). For a single sale it can be omitted. A malformed quoteRunId returns 400 BAD_QUOTE_RUN_ID.
Returns { saleAttempt } (the matching attempt, or the newest for the job when no param is given), or { saleAttempt: null } if no sell has been triggered yet. The saleAttempt always carries id, jobId, quoteRunId, insurerId, quotationRef, customerEmail, status, brokerConsentAt, initiatedAt; completedAt appears once the link is sent; on failure an error: { step, code, message } object is included — see the Sale Error Codes reference for the stable code values (e.g. IH_COMPLIANCE_HOLD is a permanent customer-specific hold; INTERNAL is retryable). No `paymentUrl` is returned — the carrier emails the payment link straight to the broker’s central inbox (eSanad mediates payment), so the API never hands back a URL. (Note: the field is completedAt, not sentAt.)
Status progression: pending → payment-link-sent → policy-issued | failed. The terminal policy-issued state is set by the auto-polling workflow once the carrier confirms the policy has been issued; you can also watch GET /policy directly for the richer policy data.
Status codes
Request
curl "https://api.rpai.vionark.com/api/quotes/7f3a1c2e-9b4d-4e1a-8c2f-1a2b3c4d5e6f/sale?quoteRunId=8f3a1c2e-aaaa-4e1a-8c2f-1a2b3c4d5e60" \-H "x-api-key: YOUR_API_KEY"
Response
{"saleAttempt": {"id": "4a1b2c3d-1111-4e1a-8c2f-1a2b3c4d5e70","jobId": "7f3a1c2e-9b4d-4e1a-8c2f-1a2b3c4d5e6f","quoteRunId": "8f3a1c2e-aaaa-4e1a-8c2f-1a2b3c4d5e60","insurerId": "dni","quotationRef": "MT-2026-0000001","customerEmail": "john@example.com","status": "payment-link-sent","brokerConsentAt": "2026-05-26T12:00:00.000Z","initiatedAt": "2026-05-26T12:00:01.000Z","completedAt": "2026-05-26T12:00:15.000Z"}}
/api/quotes/:jobId/refresh-policyAuth requiredRefresh policy
One-shot retrieval of an issued policy. DNI: pass crsPolNo. Insurance House / QIC: omit it (auto-derived). Optional after a sell — auto-polling already runs.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| quoteRunId | string (UUID) | yes | ID of the specific quote run that produced the policy. Find it in the `results[]` array of `GET /api/quotes/:jobId`. |
| crsPolNo | string | no | DNI ONLY (required for DNI; ignored for Insurance House, which auto-derives the policy from its saved quote ref). CRS policy number in UAE regulator format. Regex: `^\d{2}/\d{3}/[\w-]+/\d{4}/\d+$`. Example: `02/601/65S/2026/00001`. The scheme segment accepts letters + hyphens so future scheme codes (66S, 65-CH, ...) round-trip without an update. |
| saleAttemptId | string (UUID) | no | Set when refreshing after a sale. When `coreStatus === "Approved"` is detected, the corresponding sale_attempt is bumped from `payment-link-sent` to `policy-issued`. Omit for stand-alone refreshes. |
Details
Trigger a one-shot retrieval of an issued policy from the carrier. Carrier-agnostic: the same endpoint serves every carrier; only HOW the policy is identified differs. DNI - the broker PASTES crsPolNo (the UAE regulator policy ID, format 02/601/65S/2026/00001), from the customer confirmation email or DNI dashboard. Insurance House - auto-derives the policy from its saved quote ref, so OMIT crsPolNo (it is ignored for IH); IH policy numbers are shaped DP/01/1001/26/00001 and appear in the response only once IIRIS issues the policy (before that the status reads Pending Approval); IH caches the Policy Schedule + Tax Invoice PDFs to S3 (returned as schedulePdfSignedUrl + invoicePdfSignedUrl). QIC - also auto-derives from the saved quote number, so OMIT crsPolNo; QIC policy numbers are shaped 2620000001 and appear only once QIC issues the policy (before that the status reads Pending Approval); QIC caches the Policy Schedule + Receipt PDFs to S3 (returned as schedulePdfSignedUrl + invoicePdfSignedUrl). Read-only: never binds, never emails. The retrieval takes ~20–30s; poll GET /policy for the result.
This endpoint is OPTIONAL when a sell has been triggered: the auto-polling workflow (started by POST /sell) already polls the carrier every 10 minutes for up to 24 hours and will discover the policy for you (the CRS for DNI; the issued policy for IH). Use this endpoint to short-circuit the wait when you already have the CRS in hand. It is ALSO the fallback after that window: the auto-poll gives up at 24 hours, so for a policy the carrier issues later (often days after payment) call this endpoint with the CRS number — it has no time limit.
Status codes
Request
curl -X POST https://api.rpai.vionark.com/api/quotes/job_abc123def456/refresh-policy \-H "x-api-key: YOUR_API_KEY" \-H "Content-Type: application/json" \-d '{"quoteRunId":"123e4567-e89b-12d3-a456-426614174000","crsPolNo":"02/601/65S/2026/00001","saleAttemptId":"sale_abc123"}'
Response
{"workflowId": "policy-123e4567-e89b-12d3-a456-426614174000-abc123","quoteRunId": "123e4567-e89b-12d3-a456-426614174000","crsPolNo": "02/601/65S/2026/00001","status": "running"}
/api/quotes/:jobId/policyAuth requiredGet policy
Poll the latest retrieved policy. Returns { policy, policies } with coreStatus and 15-minute signed PDF URLs. One row per policy on multi-policy jobs.
Details
Poll the latest retrieved policy for a job. Returns { policy: null } until the first successful retrieval, then the full policy record. For multi-policy jobs (e.g. 65S Comprehensive + 66S TPL sold to the same customer), an additional policies array is returned ordered DESC by retrievedAt — render one panel per row; the convenience policy field is policies[0] for backward compatibility.
Signed PDF URLs (schedulePdfSignedUrl, jacketPdfSignedUrl, invoicePdfSignedUrl) are valid for 15 minutes and point to our S3 cache; refresh the row to regenerate them. Carrier-hosted URLs (schedulePdfUrl, dnLink, cnLink, receiptLink) are absolute and may require a carrier login.
`customerSnapshot.email` and `customerSnapshot.mobile` reflect the fixed broker contact (cs@esanad.com / 0522486502), NOT the customer’s real email/phone — the carrier-facing contact is centralised (the customer’s real contact stays in our DB only). fullName and gender ARE the real customer’s.
Status codes
Request
curl https://api.rpai.vionark.com/api/quotes/7f3a1c2e-9b4d-4e1a-8c2f-1a2b3c4d5e6f/policy \-H "x-api-key: YOUR_API_KEY"
Response
{"policy": {"id": "5b2c3d4e-2222-4e1a-8c2f-1a2b3c4d5e80","jobId": "7f3a1c2e-9b4d-4e1a-8c2f-1a2b3c4d5e6f","quoteRunId": "8f3a1c2e-aaaa-4e1a-8c2f-1a2b3c4d5e60","saleAttemptId": "4a1b2c3d-1111-4e1a-8c2f-1a2b3c4d5e70","insurerId": "dni","schemeCode": "65S","crsPolNo": "02/601/65S/2026/00001","pmtPolNo": "PMT00000000000","quotationRef": "MT-2026-0000001","coreStatus": "Approved","coreStatusRaw": "Approved","isPolicyOnAccount": false,"dateIssued": "2026-06-18","inceptionDate": "2026-06-30","expiryDate": "2027-07-29","baseContribution": 2000,"vatAmount": 100,"totalContribution": 2100,"currencyCode": "AED","paymentType": "ONLINE","productName": "MOTOR COMPREHENSIVE INSURANCE","productCode": "MT00061","schedulePdfUrl": "https://portal.dni.ae/ShieldXLAPI/pdfDoc/PMT000...pdf","schedulePdfSignedUrl": "https://rpa-docs.s3.me-central-1.amazonaws.com/policies/2026/06/.../schedule-...uuid.pdf?X-Amz-Expires=900&...","jacketPdfSignedUrl": "https://rpa-docs.s3.me-central-1.amazonaws.com/policies/2026/06/.../jacket-...uuid.pdf?X-Amz-Expires=900&...","dnLink": "https://portal.dni.ae/PortalDocs/...DN.PDF","cnLink": "https://portal.dni.ae/PortalDocs/...CN.PDF","receiptLink": "https://portal.dni.ae/PortalDocs/...Receipt.PDF","customerSnapshot": {"fullName": "Sample Customer","email": "cs@esanad.com","mobile": "0522486502","gender": "Female"},"vehicleSnapshot": {"make": "BMW","model": "2 SERIES","modelYear": 2017,"chassisNumber": "WBA00000000000002","color": "WHITE","bodyType": "SEDAN","registrationNumber": "12345"},"retrievedAt": "2026-06-18T12:00:30.000Z","updatedAt": "2026-06-18T12:00:30.000Z","createdAt": "2026-06-18T12:00:30.000Z"},"policies": [{"id": "5b2c3d4e-2222-4e1a-8c2f-1a2b3c4d5e80","jobId": "7f3a1c2e-9b4d-4e1a-8c2f-1a2b3c4d5e6f","quoteRunId": "8f3a1c2e-aaaa-4e1a-8c2f-1a2b3c4d5e60","saleAttemptId": "4a1b2c3d-1111-4e1a-8c2f-1a2b3c4d5e70","insurerId": "dni","schemeCode": "65S","crsPolNo": "02/601/65S/2026/00001","pmtPolNo": "PMT00000000000","quotationRef": "MT-2026-0000001","coreStatus": "Approved","coreStatusRaw": "Approved","isPolicyOnAccount": false,"dateIssued": "2026-06-18","inceptionDate": "2026-06-30","expiryDate": "2027-07-29","baseContribution": 2000,"vatAmount": 100,"totalContribution": 2100,"currencyCode": "AED","paymentType": "ONLINE","productName": "MOTOR COMPREHENSIVE INSURANCE","productCode": "MT00061","schedulePdfUrl": "https://portal.dni.ae/ShieldXLAPI/pdfDoc/PMT000...pdf","schedulePdfSignedUrl": "https://rpa-docs.s3.me-central-1.amazonaws.com/policies/2026/06/.../schedule-...uuid.pdf?X-Amz-Expires=900&...","jacketPdfSignedUrl": "https://rpa-docs.s3.me-central-1.amazonaws.com/policies/2026/06/.../jacket-...uuid.pdf?X-Amz-Expires=900&...","dnLink": "https://portal.dni.ae/PortalDocs/...DN.PDF","cnLink": "https://portal.dni.ae/PortalDocs/...CN.PDF","receiptLink": "https://portal.dni.ae/PortalDocs/...Receipt.PDF","customerSnapshot": {"fullName": "Sample Customer","email": "cs@esanad.com","mobile": "0522486502","gender": "Female"},"vehicleSnapshot": {"make": "BMW","model": "2 SERIES","modelYear": 2017,"chassisNumber": "WBA00000000000002","color": "WHITE","bodyType": "SEDAN","registrationNumber": "12345"},"retrievedAt": "2026-06-18T12:00:30.000Z","updatedAt": "2026-06-18T12:00:30.000Z","createdAt": "2026-06-18T12:00:30.000Z"},{"id": "6c3d4e5f-3333-4e1a-8c2f-1a2b3c4d5e90","jobId": "7f3a1c2e-9b4d-4e1a-8c2f-1a2b3c4d5e6f","quoteRunId": "9a4b2c3d-bbbb-4e1a-8c2f-1a2b3c4d5e61","saleAttemptId": "5b2c3d4e-2222-4e1a-8c2f-1a2b3c4d5e71","insurerId": "insurancehouse","schemeCode": "IH-COMP","crsPolNo": "DP/01/1001/26/00001","pmtPolNo": "000000","quotationRef": "Q/MOT/000001","coreStatus": "Approved","coreStatusRaw": "A","isPolicyOnAccount": false,"inceptionDate": "2026-06-25","expiryDate": "2027-07-24","baseContribution": 2228.61,"vatAmount": 111.43,"totalContribution": 2340.04,"currencyCode": "AED","productName": "Motor Comprehensive","schedulePdfSignedUrl": "https://rpa-docs.s3.me-central-1.amazonaws.com/policies/2026/06/.../schedule-...uuid.pdf?X-Amz-Expires=900&...","invoicePdfSignedUrl": "https://rpa-docs.s3.me-central-1.amazonaws.com/policies/2026/06/.../invoice-...uuid.pdf?X-Amz-Expires=900&...","customerSnapshot": {"fullName": "Sample Customer","email": "cs@esanad.com","mobile": "0522486502","gender": "Male"},"vehicleSnapshot": {"make": "INFINITI","model": "Q50","modelYear": 2023,"chassisNumber": "JN100000000000003"},"retrievedAt": "2026-06-28T09:15:30.000Z","updatedAt": "2026-06-28T09:15:30.000Z","createdAt": "2026-06-28T09:15:30.000Z"}]}
Supported countries
customer.nationality and motor.licence.issueCountry take an ISO 3166-1 alpha-2 code — "SY", not "Syrian Arab Republic". Map from the code, not the display name: about 30 countries have a formal name that differs from the everyday one ("Russian Federation" = RU, "Viet Nam" = VN).
Coverage differs per carrier. A country ticked for some carriers but not others is not an error: the carriers that cover it quote normally, and the one that does not returns a rejected result with CARRIER_UNSUPPORTED_VALUE. You only get a 400 when no carrier you requested covers it.
Supported countries — 241 total (DNI 41 · Insurance House 236 · QIC 234)
| Code | Country | DNI | Insurance House | QIC |
|---|---|---|---|---|
| AF | Afghanistan | ✓ | ✓ | ✓ |
| AL | Albania | · | ✓ | ✓ |
| DZ | Algeria | · | ✓ | ✓ |
| AS | American Samoa | · | ✓ | ✓ |
| AD | Andorra | · | ✓ | ✓ |
| AO | Angola | · | ✓ | ✓ |
| AI | Anguilla | · | ✓ | ✓ |
| AQ | Antarctica | · | ✓ | · |
| AG | Antigua & Barbuda | · | · | ✓ |
| AR | Argentina | · | ✓ | ✓ |
| AM | Armenia | · | ✓ | ✓ |
| AW | Aruba | · | ✓ | ✓ |
| AU | Australia | ✓ | ✓ | ✓ |
| AT | Austria | · | ✓ | ✓ |
| AZ | Azerbaijan | · | ✓ | ✓ |
| BS | Bahamas | · | ✓ | ✓ |
| BH | Bahrain | ✓ | ✓ | ✓ |
| BD | Bangladesh | ✓ | ✓ | ✓ |
| BB | Barbados | · | ✓ | ✓ |
| BY | Belarus | · | ✓ | ✓ |
| BE | Belgium | · | ✓ | ✓ |
| BZ | Belize | · | ✓ | ✓ |
| BJ | Benin | · | ✓ | ✓ |
| BM | Bermuda | · | ✓ | ✓ |
| BT | Bhutan | · | ✓ | ✓ |
| BO | Bolivia | · | ✓ | ✓ |
| BA | Bosnia & Herzegovina | · | ✓ | ✓ |
| BW | Botswana | · | ✓ | ✓ |
| BV | Bouvet Island | · | ✓ | ✓ |
| BR | Brazil | · | ✓ | ✓ |
| VG | British Virgin Islands | · | ✓ | ✓ |
| BN | Brunei | · | ✓ | ✓ |
| BG | Bulgaria | · | ✓ | ✓ |
| BF | Burkina Faso | · | ✓ | ✓ |
| BI | Burundi | · | ✓ | ✓ |
| KH | Cambodia | · | ✓ | ✓ |
| CM | Cameroon | · | ✓ | ✓ |
| CA | Canada | ✓ | ✓ | ✓ |
| CV | Cape Verde | · | ✓ | ✓ |
| KY | Cayman Islands | · | ✓ | ✓ |
| CF | Central African Republic | · | ✓ | ✓ |
| TD | Chad | · | ✓ | ✓ |
| CL | Chile | · | ✓ | ✓ |
| CN | China | ✓ | ✓ | ✓ |
| CX | Christmas Island | · | ✓ | ✓ |
| CC | Cocos (Keeling) Islands | · | ✓ | · |
| CO | Colombia | · | ✓ | ✓ |
| KM | Comoros | · | ✓ | ✓ |
| CG | Congo - Brazzaville | · | ✓ | ✓ |
| CD | Congo - Kinshasa | · | · | ✓ |
| CK | Cook Islands | · | ✓ | ✓ |
| CR | Costa Rica | · | ✓ | ✓ |
| CI | Côte d’Ivoire | · | ✓ | ✓ |
| HR | Croatia | · | ✓ | ✓ |
| CU | Cuba | · | ✓ | ✓ |
| AN | Curaçao | · | · | ✓ |
| CY | Cyprus | · | ✓ | ✓ |
| CZ | Czechia | · | ✓ | ✓ |
| DK | Denmark | · | ✓ | ✓ |
| DJ | Djibouti | · | ✓ | ✓ |
| DM | Dominica | · | ✓ | ✓ |
| DO | Dominican Republic | · | ✓ | ✓ |
| EC | Ecuador | · | ✓ | ✓ |
| EG | Egypt | ✓ | ✓ | ✓ |
| SV | El Salvador | · | ✓ | ✓ |
| GQ | Equatorial Guinea | · | ✓ | ✓ |
| ER | Eritrea | · | ✓ | ✓ |
| EE | Estonia | · | ✓ | ✓ |
| SZ | Eswatini | · | ✓ | ✓ |
| ET | Ethiopia | · | ✓ | ✓ |
| FK | Falkland Islands | · | ✓ | ✓ |
| FO | Faroe Islands | · | ✓ | · |
| FJ | Fiji | · | ✓ | ✓ |
| FI | Finland | · | ✓ | ✓ |
| FR | France | ✓ | ✓ | ✓ |
| GF | French Guiana | · | ✓ | · |
| PF | French Polynesia | · | ✓ | ✓ |
| TF | French Southern Territories | · | ✓ | ✓ |
| GA | Gabon | · | ✓ | ✓ |
| GM | Gambia | · | ✓ | ✓ |
| GE | Georgia | · | ✓ | ✓ |
| DE | Germany | ✓ | ✓ | ✓ |
| GH | Ghana | · | ✓ | ✓ |
| GI | Gibraltar | · | ✓ | ✓ |
| GR | Greece | · | ✓ | ✓ |
| GL | Greenland | · | ✓ | ✓ |
| GD | Grenada | · | ✓ | ✓ |
| GP | Guadeloupe | · | ✓ | ✓ |
| GU | Guam | · | ✓ | ✓ |
| GT | Guatemala | · | ✓ | ✓ |
| GG | Guernsey | · | ✓ | ✓ |
| GN | Guinea | · | ✓ | ✓ |
| GW | Guinea-Bissau | · | ✓ | ✓ |
| GY | Guyana | · | ✓ | ✓ |
| HT | Haiti | · | ✓ | ✓ |
| HM | Heard & McDonald Islands | · | ✓ | ✓ |
| HN | Honduras | · | ✓ | ✓ |
| HK | Hong Kong SAR China | · | · | ✓ |
| HU | Hungary | · | ✓ | ✓ |
| IS | Iceland | · | ✓ | ✓ |
| IN | India | ✓ | ✓ | ✓ |
| ID | Indonesia | ✓ | ✓ | ✓ |
| IR | Iran | ✓ | ✓ | ✓ |
| IQ | Iraq | ✓ | ✓ | ✓ |
| IE | Ireland | · | ✓ | ✓ |
| IM | Isle of Man | · | ✓ | · |
| IL | Israel | · | ✓ | ✓ |
| IT | Italy | ✓ | ✓ | ✓ |
| JM | Jamaica | · | ✓ | ✓ |
| JP | Japan | ✓ | ✓ | ✓ |
| JO | Jordan | ✓ | ✓ | ✓ |
| KZ | Kazakhstan | · | ✓ | ✓ |
| KE | Kenya | ✓ | ✓ | ✓ |
| KI | Kiribati | · | ✓ | ✓ |
| XK | Kosovo | · | · | ✓ |
| KW | Kuwait | ✓ | ✓ | ✓ |
| KG | Kyrgyzstan | · | ✓ | ✓ |
| LA | Laos | · | ✓ | ✓ |
| LV | Latvia | · | ✓ | ✓ |
| LB | Lebanon | ✓ | ✓ | ✓ |
| LS | Lesotho | · | ✓ | ✓ |
| LR | Liberia | · | ✓ | ✓ |
| LY | Libya | · | ✓ | ✓ |
| LI | Liechtenstein | · | ✓ | ✓ |
| LT | Lithuania | · | ✓ | ✓ |
| LU | Luxembourg | · | ✓ | ✓ |
| MO | Macao SAR China | · | ✓ | ✓ |
| MG | Madagascar | · | ✓ | ✓ |
| MW | Malawi | · | ✓ | ✓ |
| MY | Malaysia | ✓ | ✓ | ✓ |
| MV | Maldives | · | ✓ | ✓ |
| ML | Mali | · | ✓ | ✓ |
| MT | Malta | · | ✓ | ✓ |
| MH | Marshall Islands | · | ✓ | ✓ |
| MQ | Martinique | · | ✓ | ✓ |
| MR | Mauritania | · | ✓ | ✓ |
| MU | Mauritius | · | ✓ | ✓ |
| YT | Mayotte | · | ✓ | ✓ |
| MX | Mexico | · | ✓ | ✓ |
| FM | Micronesia | · | ✓ | ✓ |
| MD | Moldova | · | ✓ | ✓ |
| MC | Monaco | · | ✓ | ✓ |
| MN | Mongolia | · | ✓ | ✓ |
| ME | Montenegro | · | ✓ | ✓ |
| MS | Montserrat | · | ✓ | ✓ |
| MA | Morocco | · | ✓ | ✓ |
| MZ | Mozambique | · | ✓ | ✓ |
| MM | Myanmar (Burma) | · | ✓ | ✓ |
| NA | Namibia | · | ✓ | ✓ |
| NR | Nauru | · | ✓ | ✓ |
| NP | Nepal | ✓ | ✓ | ✓ |
| NL | Netherlands | · | ✓ | ✓ |
| NC | New Caledonia | · | ✓ | ✓ |
| NZ | New Zealand | · | ✓ | ✓ |
| NI | Nicaragua | · | ✓ | ✓ |
| NE | Niger | · | ✓ | ✓ |
| NG | Nigeria | ✓ | ✓ | ✓ |
| NU | Niue | · | ✓ | ✓ |
| NF | Norfolk Island | · | ✓ | ✓ |
| KP | North Korea | · | ✓ | ✓ |
| MK | North Macedonia | · | ✓ | ✓ |
| MP | Northern Mariana Islands | · | ✓ | ✓ |
| NO | Norway | · | ✓ | ✓ |
| OM | Oman | ✓ | ✓ | ✓ |
| PK | Pakistan | ✓ | ✓ | ✓ |
| PW | Palau | · | ✓ | ✓ |
| PS | Palestinian Territories | ✓ | ✓ | ✓ |
| PA | Panama | · | ✓ | ✓ |
| PG | Papua New Guinea | · | ✓ | ✓ |
| PY | Paraguay | · | ✓ | ✓ |
| PE | Peru | · | ✓ | ✓ |
| PH | Philippines | ✓ | ✓ | ✓ |
| PN | Pitcairn Islands | · | ✓ | ✓ |
| PL | Poland | · | ✓ | ✓ |
| PT | Portugal | · | ✓ | ✓ |
| PR | Puerto Rico | · | ✓ | ✓ |
| QA | Qatar | ✓ | ✓ | ✓ |
| RE | Réunion | · | ✓ | ✓ |
| RO | Romania | · | ✓ | ✓ |
| RU | Russia | ✓ | ✓ | ✓ |
| RW | Rwanda | · | ✓ | ✓ |
| WS | Samoa | · | ✓ | ✓ |
| SM | San Marino | · | ✓ | ✓ |
| ST | São Tomé & Príncipe | · | ✓ | ✓ |
| SA | Saudi Arabia | ✓ | ✓ | ✓ |
| SN | Senegal | · | ✓ | ✓ |
| RS | Serbia | · | ✓ | ✓ |
| SC | Seychelles | · | ✓ | ✓ |
| SL | Sierra Leone | · | ✓ | ✓ |
| SG | Singapore | · | ✓ | ✓ |
| SK | Slovakia | · | ✓ | ✓ |
| SI | Slovenia | · | ✓ | ✓ |
| SB | Solomon Islands | · | ✓ | ✓ |
| SO | Somalia | · | ✓ | ✓ |
| ZA | South Africa | ✓ | ✓ | ✓ |
| KR | South Korea | ✓ | ✓ | ✓ |
| SS | South Sudan | · | ✓ | ✓ |
| ES | Spain | ✓ | ✓ | ✓ |
| LK | Sri Lanka | ✓ | ✓ | ✓ |
| SH | St. Helena | · | ✓ | ✓ |
| KN | St. Kitts & Nevis | · | ✓ | ✓ |
| LC | St. Lucia | · | ✓ | ✓ |
| PM | St. Pierre & Miquelon | · | ✓ | ✓ |
| VC | St. Vincent & Grenadines | · | ✓ | ✓ |
| SD | Sudan | ✓ | ✓ | ✓ |
| SR | Suriname | · | ✓ | ✓ |
| SJ | Svalbard & Jan Mayen | · | ✓ | ✓ |
| SE | Sweden | · | ✓ | ✓ |
| CH | Switzerland | · | ✓ | ✓ |
| SY | Syria | ✓ | ✓ | ✓ |
| TW | Taiwan | · | ✓ | ✓ |
| TJ | Tajikistan | · | ✓ | ✓ |
| TZ | Tanzania | · | ✓ | ✓ |
| TH | Thailand | ✓ | ✓ | ✓ |
| TL | Timor-Leste | · | ✓ | ✓ |
| TG | Togo | · | ✓ | ✓ |
| TK | Tokelau | · | ✓ | ✓ |
| TO | Tonga | · | ✓ | ✓ |
| TT | Trinidad & Tobago | · | ✓ | ✓ |
| TN | Tunisia | · | ✓ | ✓ |
| TR | Türkiye | ✓ | ✓ | ✓ |
| TM | Turkmenistan | · | ✓ | ✓ |
| TC | Turks & Caicos Islands | · | ✓ | ✓ |
| TV | Tuvalu | · | ✓ | ✓ |
| VI | U.S. Virgin Islands | · | ✓ | ✓ |
| UG | Uganda | · | ✓ | ✓ |
| UA | Ukraine | · | ✓ | ✓ |
| AE | United Arab Emirates | ✓ | ✓ | ✓ |
| GB | United Kingdom | ✓ | ✓ | ✓ |
| US | United States | ✓ | ✓ | ✓ |
| UY | Uruguay | · | ✓ | ✓ |
| UZ | Uzbekistan | · | ✓ | ✓ |
| VU | Vanuatu | · | ✓ | ✓ |
| VA | Vatican City | · | ✓ | ✓ |
| VE | Venezuela | · | ✓ | ✓ |
| VN | Vietnam | · | ✓ | ✓ |
| WF | Wallis & Futuna | · | ✓ | · |
| EH | Western Sahara | · | ✓ | · |
| YE | Yemen | ✓ | ✓ | ✓ |
| ZM | Zambia | · | ✓ | ✓ |
| ZW | Zimbabwe | · | ✓ | ✓ |
Accepted emirates
Every emirate field — customer.address.emirate, motor.vehicle.registrationEmirate and motor.licence.issueEmirate — takes one of the values below, spelled exactly as shown. Anything else is rejected with a 400, for every carrier, before any of them is asked.
As with countries, coverage differs per carrier and a place ticked for some but not others is not an error: the carriers that list it quote normally, the one that does not returns a rejected result with CARRIER_UNSUPPORTED_VALUE and appears in blockedCarriers. You only get a 400 when no carrier you requested lists it.
Accepted places — 8 total (DNI 8 · Insurance House 7 · QIC 8). Send the value exactly as written, in customer.address.emirate, motor.vehicle.registrationEmirate and motor.licence.issueEmirate. A carrier that does not list a place is skipped for that request with CARRIER_UNSUPPORTED_VALUE — the others still quote, and the skip is named in blockedCarriers.
| Value | DNI | Insurance House | QIC |
|---|---|---|---|
| Abu Dhabi | ✓ | ✓ | ✓ |
| Dubai | ✓ | ✓ | ✓ |
| Sharjah | ✓ | ✓ | ✓ |
| Ajman | ✓ | ✓ | ✓ |
| Umm Al Quwain | ✓ | ✓ | ✓ |
| Ras Al Khaimah | ✓ | ✓ | ✓ |
| Fujairah | ✓ | ✓ | ✓ |
| Al Ain | ✓ | · | ✓ |
Al Ain is a city inside Abu Dhabi, but DNI and QIC carry it as its own place row and rate against their own lists — so send it as-is when that is where the customer lives or the vehicle is registered. Do not substitute “Abu Dhabi”: that is a different declaration, and it is not what makes Insurance House quote (its own province list has no Al Ain row).
Quote result statuses
Each entry in results[] carries a status. The poll itself is always 200; keep polling until every entry is a settled state (not queued/running).
| status | What it means |
|---|---|
| success | Quote priced. The quote object carries premium, vatAmount, totalAmount and insurerQuoteRef. Normally sellable via the /sell endpoint — but always check quote.sellable: when it is false the price is real yet the carrier will not sell THIS quote. quote.notSellableReason then explains why in plain language and quote.notSellableCode gives the machine-readable reason to branch on (IH_COMPARISON_ONLY / IH_APPLICATION_INCOMPLETE / IH_POLICY_ALREADY_ISSUED / QIC_PREVIOUS_INSURANCE_INVALID / QIC_SCHEME_NOT_COMMITTED — see the poll endpoint for what each one asks you to do). A success may carry quote.reclaimed: true (recovered after the carrier’s website hung post-pricing, with a note in quote.warnings) — its code is usually IH_APPLICATION_INCOMPLETE, or IH_POLICY_ALREADY_ISSUED when the recovered quotation had already become a policy. An IH_APPLICATION_INCOMPLETE quote must NOT be auto-re-quoted: surface it to a human broker. |
| duplicate | This chassis was already quoted at the carrier — no new quote is created. For DNI (and Insurance House’s pre-quote duplicate check) the existing references are in quote.existingQuotations. For Insurance House duplicates detected while completing the application, the existing reference is in results[].error.message instead (error.code IH_DUPLICATE_QUOTE). QIC’s duplicate answer carries NO reference of its own (its check runs before a quotation exists in the session) — so when WE quoted that same vehicle and product at QIC before, we fill in the quotation number from our own history: it appears in quote.insurerQuoteRef and quote.existingQuotations exactly like DNI’s, and quote.priorQuotation = { insurerQuoteRef, quotedAt } tells you it came from our records and when we obtained it. **quotedAt is not a validity date** — we make no claim the quotation is still open at the carrier, so confirm it there. When quote.priorQuotation is absent and no reference is present, that vehicle was quoted at QIC through some other channel and we have nothing to give you: open the QIC portal and search by chassis. In all cases, retrieve the existing quotation rather than re-quoting it. **Treat "duplicate" as POSSIBLE, never guaranteed:** at both DNI and QIC the check is the carrier’s own and is time-windowed, so re-sending the same chassis may answer "duplicate" OR simply mint a fresh quotation depending on how recently it was quoted (observed at DNI: a request deduped against a 2-hour-old quotation of the same vehicle while a 26-hour-old one did not). Do NOT use this status as your own de-duplication: for that, rely on the API’s own idempotency — an identical POST /api/quotes returns 200 with { deduped: true } and the original jobId. |
| referral | The carrier’s underwriter must price this manually. A reference may be issued but there is no automatic price — not auto-sellable. |
| rejected | The carrier declined for a business reason. See error.code / error.message. Re-submitting the same data will not change the outcome. |
| failed | The quote could not be completed. See error.code — many causes are transient and safe to retry. |
| running | Still in progress. Keep polling until every result is one of the settled states above. (The job-level status uses "queued" before work starts and "completed" / "failed" once every run settles — branch the per-result outcome on the values above. "partial" / "skipped" exist in the enum but are not produced by the current carrier path.) |
Quote warnings
A success quote can also carry quote.warnings — an array of plain-language, NON-blocking notes on a REAL, priced quote. A warned quote is not an error. (Whether that quote can be SOLD is a separate signal — see quote.sellable, quote.notSellableReason and quote.notSellableCode.) Never suppress or drop a quote because it carries warnings. Display them to the broker alongside the quote.
| Warning | What it means |
|---|---|
| Declared vehicle value differs from the insured value | The market value sent in the request differs materially from the value the vehicle is actually being insured for. Every carrier values the vehicle itself from the chassis and insures on its OWN figure, so motor.vehicle.marketValueAed never changes the price or the cover. The warning names both numbers and the carrier’s valuation range. Show it to the broker and confirm the cover amount with the customer BEFORE selling — this is the amount that gets paid out on a write-off. |
| Engine size substituted (TPL) | For a Third Party (TPL) quote, the vehicle’s exact engine size was not in the carrier’s allowed list for that model, so the nearest allowed size was declared. The price is unaffected — TPL does not rate on engine size. |
| Engine could not be read (TPL) | For a TPL quote, the engine could not be read from the chassis, so a default engine was declared — the quote may come back as a referral. Verify the vehicle. |
| Colour normalized | The vehicle colour was not in the carrier’s colour list, so the closest match was declared instead. Colour does not affect the premium. |
| Model year read from the chassis | The model year could not be read from the chassis, so the first-registration year was used — which can overstate the vehicle’s age band. Verify the model year. |
| Declared model year disagrees with the chassis | The model year you sent differs from the year the chassis decodes to. The quote still priced; double-check the model year. |
| Seat / door count disagrees with the chassis | The seat or door count you sent differs from the carrier’s own chassis reading; the chassis value was used for the quote. Double-check the count on the registration card. |
| First-registration date looks implausibly recent | The first-registration date is very recent but the model year is older — if the car was first registered earlier, the price may be wrong. Verify the first-registration date. |
| No-Claims Discount not applied | No claim-free-years value was provided, so no No-Claims Discount was applied. Rare now that the field is required — send motor.licence.claimFreeYears to get the discount. |
| Plate code unverified / defaulted | The plate code could not be matched to the carrier’s per-emirate list (or none was sent), so a default plate category was used. Plate code does not affect the premium. |
| Plate category not declared as asked | A motor.vehicle.plateCategory was sent but could not be matched to the carrier’s list for that emirate and cover type, so PRIVATE was declared instead. Only fires when you actually sent a category — omitting the field is silent. |
| GCC-spec defaulted | The GCC-spec status could not be read from the chassis, so the car was declared GCC-spec by default. If it is an imported (non-GCC) car, verify the declaration. |
| Occupation defaulted | The driver’s industry was not in the carrier’s occupation list, so a default occupation was used. It does not change the premium band. |
| Fuel type assumed petrol | The fuel type (e.g. hybrid or electric) could not be represented in the carrier’s list, so the car was rated as petrol. Verify the fuel type before selling. |
| Declared as electric | The chassis decode showed the vehicle is electric (either an explicit electric fuel type, or no fuel type together with a 0-litre / 0-cylinder engine), so it was declared to the carrier as an electric vehicle rather than the previous blanket "not electric". Hybrids are NOT declared electric. If the vehicle is not electric, ask the carrier to correct the quotation. |
| Make matched to the carrier’s catalogue name | The make you sent is spelled differently in the carrier’s own catalogue (e.g. "VW" → "VOLKSWAGEN", "MERC" → "MERCEDES BENZ"), so the carrier’s name was used to look up the model and price the cover. A pure formatting difference is silent — this only appears when the brand is genuinely written another way. Double-check the make on the registration card. |
| Model matched to the nearest catalogue entry | The model you sent was matched to the closest entry in the carrier’s catalogue so the quote could price. Double-check the model. |
| Valuation note | A note about the vehicle valuation (e.g. the carrier could not value the vehicle). These quotes usually come back as a referral rather than a directly-sellable price. |
| Quote recovered after a carrier hang (“reclaimed”) | The carrier’s site hung after it had already priced the quote, and we recovered the priced quotation. The result also sets quote.reclaimed: true — check quote.sellable before selling. |
| Referral — manual approval before issuance | The carrier priced the quote but flagged it for manual underwriter approval before it can be issued. |
Quote error codes
On a rejected or failed result, error.code is a stable string you can branch on. Codes prefixed IH_ are Insurance House specific; treat any unrecognised code as a generic non-success (the detail is in error.message).
| error.code | Retry? | Meaning |
|---|---|---|
| CARRIER_UNSUPPORTED_VALUE | No — not for this carrier | This carrier cannot serve a value the request carries — most often a nationality (or licence issue country) that its own catalogue does not include; also an emirate, a vehicle usage or a product it does not offer. The value is valid; that insurer simply does not cover it, so its result is `rejected` and error.stepFailed names the field. **The other insurers you requested are unaffected and quote normally** — treat this as one carrier declining, not as a failed request. Carrier coverage differs: a country DNI cannot quote is usually still quotable at Insurance House and QIC. One case is not about a catalogue: at QIC, dates showing a gap of 2 days or more between motor.previousPolicyExpiry and motor.coverage.startDate mean the vehicle really was uninsured, and QIC refuses to pay-link such a quote for its whole life — so we skip QIC (field `coverage.startDate`) instead of billing you a quote nobody can use. Correct dates fix that one; a genuine lapse has to go through QIC’s underwriter channel. |
| CARRIER_REQUIREMENT_MISSING | After adding the field | This carrier needs a field your request did not include, so it was skipped — error.stepFailed and error.message name the field. **The other insurers you requested quoted normally**; nothing about this result affects them. Carriers require different things (Insurance House needs the vehicle colour and all three documents; DNI needs the licence traffic-code number; QIC needs the registration emirate, and — unless motor.transactionType is "renewal" — motor.previousPolicyExpiry, because it must declare to the carrier whether the vehicle is already insured), so add the named field to include this carrier next time. Distinct from CARRIER_UNSUPPORTED_VALUE, which you cannot fix by changing the request. |
| CARRIER_PAUSED | Yes — when the insurer resumes | We have temporarily taken this insurer offline, so it was not asked for a quote. Nothing is wrong with your request and nothing is wrong with the customer — this is our side, not yours. **The other insurers you requested quoted normally**, and this result does not affect them in any way. There is no field to add and no value to change: resubmitting the same request while the pause is on produces the same result. The insurer comes back on its own and you will see it quoting again with no change to your integration. The response’s top-level blockedCarriers[] names it with code "CARRIER_PAUSED", field null, fixable false and retryable true. |
| INSURER_INTERNAL_ERROR | Yes — soon | The carrier’s own system errored (e.g. its vehicle-data feed was down). Not the customer’s data. Retry in a few minutes. |
| INSURER_UNAVAILABLE | Yes — soon | The carrier’s site was too slow or unreachable. Usually temporary — retry shortly. Insurance House only: if the hang happened AFTER the quote priced, the result usually comes back as a success with quote.reclaimed: true instead of this failure (see the poll endpoint). |
| NON_GCC | No | Vehicle is not GCC-spec; the carrier will not write comprehensive cover for it. |
| INSERT_QUOTATION_REJECTED | No | The carrier declined at quote creation. The reason is in the message. |
| DNI_VALUE_ABOVE_SCHEME_CAP | After fix | DNI only — DNI valued the vehicle entirely above the AED 250,000 limit of its Comprehensive Standard (65S) scheme, so that scheme cannot cover it. The message quotes the carrier’s valuation range and the limit. **Fix: re-quote the same vehicle with `coverage.schemes: ["60A-S1"]`** — DNI’s Comprehensive High Value scheme, which covers vehicles it values above AED 250,001. The result status is "rejected" (a carrier decline, not a system failure), and the check runs before any quotation is created at DNI, so nothing is left open on their side. Third Party (66S) is unaffected: it does not insure the vehicle’s value. |
| DNI_VALUE_BELOW_SCHEME_MIN | After fix | DNI only — the mirror of DNI_VALUE_ABOVE_SCHEME_CAP. You requested the High Value scheme ("60A-S1") but DNI valued the vehicle entirely below its AED 250,001 minimum. **Fix: re-quote with `coverage.schemes: ["65S"]`** (Comprehensive Standard), which covers that band. The message quotes the carrier’s valuation range and the minimum. Status "rejected" (a carrier decline, not a system failure), and again the check runs before any quotation is created at DNI. The two bands interlock exactly, so every vehicle fits one of the two schemes. |
| QUOTE_CONDITION_FAILED | No | A carrier eligibility condition failed. The failing condition is in the message. |
| QUOTE_BLOCKED | No | The carrier blocked this quote. |
| DNI_DUPLICATE_NO_REF | No | Chassis already quoted, but the carrier returned no reference. Contact support. |
| CHASSIS_DECODE_INCOMPLETE | After fix | For Comprehensive (65S), the carrier’s chassis decoder could not read the passenger or door count. Resubmit with motor.vehicle.numOfPassengers and motor.vehicle.numOfDoors taken from the Mulkiya. |
| VALUATION_UNAVAILABLE | No | Insurance House only — the vehicle isn’t in the carrier’s valuation list (eData), so no sum insured is possible and Comprehensive cannot be quoted automatically. Quote it manually in the portal. The result status is "rejected" (a carrier decline, not a system failure). Third Party has no sum insured, so a request for Third Party ONLY (`coverage.schemes: ["66S"]`) is still attempted and may come back priced; if Insurance House does not price it, the Third Party result carries this code too, with a note that it was rated with no car value. When Comprehensive is requested, both products stop with this code, as before. |
| VEHICLE_DATA_INCOMPLETE | No | Insurance House only — the carrier needs a vehicle detail the chassis decode didn’t provide (seen: weight, for some heavy/commercial vehicles). Quote it manually in the portal. The result status is "rejected" (a carrier decline, not a system failure). |
| IH_COMPLETION_VALIDATION | After fix | Insurance House only — a field was missing/invalid while completing the application (the message names it). Fix the field and re-submit with a fresh vehicle. |
| IH_DOC_REJECTED | After fix | Insurance House only — one of the 3 documents did not get through while completing the application (result status "failed"). Two cases, told apart by the message: (1) Insurance House never confirmed receiving a file — the message names the document and its size (e.g. "did not confirm receiving the Emirates ID file (3.1 MB)"); if the file is large send a smaller scan (Insurance House's limit is 5 MB), otherwise retry. This stops before the application is completed, so the vehicle is not used up. (2) Insurance House refused a document it received — the Driving Licence / Registration Card / Emirates ID usually needs to be a clearer, valid copy. |
| SELL_STAGE_REFERRAL | No | Insurance House only — the application priced but the carrier flagged it for an underwriter referral ("refer to employee"). A person at the carrier must approve it; this result has status "referral". |
| IH_COMPLETION_FAILED | Maybe | Insurance House only — the application could not be completed to a ready-to-pay quotation (e.g. the summary never rendered). Details are in the message. |
| IH_DUPLICATE_QUOTE | No | Insurance House only — this chassis already has a quote/policy at the carrier (detected while completing the application). The result status is "duplicate"; the existing reference is in error.message. Retrieve the existing quotation instead of re-quoting. |
| IH_PLATE_CODE_UNAVAILABLE | After fix | Insurance House only — the carrier does not offer the submitted plate code for the vehicle’s emirate (plate codes are emirate-specific). Caught before any car is used up. Correct motor.vehicle.plateCode, or quote the vehicle manually in the portal. The result status is "rejected" (a carrier decline, not a system failure). |
| VEHICLE_DATA_MISMATCH | Maybe | Insurance House only — a vehicle detail we sent (seats / doors / cylinders) conflicts with the carrier’s own chassis decode (eData). We defer to eData, so a retry usually clears it; if it persists, quote the vehicle manually. The result status is "rejected" (a carrier data decline, not a system failure). |
| POLICY_BLOCKED | No | The carrier flagged the quote for an AML / compliance review at the quote stage — it cannot be sold until they clear it. (A compliance hold detected later, at the payment stage, surfaces on the sale as IH_COMPLIANCE_HOLD.) |
| PRODUCT_NOT_OFFERED | No | The carrier returned no priced plan for this product on this (GCC) vehicle, with no specific decline reason. The other product may still be available. |
| QUOTE_DECLINED | No | The carrier returned no plans together with a clear decline message. The reason is in error.message. |
| IH_QUOTE_FAILED | Maybe | Insurance House only — a quote failure we could not classify more precisely. error.message carries a clean, plain-language summary (never raw internal text or HTML); the full technical detail stays in our logs. This is also the default bucket: treat any unrecognised error.code the same way — a generic non-success, safe to retry. |
| FAST_PATH_FAILED | Maybe | An unexpected error during quoting. Details are in the message. |
| QIC_QUOTE_FAILED | Maybe | QIC (Qatar Insurance) only — a quote failure we could not classify more precisely. error.message carries a clean, plain-language summary (never raw internal text); the full detail stays in our logs. This is also QIC’s default bucket: treat any unrecognised QIC error.code the same way — a generic non-success, safe to retry. |
| QIC_WAF_BLOCKED | Yes — soon | QIC (Qatar Insurance) only — QIC’s own security layer blocked one call in the middle of the quote. Nothing is wrong with the request or the customer’s data — the blocked call never reached QIC’s application. We already retried it once automatically; submit the same quote again after a short wait and it normally goes straight through. |
| QIC_PASSWORD_RESET_REQUIRED | Later | QIC (Qatar Insurance) only — the carrier forced a password reset on the broker’s agent account. QIC quoting resumes once the broker completes the reset; retry later. |
| PORTAL_AUTH_REFUSED | Yes — when the insurer resumes | The insurer refused our sign-in (status "failed"). Our side, not your request or the customer: we stop the insurer at once and take it offline, so from the next request it is skipped as CARRIER_PAUSED until we fix the sign-in. The other insurers quote normally; resubmit once it is back. |
| SESSION_RESEED_REQUIRED | Later | Insurance House only — the carrier session expired and could not be renewed automatically. Transient; retry shortly. |
| SESSION_NOT_SEEDED | Later | Insurance House only — no carrier session is available yet. Transient operator setup; retry shortly. |
Sale error codes
When a sale attempt fails, GET /api/quotes/:jobId/sale returns an error object. These are the payment-stage codes — the key one is IH_COMPLIANCE_HOLD (a permanent, customer-specific hold) versus the retryable ones.
| error.code | Retry? | Meaning |
|---|---|---|
| IH_COMPLIANCE_HOLD | No | Insurance House only — the carrier placed THIS CUSTOMER on a compliance / AML hold at the payment gate. No payment link is sent. It is specific to the customer (other customers sell normally) and cannot be bypassed — the carrier’s Compliance team must clear them. |
| SELL_STAGE_REFERRAL | No | The application priced but the carrier flagged it for an underwriter referral at the payment stage. A person at the carrier must approve it before a link can be sent. |
| IH_SELL_EID_INVALID | After fix | Insurance House only — the carrier did not accept the Emirates ID at the payment gate. Re-check the Emirates ID before retrying. (Rare — most sells pass this.) |
| IH_SELL_IDENTITY_MISMATCH | No | Insurance House only — the saved quote being sold is registered to a different customer than the one on file, so no payment link was sent (a data-safety refusal against selling the wrong customer’s quote). Check the quote reference, or re-quote the vehicle for the correct customer. |
| ALREADY_ISSUED | No | The quote is already an issued policy at the carrier. Retrieve the policy (GET /policy / POST /refresh-policy) instead of re-sending a payment link. |
| QUOTE_MONEY_INCONSISTENT | No | status `failed` — the stored quote’s own figures do not add up (premium + VAT does not equal the total), so no payment link was sent. This is a safety refusal: we will not stamp a price onto the carrier’s record that the quote does not support. Re-run the quote for this vehicle; if it recurs, report the quote reference. |
| DNI_SCHEME_SELL_NOT_ENABLED | No | **409 from POST /sell, not a sale-attempt status** — the quote is valid and priced, but selling this DNI product is not enabled through the API yet. Today that means the Comprehensive High Value scheme ("60A-S1"), which is quote-only: DNI publishes the plan a payment link must be issued against only once a real quote of that product exists, and we will not guess it onto a regulated record. Nothing was contacted at the carrier and no sale attempt is created, so there is nothing to clean up. Issue the policy for this quote through the DNI portal, or quote "65S" / "66S" if the vehicle qualifies. Retrying will not help — contact support if you need it enabled. |
| POLL_GAVE_UP | No | Appears WITH status `payment-link-sent` (NOT `failed`) — the automatic 24-hour policy tracker stopped, but the payment link is still LIVE and the customer can still pay. Do NOT treat this as a failed sale. Retrieve a late-issued policy at any time with POST /refresh-policy. |
| POLICY_CANCELLED | No | status `failed` — after the link was sent, the carrier’s policy landed on the terminal negative status Cancelled. The sale did not complete; investigate with the carrier before re-selling. |
| POLICY_LAPSED | No | status `failed` — the carrier’s policy lapsed (expired without being paid) before issuance. Treat the customer as uninsured. |
| POLICY_REJECTED | No | status `failed` — the carrier refused to issue the policy after payment (rare; usually a data issue surfaced late). Review the rejection reason with the carrier. |
| IH_QUOTE_INCOMPLETE | After fix | Insurance House only — the saved quote was only rated, not fully completed, so it cannot be sold yet. Re-run the quote for this vehicle (which completes the application), then send the payment link. |
| INSURER_UNAVAILABLE | Yes — soon | The carrier was too slow or busy to load the payment page. Temporary — please retry shortly. |
| PORTAL_AUTH_REFUSED | Yes — when the insurer resumes | status `failed` — the insurer refused our sign-in, so no payment link was sent. This is our side, not your request and not the customer: we stop the insurer at once and take it offline until we have fixed the sign-in. The quote itself is still good — keep it, and send the payment link again once the insurer is back (until then POST /sell answers 503 CARRIER_PAUSED). |
| IH_REJECT | Maybe | Insurance House only — the payment gate rejected the sale for a reason we could not classify more precisely. The detail is in error.message. |
| DNI_REJECT | Maybe | DNI only — the carrier rejected the payment-link send. The reason is in error.message. |
| DNI_MODEL_YEAR_MISMATCH | After fix | DNI only — the saved quote was priced under a different model year than the sell would declare, so no payment link was sent (the rating basis is protected). Resubmit the quote with motor.vehicle.year set to the year named in error.message, or ask DNI to correct the quotation. |
| DNI_IDENTITY_MISMATCH | No | DNI only — the carrier returned a different quotation than the one being sold, so no payment link was sent (a data-safety refusal). Contact support before retrying. |
| QIC_REJECT | Maybe | QIC (Qatar Insurance) only — the carrier declined to send the payment link for a reason we could not classify more precisely. The detail is in error.message. |
| QIC_PREVIOUS_INSURANCE_INVALID | After fix — see below | QIC only — this quote declared the vehicle as NOT previously insured, and QIC refuses payment links for such quotes. The declaration is written when the quote is CREATED and cannot be repaired on the existing quote, so re-quoting the same vehicle with the SAME dates simply reproduces it: a re-quote only helps if the dates change. The vehicle counts as previously insured when the new policy starts no later than the day AFTER motor.previousPolicyExpiry — so for a seamless renewal set motor.coverage.startDate to that day (leaving the start date to default to today, while the previous expiry stays fixed, is what turns a renewal into a gap). If the vehicle genuinely was uninsured between the two dates, no data change makes it sellable — that case has to go through QIC’s underwriter channel. You will normally meet this long BEFORE selling now, in two earlier layers. Since 2026-08-17 the dates are checked in your REQUEST: QIC is skipped at preflight (both products) and named in the response’s blockedCarriers with CARRIER_UNSUPPORTED_VALUE on coverage.startDate, so no unsellable quote is produced at all. For quotes created before that, the quote itself comes back with quote.sellable false, quote.notSellableCode QIC_PREVIOUS_INSURANCE_INVALID and a plain-language quote.notSellableReason, and POST /sell rejects it instantly with 409 without contacting the carrier. The same code on a failed sale attempt means QIC itself refused — a quote older than both pre-checks. |
| QIC_QUOTE_EXPIRED | After fix | QIC only — this quote is no longer valid at Qatar Insurance (the quotation expired at the carrier). Re-quote the vehicle before sending the payment link. |
| QIC_NO_HANDLE | After fix | QIC only — the saved quote is missing the carrier transaction handle needed to send its payment link (e.g. a referral quote, which QIC does not make sellable). Re-quote the vehicle to a priced, sellable quote before selling. |
| INTERNAL | Yes — soon | An unexpected server error during the sale (not a carrier decline). Safe to retry — the send is idempotent, so a retry never double-emails the customer. |
Policy statuses
The issued policy’s coreStatus from GET /api/quotes/:jobId/policy. Auto-polling refreshes it until a terminal state (Approved / Cancelled / Lapsed / Rejected).
| coreStatus | What it means |
|---|---|
| Approved | Policy issued and synced to RTA (the UAE regulator). The customer is insured. Download the Schedule PDF for the proof-of-insurance document. |
| Pending Approval | The carrier is still processing. Usually minutes after payment — the auto-poll keeps /policy updated until it flips to Approved. Note the auto-poll stops after 24h; if the carrier issues later (often days after payment), trigger POST /refresh-policy to update it (DNI: paste the CRS number; Insurance House: no number needed). |
| Cancelled | The policy was cancelled after issuance (rare). The broker should investigate with the carrier before re-selling. |
| Lapsed | The policy expired without being paid. Treat the customer as uninsured. |
| Rejected | The carrier refused to issue the policy after payment (rare; usually a data issue surfaced late). The broker should review the rejection reason. |
| unknown | The carrier returned a status string we have not catalogued yet. Check `coreStatusRaw` in the response for the verbatim value and contact support — we will add the mapping. |
HTTP / transport errors
Returned as the HTTP status of the request itself (rejected before any quote runs) — distinct from the per-quote outcomes above.
| Code | HTTP | Description |
|---|---|---|
| VALIDATION_FAILED | 400 | Request failed validation. The body carries fieldErrors[] — every problem at once, each with a path + message in the field names used here, plus insurerId when exactly one carrier raised it (omitted = several/all). Fix them all and resubmit. A field only SOME of your carriers require does NOT land here: those carriers are skipped with CARRIER_REQUIREMENT_MISSING and the rest quote. You see this 400 for a malformed value, or when the missing field is required by every carrier you requested. |
| FILE_CORRUPT | 400 | An uploaded document could not be read. The body names the file; re-upload a clean copy. |
| NO_CARRIER_SUPPORTS_VALUE | 400 | The request is well-formed, but NOT ONE of the insurers you requested can serve this customer — e.g. a nationality none of their catalogues carry. fieldErrors[] names what each could not accept, with insurerId per entry. Nothing is wrong with your request; this customer cannot be quoted by the carriers you chose. (If only SOME carriers cannot serve the value, you do not get this error — the others quote and the rest return a rejected result. See CARRIER_UNSUPPORTED_VALUE.) When the reason every carrier dropped out is a MISSING field rather than an unservable value, you get VALIDATION_FAILED listing those fields instead — because that one you can fix. And if any of the carriers dropped out because WE paused it, you get 503 NO_CARRIER_AVAILABLE instead — that one is ours, not yours. |
| NO_CARRIER_AVAILABLE | 503 | Every insurer you requested is currently unavailable, and at least one of them is PAUSED on our side. This is our outage, not a problem with your request — the very same request will work once the insurer resumes. blockedCarriers[] in the body names each one and why. Respect the Retry-After header, or request an insurer that is still running. No job is created, so there is nothing to poll. (This is deliberately a 503 and not a 400: a 400 would mean you had sent us something wrong, and you had not.) |
| UNAUTHORIZED | 401 | Missing or invalid x-api-key header. |
| CONFLICT | 409 | The request conflicts with the current state — e.g. a sale is already in progress/sent for this quote (SALE_ATTEMPT_EXISTS), the quote is not sellable (QUOTE_NOT_SELLABLE), the quote has no price (QUOTE_NOT_PRICED), or a QIC quote declared the vehicle as not previously insured so the carrier will never pay-link it (QIC_PREVIOUS_INSURANCE_INVALID). The specific code is in the body. NOTE: a sale refused because the insurer is PAUSED on our side is a 503 CARRIER_PAUSED, not a 409 — the quote is still perfectly good and must NOT be discarded; retry the sale when the insurer resumes. NOTE: a duplicate quote submission does NOT 409 — an identical POST /api/quotes returns 200 with { deduped: true } and the existing jobId. |
| RATE_LIMITED | 429 | Too many requests. Wait and retry after the window resets. |
| SERVICE_UNAVAILABLE | 503 | The service could not start the requested workflow (quote or policy retrieval) — usually a brief engine hiccup. Retry shortly. |
| INTERNAL | 500 | Unexpected server error. Retry; contact support if it persists. |