Skip to content

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

  1. Open your Mercura tenant and go to Settings → Webhooks.
  2. Select Add Webhook.
  3. Enter your receiver’s URL.
  4. Configure Secret according to the authentication method confirmed for your tenant.
  5. Add a Description, such as “CRM quote synchronization”.
  6. Select the Event Types your integration needs.
  7. Set Active according to whether the integration should run.
  8. Select Save and back.
SettingPurpose
ActiveActivation control for the webhook.
URLDestination for the notification. Marked as required in the form. Use an HTTPS endpoint you control.
SecretAuthentication-related setting. The field alone does not establish how it is transmitted or used to sign requests.
DescriptionHuman-readable description of the integration.
Event TypesEvent 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 familyEvent labels in the settings
DraftDraft created, Draft updated, Draft deleted
Form configurationForm Configuration Created, Form Configuration Updated, Form Configuration Deleted
QuoteQuote created, Quote updated, Quote deleted
RequestRequest 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.

PropertyObserved typeHow to use it
eventstringRoute the notification to the relevant handler.
dataobjectRead the quote and configuration details.
data.idintegerIdentify 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 pathObserved typeMeaning and integration notes
data.idintegerQuote identifier. Include tenant context in keys when integrating several tenants.
data.statusintegerStatus code; the sample contains 2. Its business meaning has not been confirmed.
data.typestringThe sample contains form. Do not assume this is the only supported quote type.
data.origin_request_idnullReference field for an originating request/quote. Exact webhook semantics require confirmation.
data.revision_numberintegerQuote revision, 1 in the example. Do not use it as a unique delivery identifier.
data.namestringTop-level quote contact/name field. Keep it distinct from data.user.name.
data.emailstringTop-level quote contact email. Keep it distinct from the Mercura user’s email.
data.notesnullNotes are empty in this example.
data.conf_totalnumberQuote configuration total, 173300 in the example. Confirm the webhook’s tax and monetary-unit contract before accounting use.
data.vat_totalnumberVAT total field, 0 in the example.
data.created_atstringExample: 2026-10-08 09:32:00. No timezone offset is supplied.
data.updated_atstringExample: 2026-10-08 10:17:06. No timezone offset is supplied.
data.pdfsarrayEmpty in this example; document item shape is not established.
data.documentsarrayEmpty in this example; document item shape is not established.
data.extra_fieldsarrayTenant-defined quote/request field groups.
data.userobjectMercura user information, including user ID, name, email, role and language.
data.form_configurationobjectParent configuration with country, currency and submissions.
data.form_submissionsarrayConfiguration 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 pathSample value/typeNotes
data.form_configuration.id9996Parent configuration identifier; distinct from the quote ID.
data.form_configuration.name"Project name"Configuration name.
data.form_configuration.locked1Numeric flag in the sample.
data.form_configuration.country.slug"DK"Country code.
data.form_configuration.currency.code"DKK"Currency code.
data.form_configuration.currency.digits2Currency precision metadata.
data.form_configuration.configuration_3d_data"{}"JSON stored as a string; parse separately if needed.
data.form_configuration.form_submissionsarrayNested 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 typeDescription
idintegerSubmission identifier; 14599 in the example.
namestringLine/configuration name.
quantitynumberConfiguration quantity.
pricestringDecimal text, such as "173300.00".
form.idintegerConfigurator identifier; 35 in the example.
form.namestringConfigurator name; Truck Configurator.
form_configuration_idintegerParent configuration reference.
groupsarrayForm groups and their elements.
elementsarrayAdditional element collection; empty in this example.
created_at, updated_atstringSample 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/optionNotes
group.id, group.labelGroup identifier and display label.
element.id, element.label, element.typeForm element identifier, label and input type.
element.stateExample 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.valueNull for the radio elements in the sample.
element.attribute_values[]Empty in the example.
option.id, option.form_element_idOption identity and parent element reference.
option.labelCan be an empty string, including for priced options.
option.price, option.base_priceNumeric pricing fields in the sample. Discount and pricing semantics require a separate contract.
option.quantityNumeric option quantity.
translationTranslation objects, null, or empty arrays occur in different places.
extra_fieldsTenant/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:

  1. Authenticate the incoming request using the confirmed Mercura mechanism.
  2. Parse and validate the event envelope and the fields your handler needs.
  3. Store the notification durably before acknowledging it.
  4. Perform CRM/ERP work in a background worker.
  5. 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

  1. Configure an approved receiver in a non-production tenant.
  2. Subscribe to Quote updated.
  3. Make a controlled quote change.
  4. Capture the request method, headers, raw body and response outcome securely.
  5. Confirm that the event is quote.updated and that the quote ID matches.
  6. Verify currency, custom fields and every configuration line against the quote.
  7. 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

SymptomWhat to check
No notification receivedCheck Active, selected events, the receiver URL and whether the action actually emits that event. Check any available delivery diagnostics.
Request rejectedCompare receiver authentication with Mercura’s confirmed secret/signature behavior.
Duplicate recordsCheck that processing upserts by tenant and quote ID rather than creating a record on every notification.
Configuration lines doubledProcess one submission collection; do not concatenate the top-level and nested copies.
Selections missingRead groups[].elements[]; the direct elements[] array is empty in the sample. Check product-backed and free-input element shapes separately.
Blank product/option nameSome option labels are empty. Resolve your configured ID mapping rather than dropping the option.
Wrong monetary totalsCheck currency, decimal conversion, quantity semantics, discounts and tax interpretation.
Unexpected statusUse a confirmed status-code mapping; do not infer a business label from 2.
Incorrect timestamp conversionSome sample timestamps have Z; others have no timezone offset. Confirm timezone before conversion.
BoM fields not foundDocument-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.