rpa-api

Insurance Broker API — API Reference

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.

1POST /api/quotes

Submit the customer intake + documents. You get back a jobId.

2GET /api/quotes/:jobId

Poll until settled, then read the premium on each result.

3POST /api/quotes/:jobId/sell

After the broker approves, send the payment link.

4GET /api/quotes/:jobId/policy

Retrieve 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.

x-api-key: YOUR_API_KEY

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.

DNIInsurance HouseQIC
targetInsurers value"dni""insurancehouse""qic"
ProductsOne per scheme: 65S Comprehensive, 66S TPLComprehensive + TPL priced together from one submitOne per scheme: 65S Comprehensive, 66S TPL
coverage.schemes to sendChoose ["65S"] and/or ["66S"]Send ["65S","66S"] — both are returnedChoose ["65S"] and/or ["66S"]
Result rows (results[])One per scheme you requestedAlways 2 (Comprehensive + TPL)One per scheme you requested
Typical quote time~20–30 s~30–70 s~10–30 s
Policy retrievalPaste the CRS policy number (crsPolNo)Automatic — no number needed (auto-derived)Automatic — no number needed (auto-derived)
Policy PDFs returnedSchedule + JacketSchedule + Tax InvoiceSchedule + Receipt
Pay-link recipientBroker’s central eSanad inbox — never the customer directlyBroker’s central eSanad inbox — never the customer directlyBroker’s central eSanad inbox — never the customer directly
NotesEchoes quote.placeOfRegistration; 65S returns a valuation block (none on 66S/TPL).One submit prices both products; no valuation block; 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.

ScopeLimitApplies to
Quote submission10 / minPOST /api/quotes
Sell / payment link5 / minPOST /api/quotes/:jobId/sell
Policy refresh10 / minPOST /api/quotes/:jobId/refresh-policy
Global100 / minAll endpoints combined

Endpoints

The full quote → sell → policy loop. Every endpoint is identical across carriers.

GET/healthPublic

Health 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

200Reachable — status is "ok" (all up) or "degraded" (engine down, reads still work)
503Database is down — status is "down"

Request

GET /health
curl https://api.rpai.vionark.com/health

Response

200 OK
{
"status": "ok",
"service": "rpa-api",
"version": "0.0.0",
"timestamp": "2026-05-26T12:00:00.000Z",
"database": "up",
"engine": "up"
}
POST/api/quotesAuth required

Submit 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

FieldTypeRequiredDescription
Customer
customer.fullNamestringyesFull name as on Emirates ID
customer.emiratesIdstringyesEmirates ID (format: 784-YYYY-NNNNNNN-C)
customer.emiratesIdExpirystringyesEID expiry date (YYYY-MM-DD)
customer.dobstringyesDate of birth (YYYY-MM-DD). Used for pricing — must be the real DOB.
customer.nationalitystringyesISO 3166-1 alpha-2 country code (e.g. "IN", "AE", "PH"). Used for pricing.
customer.genderstringyes"Male" or "Female"
customer.mobileNumberstringyesUAE 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.emailstringyesCustomer 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.nameInArabicstringnoCustomer name in Arabic. Pushed to the carrier as-is when provided.
Vehicle
motor.vehicle.chassisNumberstringyes17-character VIN / chassis number
motor.vehicle.plateNumberstringyesPlate number
motor.vehicle.registrationEmiratestringyesPlace of registration — the emirate printed on the registration card / Mulkiya (e.g. "Dubai", "Abu Dhabi"). Sent to the carrier; on DNI it is echoed back as quote.placeOfRegistration (Insurance House and QIC do not echo it).
motor.vehicle.firstRegistrationDatestringyesFirst registration date from the Mulkiya (YYYY-MM-DD)
motor.vehicle.plateCodestringnoPlate code (e.g. "12", "A")
motor.vehicle.colorstringnoVehicle colour (e.g. "WHITE")
motor.vehicle.engineNumberstringnoEngine number from the Mulkiya
motor.vehicle.makestringnoVehicle make. Required ONLY for TPL (66S); for Comprehensive (65S) it is read from the chassis.
motor.vehicle.modelstringnoVehicle model. Required ONLY for TPL (66S).
motor.vehicle.yearnumbernoModel year. Required ONLY for TPL (66S).
motor.vehicle.numOfPassengersnumbernoPassenger 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.numOfDoorsnumbernoDoor count. Same rule as numOfPassengers.
motor.vehicle.registrationTcNumberstringnoTraffic 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.tcNumberSourcestringnoWhich 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.numberstringyesDriving licence number
motor.licence.tcNumberstringyesTraffic-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.expiryDatestringyesLicence expiry date (YYYY-MM-DD)
motor.licence.issueDatestringyesLicence first-issue date (YYYY-MM-DD). Used for pricing.
motor.licence.issueCountrystringyesLicence issuing country, ISO alpha-2 (e.g. "AE")
motor.licence.yearsLicensednumberyesYears the driver has held a licence. Used for pricing — must be accurate.
motor.licence.claimFreeYearsnumberyesConsecutive claim-free INSURED years — how many years in a row the customer has held motor insurance without a claim. Required for DNI. 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. An accident in the last 3 years overrides to no discount.
motor.licence.hasAccidentsLast3YearsbooleanyesWhether the driver had accidents in the last 3 years. Used for the no-claims tier.
motor.licence.issueEmiratestringnoEmirate the licence was issued in (defaults to the registration emirate)
Coverage & carriers
motor.previousPolicyExpirystringyesPrevious policy expiry date (YYYY-MM-DD). Required — never assumed.
motor.transactionTypestringno"New", "Renewal", or "Transfer"
coverage.schemesstring[]yesScheme codes: "65S" (Comprehensive) and/or "66S" (TPL)
targetInsurersstring[]noInsurer IDs to quote, run in parallel. Allowed: "dni", "insurancehouse", "qic". Defaults to ["dni"]. Note: "insurancehouse" prices Comprehensive + Third Party together from ONE submit, so a single request returns TWO result entries (schemeCode "IH-COMP" and "IH-TPL") regardless of coverage.schemes. 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; and (b) completes ONLY ONE product (the one chosen via coverage.schemes — "66S" alone → TPL, otherwise Comprehensive). That chosen result has quote.sellable=true (the others are price-only, sellable=false). 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".

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.

Status codes

202New (or retried) request accepted — body { jobId, status: "running" }
200Duplicate of an existing request — body { jobId, status, deduped: true }; reuses the existing job
400Validation failed — body { error: "VALIDATION_FAILED", message, fieldErrors: [{ path, message }] } lists every problem at once. (Also FILE_CORRUPT for an unreadable upload.)
401Missing or invalid API key
429Rate limit exceeded (max 10/min)
503Could not start the quote workflow (engine briefly unavailable) — body includes the jobId; retry shortly

Request

POST /api/quotes
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-06-01","transactionType":"New"},"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

202 Accepted
{
"jobId": "7f3a1c2e-9b4d-4e1a-8c2f-1a2b3c4d5e6f",
"status": "running"
}
GET/api/quotes/:jobIdAuth required

Poll 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 }. 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 (65S), DNI and QIC also return quote.valuation = { low, medium, high, chosenSumInsured, currency } — the car-value range the carrier fetched plus the midpoint we insured on. Both are display-only; valuation is absent for TPL (66S).

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.

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 — re-run the quote for this vehicle to complete it (a POST /sell on it returns 409 QUOTE_NOT_SELLABLE). Only when the recovery is impossible (the carrier is truly down) does the run stay failed with error.code: "INSURER_UNAVAILABLE".

Status codes

200Job found — read job.status, then results[].status
400Invalid jobId format
404Job not found
401Missing or invalid API key

Request

GET /api/quotes/:jobId
curl https://api.rpai.vionark.com/api/quotes/7f3a1c2e-9b4d-4e1a-8c2f-1a2b3c4d5e6f \
-H "x-api-key: YOUR_API_KEY"

Response

200 OK
{
"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-4262234",
"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": 41772,
"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
}
}
]
}
POST/api/quotes/:jobId/sellAuth required

Send 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

FieldTypeRequiredDescription
quoteRunIdstringyesID of the specific quote run to sell
customerEmailstringyesCustomer 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.
brokerConsentGivenbooleanyesMust be true — broker confirms they reviewed the quote
brokerConsentAtstringyesISO 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

202Sale attempt accepted — body { saleAttemptId, status, quotationRef }. status starts as "pending" (queued, not yet sent); poll GET /sale for the transition to "payment-link-sent" or "failed".
400Invalid fields; customerEmail does not match the customer on file (EMAIL_MISMATCH); brokerConsentAt is malformed (CONSENT_TIMESTAMP_INVALID), older than 30 minutes (CONSENT_STALE), or too far in the future — your server clock is ahead of ours, sync NTP (CONSENT_CLOCK_DRIFT). A caller clock up to 2 minutes ahead is tolerated.
401Missing or invalid API key
403Job belongs to another broker (multi-broker only)
404Job or quote run not found
409Cannot sell in current state: the quote is not a sellable success (QUOTE_NOT_SELLABLE) — this also blocks an Insurance House product that was priced for comparison only (quote.sellable === false; sell the completed product instead) and a RECLAIMED quote whose application never finished on the carrier (quote.reclaimed === true with sellable false; re-run the quote to complete it); the quote has no quotation reference (QUOTE_REF_MISSING); the quote came back with no price / 0 AED and cannot be sold (QUOTE_NOT_PRICED); or a sale is already in progress / sent for this quote — double-click guard (SALE_ATTEMPT_EXISTS).
429Rate limit exceeded (max 5/min)
503Could not start the sell workflow — retry shortly

Request

POST /api/quotes/:jobId/sell
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

202 Accepted
{
"saleAttemptId": "4a1b2c3d-1111-4e1a-8c2f-1a2b3c4d5e70",
"status": "pending",
"quotationRef": "MT-2026-4262234"
}
GET/api/quotes/:jobId/saleAuth required

Poll 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: pendingpayment-link-sentpolicy-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

200Returns { saleAttempt } — or { saleAttempt: null } if no sell started
400Malformed quoteRunId (BAD_QUOTE_RUN_ID)
401Missing or invalid API key
404Job not found

Request

GET /api/quotes/:jobId/sale
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

200 OK
{
"saleAttempt": {
"id": "4a1b2c3d-1111-4e1a-8c2f-1a2b3c4d5e70",
"jobId": "7f3a1c2e-9b4d-4e1a-8c2f-1a2b3c4d5e6f",
"quoteRunId": "8f3a1c2e-aaaa-4e1a-8c2f-1a2b3c4d5e60",
"insurerId": "dni",
"quotationRef": "MT-2026-4262234",
"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"
}
}
POST/api/quotes/:jobId/refresh-policyAuth required

Refresh 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

FieldTypeRequiredDescription
quoteRunIdstring (UUID)yesID of the specific quote run that produced the policy. Find it in the `results[]` array of `GET /api/quotes/:jobId`.
crsPolNostringnoDNI 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/11784`. The scheme segment accepts letters + hyphens so future scheme codes (66S, 65-CH, ...) round-trip without an update.
saleAttemptIdstring (UUID)noSet 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/11784), 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/15582 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

202Retrieval started — poll GET /policy for the result
400Bad jobId / quoteRunId / crsPolNo format (see error code in the body)
401Missing or invalid API key
403Job belongs to another broker (multi-broker only)
404Job or quote_run not found
429Rate limit exceeded (max 10/min)
503Could not start the retrieval — retry shortly

Request

POST /api/quotes/:jobId/refresh-policy
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/11784","saleAttemptId":"sale_abc123"}'

Response

202 Accepted
{
"workflowId": "policy-123e4567-e89b-12d3-a456-426614174000-abc123",
"quoteRunId": "123e4567-e89b-12d3-a456-426614174000",
"crsPolNo": "02/601/65S/2026/11784",
"status": "running"
}
GET/api/quotes/:jobId/policyAuth required

Get policy

Poll the latest retrieved policy. Returns { policy, policies } with coreStatus and 5-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 5 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

200Returns { policy, policies }. Before any retrieval: { policy: null, policies: [] }
400Bad jobId format
401Missing or invalid API key
404Job not found

Request

GET /api/quotes/:jobId/policy
curl https://api.rpai.vionark.com/api/quotes/7f3a1c2e-9b4d-4e1a-8c2f-1a2b3c4d5e6f/policy \
-H "x-api-key: YOUR_API_KEY"

Response

200 OK
{
"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/11784",
"pmtPolNo": "PMT02202632186",
"quotationRef": "MT-2026-4356083",
"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/PMT022...pdf",
"schedulePdfSignedUrl": "https://rpa-docs.s3.me-central-1.amazonaws.com/policies/2026/06/.../schedule-...uuid.pdf?X-Amz-Expires=300&...",
"jacketPdfSignedUrl": "https://rpa-docs.s3.me-central-1.amazonaws.com/policies/2026/06/.../jacket-...uuid.pdf?X-Amz-Expires=300&...",
"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": "MAHA EBRAHIM",
"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/11784",
"pmtPolNo": "PMT02202632186",
"quotationRef": "MT-2026-4356083",
"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/PMT022...pdf",
"schedulePdfSignedUrl": "https://rpa-docs.s3.me-central-1.amazonaws.com/policies/2026/06/.../schedule-...uuid.pdf?X-Amz-Expires=300&...",
"jacketPdfSignedUrl": "https://rpa-docs.s3.me-central-1.amazonaws.com/policies/2026/06/.../jacket-...uuid.pdf?X-Amz-Expires=300&...",
"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": "MAHA EBRAHIM",
"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/15582",
"pmtPolNo": "797026",
"quotationRef": "Q/MOT/645589",
"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=300&...",
"invoicePdfSignedUrl": "https://rpa-docs.s3.me-central-1.amazonaws.com/policies/2026/06/.../invoice-...uuid.pdf?X-Amz-Expires=300&...",
"customerSnapshot": {
"fullName": "AHMED HASSAN",
"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"
}
]
}

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).

statusWhat it means
successQuote priced. The quote object carries premium, vatAmount, totalAmount and insurerQuoteRef. Sellable via the /sell endpoint. A success may carry quote.reclaimed: true (recovered after the carrier’s website hung post-pricing, with a note in quote.warnings) — check quote.sellable before selling: false means the application didn’t finish, so re-run the quote to complete it.
duplicateThis 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). For QIC, the duplicate is detected before a reference exists, so it carries no reference — see the plain-language note in quote.warnings and open the existing quotation in the QIC portal. In all cases, retrieve the existing quotation rather than re-quoting.
referralThe carrier’s underwriter must price this manually. A reference may be issued but there is no automatic price — not auto-sellable.
rejectedThe carrier declined for a business reason. See error.code / error.message. Re-submitting the same data will not change the outcome.
failedThe quote could not be completed. See error.code — many causes are transient and safe to retry.
runningStill 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 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.codeRetry?Meaning
INSURER_INTERNAL_ERRORYes — soonThe carrier’s own system errored (e.g. its vehicle-data feed was down). Not the customer’s data. Retry in a few minutes.
INSURER_UNAVAILABLEYes — soonThe 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_GCCNoVehicle is not GCC-spec; the carrier will not write comprehensive cover for it.
INSERT_QUOTATION_REJECTEDNoThe carrier declined at quote creation. The reason is in the message.
QUOTE_CONDITION_FAILEDNoA carrier eligibility condition failed. The failing condition is in the message.
QUOTE_BLOCKEDNoThe carrier blocked this quote.
DNI_DUPLICATE_NO_REFNoChassis already quoted, but the carrier returned no reference. Contact support.
CHASSIS_DECODE_INCOMPLETEAfter fixFor 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_UNAVAILABLENoInsurance House only — the vehicle isn’t in the carrier’s valuation list (eData), so no sum insured (and no automated quote) is possible. Quote it manually in the portal. The result status is "rejected" (a carrier decline, not a system failure).
VEHICLE_DATA_INCOMPLETENoInsurance 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_VALIDATIONAfter fixInsurance 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_REJECTEDAfter fixInsurance House only — the carrier did not accept one of the 3 uploaded documents. Re-upload a clear, valid Driving Licence / Registration Card / Emirates ID.
SELL_STAGE_REFERRALNoInsurance 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_FAILEDMaybeInsurance 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_QUOTENoInsurance 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_UNAVAILABLEAfter fixInsurance 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_MISMATCHMaybeInsurance 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.
POLICY_BLOCKEDNoThe 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_OFFEREDNoThe 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_DECLINEDNoThe carrier returned no plans together with a clear decline message. The reason is in error.message.
IH_QUOTE_FAILEDMaybeInsurance 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_FAILEDMaybeAn unexpected error during quoting. Details are in the message.
QIC_QUOTE_FAILEDMaybeQIC (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_PASSWORD_RESET_REQUIREDLaterQIC (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.
SESSION_RESEED_REQUIREDLaterInsurance House only — the carrier session expired and could not be renewed automatically. Transient; retry shortly.
SESSION_NOT_SEEDEDLaterInsurance 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.codeRetry?Meaning
IH_COMPLIANCE_HOLDNoInsurance 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_REFERRALNoThe 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_INVALIDAfter fixInsurance 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.)
ALREADY_ISSUEDNoThe quote is already an issued policy at the carrier. Retrieve the policy (GET /policy / POST /refresh-policy) instead of re-sending a payment link.
POLL_GAVE_UPNoAppears 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_CANCELLEDNostatus `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_LAPSEDNostatus `failed` — the carrier’s policy lapsed (expired without being paid) before issuance. Treat the customer as uninsured.
POLICY_REJECTEDNostatus `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_INCOMPLETEAfter fixInsurance 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_UNAVAILABLEYes — soonThe carrier was too slow or busy to load the payment page. Temporary — please retry shortly.
IH_REJECTMaybeInsurance House only — the payment gate rejected the sale for a reason we could not classify more precisely. The detail is in error.message.
DNI_REJECTMaybeDNI only — the carrier rejected the payment-link send. The reason is in error.message.
DNI_MODEL_YEAR_MISMATCHAfter fixDNI 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_MISMATCHNoDNI 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_REJECTMaybeQIC (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_INVALIDAfter fixQIC only — the carrier refused the payment link because this quote declared the vehicle as NOT previously insured. That flag is set when the quote is created and cannot be changed on the existing quote — re-quote the vehicle (with its previous-policy details) before selling.
QIC_QUOTE_EXPIREDAfter fixQIC 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_HANDLEAfter fixQIC 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.
INTERNALYes — soonAn 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).

coreStatusWhat it means
ApprovedPolicy issued and synced to RTA (the UAE regulator). The customer is insured. Download the Schedule PDF for the proof-of-insurance document.
Pending ApprovalThe 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).
CancelledThe policy was cancelled after issuance (rare). The broker should investigate with the carrier before re-selling.
LapsedThe policy expired without being paid. Treat the customer as uninsured.
RejectedThe carrier refused to issue the policy after payment (rare; usually a data issue surfaced late). The broker should review the rejection reason.
unknownThe 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.

CodeHTTPDescription
VALIDATION_FAILED400Request failed validation. The body carries fieldErrors[] — every problem at once, each with a path + message in the field names used here. Fix them all and resubmit.
FILE_CORRUPT400An uploaded document could not be read. The body names the file; re-upload a clean copy.
UNAUTHORIZED401Missing or invalid x-api-key header.
CONFLICT409The 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), or the quote has no price (QUOTE_NOT_PRICED). The specific code is in the body. NOTE: a duplicate quote submission does NOT 409 — an identical POST /api/quotes returns 200 with { deduped: true } and the existing jobId.
RATE_LIMITED429Too many requests. Wait and retry after the window resets.
SERVICE_UNAVAILABLE503The service could not start the requested workflow (quote or policy retrieval) — usually a brief engine hiccup. Retry shortly.
INTERNAL500Unexpected server error. Retry; contact support if it persists.