Webhooks
Get notified in your system every time a product is quoted, issued or paid, signed so you can verify it comes from Quore.
Setup
- Configured per product: one URL for policies and another for quotes, each with its signing secret.
- Like the rest of the product, they apply once the version is published.
- They are sent for every channel: API, widget, intermediary portal and client portal.
Events
| Event | URL | When it is sent |
|---|---|---|
policy.created | Policies | When a policy is issued, through any channel. |
policy.status_changed | Policies | When a pending policy changes status: pending_payment (conditions approved, with payment_url to pay), active or rejected, when its conditions are resolved, its payment is confirmed or it expires. |
payment.succeeded | Policies | When a policy's single online payment is confirmed (Stripe, Mercado Pago or Rebill). |
payment.failed | Policies | When a collection attempt of a policy waiting for payment is declined: a single payment, the first period of a recurring product or the card validation of a usage one. Once per attempt (repeated provider events for the same attempt aren't resent) and includes failure_reason. |
payment.refund_required | Policies | Only older policies paid before their conditions were resolved: rejected after being paid (or paid after being rejected), they need a manual refund. |
payment.refunded | Policies | When an admin marks that refund as done. |
quote.created | Quotes | When a quote is priced. |
quote.pending_approval | Quotes | When a quote that requires approval is priced. |
quote.approved | Quotes | When a pending quote is approved. |
quote.rejected | Quotes | When a pending quote is rejected (includes reason). |
Payload
Every event is sent as a JSON POST: event, timestamp (ISO 8601) and the record (policy or quote; payment events add payment). policy.created carries the full policy:
policy.created
{
"event": "policy.created",
"timestamp": "2026-10-26T14:03:11.512Z",
"policy": {
"id": "b2a7…",
"policy_number_internal": "POL-0042",
"policy_number_insurer": null,
"status": "active",
"product_id": "PRODUCT_ID",
"coverage_id": "basica",
"quote_id": "QUOTE_ID",
"data": {
"insured": {
"nombre": "Ana Pérez"
},
"payment": {
"method": "credit_card",
"card_last4": "4242",
"card_brand": "visa",
"card_holder": "Ana Pérez"
}
},
"conditions": [],
"net_premium": "42000",
"total_premium": "50820",
"currency": "ARS",
"tax_breakdown": [
{
"id": "iva",
"name": "IVA",
"amount": 8820
}
],
"installments": 12,
"installment_amount": "4235",
"commissions": {
"commission": {
"tierId": "TIER_ID",
"tierName": "Producer",
"type": "percentage",
"rate": 0.1,
"amount": 4200,
"affectsPricing": false
},
"ancestorCommissions": []
},
"environment": "live",
"payment_method_status": null,
"operator_id": "USER_ID",
"start_date": "2026-10-26T00:00:00.000Z",
"end_date": "2027-10-26T00:00:00.000Z",
"created_at": "2026-10-26T14:03:11.402Z",
"updated_at": "2026-10-26T14:03:11.402Z"
}
}| Field | Content |
|---|---|
data | Issuance data by group (insured, asset, coverage, general) and the masked payment method (payment: method, card_last4, card_brand, card_holder, provider). |
net_premium | Net premium (before taxes). |
total_premium | Total premium (amount to pay: premium, taxes and financing surcharge). |
tax_breakdown | Applied taxes: a list of { id, name, amount }. |
commissions | Commissions computed at quote time, or null: commission (tierId, tierName, type, rate, amount, affectsPricing) and ancestorCommissions (same shape plus intermediaryId and levelName, nearest ancestor first). |
operator_id | User who issued it (intermediary or operator), or null when issued through the API or the public widget. |
payment_method_status | Recurring or usage billing: pending until the insured saves a payment method, then active; null when not applicable. The policy activates with its first charge confirmed (recurring) or its card validated (usage). |
environment | live or test. |
In policy.created the amounts (net_premium, total_premium, installment_amount) arrive as decimal strings (for example "42000"); convert them to numbers when processing.
The full card number is never sent, unless the product explicitly enables card forwarding in manual payment mode: then it arrives only in policy.created, separately, in payment_pan.card_number, and is never stored. The CVV is never requested, stored or sent.
policy.created with card forwarding enabled
{
"event": "policy.created",
"timestamp": "2026-10-26T14:03:11.512Z",
"policy": {
"id": "b2a7…",
"…": "…"
},
"payment_pan": {
"card_number": "4242424242424242"
}
}policy.status_changed
{
"event": "policy.status_changed",
"timestamp": "2026-10-27T09:12:40.010Z",
"policy": {
"id": "b2a7…",
"policy_number_internal": "POL-0043",
"status": "active",
"conditions": [
{
"id": "inspection",
"type": "inspection",
"label": "Inspection",
"status": "approved"
}
],
"payment_status": "paid",
"pending_reason": null,
"refund_status": null,
"updated_at": "2026-10-27T09:12:39.981Z"
}
}payment.succeeded
{
"event": "payment.succeeded",
"timestamp": "2026-10-26T14:05:02.300Z",
"policy": {
"id": "b2a7…",
"policy_number_internal": "POL-0044",
"status": "active",
"environment": "live",
"payment_method_status": null
},
"payment": {
"id": "PAYMENT_ID",
"provider": "mercadopago",
"status": "paid",
"amount": 50820,
"currency": "ARS",
"provider_ref": "1234567890"
}
}payment.failed
{
"event": "payment.failed",
"timestamp": "2026-10-26T14:04:40.120Z",
"policy": {
"id": "b2a7…",
"policy_number_internal": "POL-0045",
"status": "payment_failed",
"environment": "live",
"payment_method_status": null
},
"payment": {
"id": "PAYMENT_ID",
"provider": "stripe",
"status": "failed",
"amount": 50820,
"currency": "ARS",
"provider_ref": "pi_…",
"failure_reason": "Your card was declined."
}
}payment.refund_required
{
"event": "payment.refund_required",
"timestamp": "2026-10-27T10:20:11.500Z",
"policy": {
"id": "b2a7…",
"policy_number_internal": "POL-0046",
"status": "rejected",
"environment": "live",
"payment_method_status": null,
"refund_status": "pending"
},
"payment": {
"id": "PAYMENT_ID",
"provider": "stripe",
"status": "paid",
"amount": 50820,
"currency": "ARS",
"provider_ref": "pi_…"
}
}payment.succeededandpayment.failedgo to the policies URL, with a summary of the policy and the payment:id,provider,status,amount,currency,provider_refand, when the provider reports it,failure_reason.- After a
payment.failedthe insured can retry: a later approved payment sendspayment.succeededand the policy becomesactive. On conditional products the payment only opens once the conditions are approved. payment.refund_requiredandpayment.refunded(only older policies paid before their conditions) carry the collected payment andpolicy.refund_status(pendingordone). Quore never refunds on its own.
quote.created
{
"event": "quote.created",
"timestamp": "2026-10-26T13:58:02.120Z",
"quote": {
"quoteId": "QUOTE_ID",
"productId": "PRODUCT_ID",
"variables": {
"edad": 35,
"plan": "full"
},
"status": "priced",
"environment": "live",
"coverages": [
{
"key": "basica",
"totalPremium": 50820
}
]
}
}quote.approved
{
"event": "quote.approved",
"timestamp": "2026-10-26T15:20:00.000Z",
"quote": {
"quoteId": "QUOTE_ID",
"approvedBy": "USER_ID",
"environment": "live"
}
}Signature
Headers of every delivery
POST /webhooks/quore HTTP/1.1 Content-Type: application/json X-Quore-Event: policy.created X-Quore-Delivery: 7d0e6c1a-… X-Quore-Timestamp: 1793023391 X-Quore-Signature-V2: sha256=5f1c… X-Quore-Signature: sha256=a93b…
X-Quore-Timestamp: time of the delivery, in Unix seconds. It changes on every retry.X-Quore-Signature-V2:sha256=followed by the hex HMAC-SHA256 of{timestamp}.{body}(the timestamp, a dot and the raw body), keyed with the webhook secret.- Verify the signature over the raw body, exactly as received (before parsing the JSON), and compare in constant time.
- Reject with
401deliveries without a signature, with an invalid one, or with a timestamp more than 5 minutes away from your clock: a captured delivery cannot be replayed. X-Quore-Deliveryidentifies the delivery and stays the same across retries: use it to drop duplicates.X-Quore-Eventrepeats the event name.
Verify the signature
const crypto = require('crypto');
const express = require('express');
const TOLERANCE_SECONDS = 300;
app.post('/webhooks/quore', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('X-Quore-Timestamp') || '';
const received = req.get('X-Quore-Signature-V2') || '';
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (!timestamp || !Number.isFinite(age) || age > TOLERANCE_SECONDS) return res.sendStatus(401);
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.QUORE_WEBHOOK_SECRET)
.update(timestamp + '.')
.update(req.body)
.digest('hex');
const valid = received.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) return res.sendStatus(401);
const event = JSON.parse(req.body.toString('utf8'));
res.sendStatus(200);
});X-Quore-Signature (HMAC of the body only, no timestamp) is still sent for compatibility with existing integrations. It does not protect against replays: use X-Quore-Signature-V2.
Delivery and retries
- Each event is attempted up to 3 times: the first attempt immediately, the second 1 second after the first fails and the third 3 seconds after the second fails. Each attempt waits up to 10 seconds for the response.
- Any 2xx response counts as delivered. Answer fast and process in the background.
- If the 3 attempts fail, the event is recorded as failed in the account history and is not sent again: reconcile with
GET /api/policiesorGET /api/quotes. - Arrival order is not guaranteed and an event may arrive more than once: process them idempotently (by
X-Quore-Delivery, or by policyidandevent).