Webhooks
Use webhooks to receive event notifications from Mercura in your own integration. For example, a quote.updated notification can supply quote details and configuration selections to a CRM or ERP connector.
This guide covers the webhook settings and a supplied quote.updated example. Payloads for other event types may differ. Delivery and authentication details that have not yet been verified are listed under Before production use.
Create a webhook
- Open your Mercura tenant and go to Settings → Webhooks.
- Select Add Webhook.
- Enter your receiver’s URL.
- Configure Secret according to the authentication method confirmed for your tenant.
- Add a Description, such as “CRM quote synchronization”.
- Select the Event Types your integration needs.
- Set Active according to whether the integration should run.
- Select Save and back.
| Setting | Purpose |
|---|---|
| Active | Activation control for the webhook. |
| URL | Destination for the notification. Marked as required in the form. Use an HTTPS endpoint you control. |
| Secret | Authentication-related setting. The field alone does not establish how it is transmitted or used to sign requests. |
| Description | Human-readable description of the integration. |
| Event Types | Event subscriptions. Marked as required in the form. |
Use separate endpoints or subscriptions when integrations serve different purposes. Send quote data only to systems approved to receive it.
Choose events
The demo tenant exposes the following event choices:
| Event family | Event labels in the settings |
|---|---|
| Draft | Draft created, Draft updated, Draft deleted |
| Form configuration | Form Configuration Created, Form Configuration Updated, Form Configuration Deleted |
| Quote | Quote created, Quote updated, Quote deleted |
| Request | Request Created, Request Updated, Request Deleted |
The supplied payload confirms the wire event name quote.updated. Display labels are not necessarily the event strings delivered to your receiver. The numeric values behind the selector are internal subscription identifiers, not payload event names.
Do not assume that Quote and Request events, or Draft and Form Configuration events, are aliases. Their relationship, exact triggers, and whether one action can emit several events need confirmation before subscribing to both families.
Understand the event envelope
The supplied notification contains two top-level properties:
{ "event": "quote.updated", "data": { "id": 5680, "status": 2, "type": "form", "revision_number": 1 }}This is an excerpt; the real payload contains additional properties.
| Property | Observed type | How to use it |
|---|---|---|
event | string | Route the notification to the relevant handler. |
data | object | Read the quote and configuration details. |
data.id | integer | Identify the quote. It is not a unique notification or delivery ID. |
The sample does not include an envelope-level event ID, schema version, delivery ID, or event occurrence timestamp. Do not assume these are available in other payloads or request headers without checking the delivery contract.
Quote payload reference
Types below describe the supplied example. They are not a complete schema for all events and tenant configurations.
Quote details
| JSON path | Observed type | Meaning and integration notes |
|---|---|---|
data.id | integer | Quote identifier. Include tenant context in keys when integrating several tenants. |
data.status | integer | Status code; the sample contains 2. Its business meaning has not been confirmed. |
data.type | string | The sample contains form. Do not assume this is the only supported quote type. |
data.origin_request_id | null | Reference field for an originating request/quote. Exact webhook semantics require confirmation. |
data.revision_number | integer | Quote revision, 1 in the example. Do not use it as a unique delivery identifier. |
data.name | string | Top-level quote contact/name field. Keep it distinct from data.user.name. |
data.email | string | Top-level quote contact email. Keep it distinct from the Mercura user’s email. |
data.notes | null | Notes are empty in this example. |
data.conf_total | number | Quote configuration total, 173300 in the example. Confirm the webhook’s tax and monetary-unit contract before accounting use. |
data.vat_total | number | VAT total field, 0 in the example. |
data.created_at | string | Example: 2026-10-08 09:32:00. No timezone offset is supplied. |
data.updated_at | string | Example: 2026-10-08 10:17:06. No timezone offset is supplied. |
data.pdfs | array | Empty in this example; document item shape is not established. |
data.documents | array | Empty in this example; document item shape is not established. |
data.extra_fields | array | Tenant-defined quote/request field groups. |
data.user | object | Mercura user information, including user ID, name, email, role and language. |
data.form_configuration | object | Parent configuration with country, currency and submissions. |
data.form_submissions | array | Configuration lines included with the quote. |
The document-template data reference describes conf_total as excluding VAT, but document-template output is a separately transformed representation. Verify that meaning for webhook payloads rather than assuming the two schemas are identical.
Parent configuration and currency
| JSON path | Sample value/type | Notes |
|---|---|---|
data.form_configuration.id | 9996 | Parent configuration identifier; distinct from the quote ID. |
data.form_configuration.name | "Project name" | Configuration name. |
data.form_configuration.locked | 1 | Numeric flag in the sample. |
data.form_configuration.country.slug | "DK" | Country code. |
data.form_configuration.currency.code | "DKK" | Currency code. |
data.form_configuration.currency.digits | 2 | Currency precision metadata. |
data.form_configuration.configuration_3d_data | "{}" | JSON stored as a string; parse separately if needed. |
data.form_configuration.form_submissions | array | Nested submissions also appear at data.form_submissions in this example. |
Do not add the two submission arrays together: they contain the same submission in the supplied notification. The example below reads the top-level array once. Confirm which location is guaranteed for your integration.
Configuration lines
Path relative to data.form_submissions[] | Observed type | Description |
|---|---|---|
id | integer | Submission identifier; 14599 in the example. |
name | string | Line/configuration name. |
quantity | number | Configuration quantity. |
price | string | Decimal text, such as "173300.00". |
form.id | integer | Configurator identifier; 35 in the example. |
form.name | string | Configurator name; Truck Configurator. |
form_configuration_id | integer | Parent configuration reference. |
groups | array | Form groups and their elements. |
elements | array | Additional element collection; empty in this example. |
created_at, updated_at | string | Sample submission timestamps use ISO 8601 with Z. |
Do not infer whether line price must be multiplied by quantity from this example: its quantity is one. Confirm whether it represents a unit price or an extended line price before calculating totals.
Groups, elements and options
Traverse grouped selections using:
data.form_submissions[] └─ groups[] └─ elements[] ├─ options[] ├─ products[] ├─ value └─ attribute_values[]| Path relative to a group/element/option | Notes |
|---|---|
group.id, group.label | Group identifier and display label. |
element.id, element.label, element.type | Form element identifier, label and input type. |
element.state | Example states include open and required. Do not use this alone as proof of completed validation. |
element.options[] | Options included with the submission. Confirm selection semantics across other element types. |
element.products[] | Empty in all supplied elements; product record shape is not established. |
element.value | Null for the radio elements in the sample. |
element.attribute_values[] | Empty in the example. |
option.id, option.form_element_id | Option identity and parent element reference. |
option.label | Can be an empty string, including for priced options. |
option.price, option.base_price | Numeric pricing fields in the sample. Discount and pricing semantics require a separate contract. |
option.quantity | Numeric option quantity. |
translation | Translation objects, null, or empty arrays occur in different places. |
extra_fields | Tenant/entity-specific extensions with differing shapes. |
Map configurators, elements and options using IDs configured for the relevant tenant. Labels and translations can change. Do not treat IDs as universal across tenants or as ERP article numbers.
Custom quote fields
Custom fields are nested under groups in data.extra_fields:
[ { "id": 43, "name": "Contact Details", "fields": [ { "name": "Deal Name", "slug": "deal_name", "type": "text", "value": "Deal A", "translation": [] } ] }]Look up a configured field by slug instead of relying on a group label or array position:
function getQuoteField(quote, slug) { const groups = Array.isArray(quote.extra_fields) ? quote.extra_fields : []; const matches = groups.flatMap(group => Array.isArray(group.fields) ? group.fields : [] ).filter(field => field.slug === slug);
if (matches.length > 1) throw new Error(`Ambiguous field slug: ${slug}`); return matches[0]?.value ?? null;}
const dealName = getQuoteField(payload.data, 'deal_name');In the example, blank fields use "". Distinguish a blank field from a missing field when your downstream system needs that distinction.
Map a quote to another system
This dependency-free JavaScript function extracts a small integration record. It demonstrates payload mapping, not an authenticated webhook server.
export function mapQuoteUpdated(payload) { if (payload?.event !== 'quote.updated') { throw new Error('Expected quote.updated'); }
const quote = payload.data; if (!quote || !Number.isInteger(quote.id)) { throw new Error('Missing or invalid quote ID'); } if (!Array.isArray(quote.form_submissions)) { throw new Error('Expected top-level form_submissions'); }
return { mercuraQuoteId: quote.id, revision: quote.revision_number ?? null, statusCode: quote.status ?? null, currency: quote.form_configuration?.currency?.code ?? null, configurationTotal: quote.conf_total ?? null, vatTotal: quote.vat_total ?? null, lines: quote.form_submissions.map(line => { const groups = Array.isArray(line.groups) ? line.groups : []; const direct = Array.isArray(line.elements) ? line.elements : []; const grouped = groups.flatMap(group => Array.isArray(group.elements) ? group.elements : [] ); const seen = new Set(); const elements = [...grouped, ...direct].filter(element => { if (element.id == null) return true; if (seen.has(element.id)) return false; seen.add(element.id); return true; });
return { mercuraSubmissionId: line.id, configuratorId: line.form?.id ?? null, name: line.name ?? null, quantity: line.quantity ?? null, price: line.price ?? null, selections: elements.map(element => ({ elementId: element.id, label: element.label ?? '', value: element.value ?? null, options: (Array.isArray(element.options) ? element.options : []) .map(option => ({ optionId: option.id, label: option.label ?? '', quantity: option.quantity ?? null, price: option.price ?? null })) })) }; }) };}Retain decimal strings until your accounting or decimal library converts them. Avoid binary floating-point arithmetic for financial reconciliation. Preserve the supplied quote total instead of rebuilding it from option prices: discounts, quantity rules and other adjustments may affect totals.
This mapper does not handle product-backed selections because none are present in the supplied sample. Extend it only after checking a representative product payload.
Build a reliable receiver
The following are receiver design recommendations, not statements about Mercura’s delivery guarantees:
- Authenticate the incoming request using the confirmed Mercura mechanism.
- Parse and validate the event envelope and the fields your handler needs.
- Store the notification durably before acknowledging it.
- Perform CRM/ERP work in a background worker.
- Record processing failures and provide controlled replay.
Do not acknowledge successful ingestion if durable storage failed. Choose response codes and timing against the confirmed Mercura retry contract.
Duplicate and out-of-order notifications
Make operations safe to repeat. For current-state synchronization, upsert the same quote using tenant context plus data.id, and use line IDs to reconcile line records instead of appending duplicates.
An upsert key identifies a business record, not a delivery. The sample’s quote ID and revision number cannot distinguish every update. If you need a notification deduplication key, confirm an event/delivery ID or define a strategy with Mercura. A payload hash can identify identical bodies, but cannot prove that two notifications represent the same business event.
Do not assume delivery order or treat data.updated_at as a reliable ordering key until its timezone, precision and update semantics are confirmed. Serialize processing per quote where needed and agree on a reconciliation method for stale updates.
Security and data handling
The sample contains contact information, user-role information, quote values and configuration data. Limit access to your receiver and stored payloads. Keep secrets out of URLs, public examples and application logs. Log event names, record IDs and processing outcomes instead of full payloads by default.
The presence of a Secret field does not establish HMAC support. Do not invent a signature header, algorithm or verification format. If signing is confirmed, verify the exact raw request bytes using the documented encoding and comparison method before parsing the body.
Test your integration
- Configure an approved receiver in a non-production tenant.
- Subscribe to Quote updated.
- Make a controlled quote change.
- Capture the request method, headers, raw body and response outcome securely.
- Confirm that the event is
quote.updatedand that the quote ID matches. - Verify currency, custom fields and every configuration line against the quote.
- Replay a sanitized fixture locally to check that repeated processing does not create duplicate records.
Also test multiple lines, quantity greater than one, product-backed selections, free-input elements, discounts, missing optional data, empty collections and unavailable downstream systems. Test delivery failures and retries only with an approved receiver and a confirmed delivery contract.
Troubleshooting
| Symptom | What to check |
|---|---|
| No notification received | Check Active, selected events, the receiver URL and whether the action actually emits that event. Check any available delivery diagnostics. |
| Request rejected | Compare receiver authentication with Mercura’s confirmed secret/signature behavior. |
| Duplicate records | Check that processing upserts by tenant and quote ID rather than creating a record on every notification. |
| Configuration lines doubled | Process one submission collection; do not concatenate the top-level and nested copies. |
| Selections missing | Read groups[].elements[]; the direct elements[] array is empty in the sample. Check product-backed and free-input element shapes separately. |
| Blank product/option name | Some option labels are empty. Resolve your configured ID mapping rather than dropping the option. |
| Wrong monetary totals | Check currency, decimal conversion, quantity semantics, discounts and tax interpretation. |
| Unexpected status | Use a confirmed status-code mapping; do not infer a business label from 2. |
| Incorrect timestamp conversion | Some sample timestamps have Z; others have no timezone offset. Confirm timezone before conversion. |
| BoM fields not found | Document-template payloads use a transformed schema. The supplied webhook has no form.bom or form.data.elements. |
Before production use
Confirm these contract details with Mercura for your tenant/version:
- Exact wire names and triggers for all subscribed events, including relationships between event families.
- Payload shapes for create, update and delete events, including whether updates are snapshots or partial changes.
- Request method, content type, relevant headers and sender identification.
- How Secret is used; signature header, algorithm, encoding and signed bytes if applicable.
- Successful response codes, timeout, retry schedule, maximum attempts and manual replay facilities.
- Duplicate and ordering guarantees, event/delivery IDs and any replay protection.
- Status-code meanings, monetary units, tax interpretation and line-price quantity semantics.
- Timezones and timestamp precision, especially strings without an offset.
- Guaranteed submission location, product/document schemas, tenant identity and schema compatibility policy.
Related documentation
- Request Fields
- Document Template Data Reference — related concepts, but a different payload representation.
- Developer Overview