Appearance
Fresh 2026
Merge webhooks sent to you
How to configure webhook endpoints that receive data change notifications from Merge. Covers event types (sync completed, data changed), payload format, and webhook verification.
Overview
Webhooks offer a real-time way to notify your app when data changes. This guide teaches how to setup these webhooks, which are inbound POST requests to your API, allowing you to create, update, or delete data in your app based on Merge events.
Webhooks are shared across your Merge organization.
Create webhooks
To create webhooks, go to the
Webhooks
management console under the Advanced configuration in your Merge dashboard and click
+Webhook.
On the next page, add the URL that you want Merge to send a POST request to. This URL should point to a POST route in your API that you'll build to handle the incoming payload.
Select the event type that you want to trigger the webhook. We recommend the
Common Model sync
and/or
changed data webhooks
if you have a lot of data to keep in sync. You can detect the affected data models within your API by parsing the JSON payload.
If you want to test your webhook, click
Send test POST request
next to your URL. This will cause Merge to send a POST request carrying a sample payload to the URL you've specified, where you can log the output and program your endpoint to do something in response to the payload.
Payload Properties
| Field | Type | Description |
|---|---|---|
hook | Object | The webhook that was triggered |
linked_account | Object | The user whose data has changed |
data | Object | The affected data model. The structure of each model can be found in our API documentation. |
Webhook event types
Receive an alert when a new Linked Account is linked.
json
{
"hook": {
"id": "cb1fe0a7-c2a1-4bd6-8cf5-57e70c7f1d53",
"event": "LinkedAccount.linked",
"target": "https://yoururl.com"
},
"linked_account": {
"id": "a3602c03-aba7-4d9d-a349-dbc338504092",
"integration": "Workday",
"integration_slug": "workday",
"category": "ats",
"end_user_origin_id": "",
"end_user_organization_name": "Test",
"end_user_email_address": "test@merge.dev",
"status": "COMPLETE",
"webhook_listener_url": "https://api.merge.dev/api/integrations/webhook-listener/IDS",
"is_duplicate": null,
"account_type": "PRODUCTION"
},
"data": {
"account_token": "YOUR_ACCOUNT_TOKEN",
"is_relink": false
}
}Receive an alert when selected Linked Accounts are synced.
json
{
"hook": {
"id": "e8affe31-8ae0-4b37-8c50-d86303094dc4",
"event": "LinkedAccount.sync_completed",
"target": "https://webhook.site/105606a8-cfa3-4415-bae0-954e25137f00"
},
"linked_account": {
"id": "4ac10f37-c656-4e9a-89a1-1b04f9e9a343",
"integration": "Ashby",
"integration_slug": "ashby",
"category": "ats",
"end_user_origin_id": "12345678910",
"end_user_organization_name": "Example Organization",
"end_user_email_address": "jack.cavalier@merge.dev",
"status": "COMPLETE",
"webhook_listener_url": "https://api.merge.dev/api/integrations/webhook-listener/abcd1234defg5678",
"is_duplicate": false,
"account_type": "PRODUCTION"
},
"data": {
"is_initial_sync": true,
"integration_name": "Ashby",
"integration_id": "ashby",
"sync_status": {
"ats.RemoteUser": {
"last_sync_finished": "2023-12-29T18:53:09Z",
"last_sync_result": "DONE"
},
"ats.Candidate": {
"last_sync_finished": "2023-12-29T18:57:12Z",
"last_sync_result": "PARTIALLY_SYNCED"
},
"ats.Application": {
"last_sync_finished": "2023-12-29T18:59:25Z",
"last_sync_result": "DONE"
},
"ats.Job": {
"last_sync_finished": "2023-12-29T17:05:45Z",
"last_sync_result": "FAILED"
}
}
}
}Receive an alert when selected Linked Accounts are deleted. Please note: deletion webhooks are triggered only after the Linked Account has been fully removed from our system. Depending on account size, this can take up to a few hours.
json
{
"hook": {
"id": "e8affe31-8ae0-4b37-8c50-d86303094dc4",
"event": "LinkedAccount.deleted",
"target": "https://webhook.site/105606a8-cfa3-4415-bae0-954e25137f00"
},
"linked_account": {
"id": "4ac10f37-c656-4e9a-89a1-1b04f9e9a343",
"integration": "Ashby",
"end_user_origin_id": "12345678910",
"end_user_organization_name": "Example Organization",
"end_user_email_address": "jack.cavalier@merge.dev",
"account_type": "PRODUCTION"
}
}Receive an alert when a Linked Account issue is created, reopened, or resolved.
Event types include Issue.new, Issue.reopened, and Issue.resolved.
json
{
"hook": {
"id": "e8affe31-8ae0-4b37-8c50-d86303094dc4",
"event": "Issue.new",
"target": "https://yoururl.com"
},
"linked_account": {
"id": "a3602c03-aba7-4d9d-a349-dbc338504092",
"integration": "Workday",
"integration_slug": "workday",
"category": "ats",
"end_user_origin_id": "",
"end_user_organization_name": "Test",
"end_user_email_address": "test@merge.dev",
"status": "COMPLETE",
"webhook_listener_url": "https://api.merge.dev/api/integrations/webhook-listener/IDS",
"is_duplicate": null,
"account_type": "PRODUCTION"
},
"data": {
"id": "test id",
"status": "ONGOING",
"error_description": "Bad API Key",
"end_user": {
"id": "",
"origin_id": "",
"organization_name": "Customer",
"organization_logo": "logo.png",
"email_address": "test@merge.dev"
},
"first_incident_time": "2023-01-24T16:08:35.954686Z",
"last_incident_time": "2023-01-24T16:09:30.245907Z",
"is_muted": false,
"error_details": []
}
}Receive an alert when selected Common Models are synced. This option is best if you have a lot of data to keep in sync.
Event types include {common_model}.synced.
json
{
"hook": {
"id": "e8affe31-8ae0-4b37-8c50-d86303094dc4",
"event": "Candidate.synced",
"target": "https://yoururl.com"
},
"linked_account": {
"id": "a3602c03-aba7-4d9d-a349-dbc338504092",
"integration": "Workday",
"integration_slug": "workday",
"category": "ats",
"end_user_origin_id": "",
"end_user_organization_name": "Test",
"end_user_email_address": "test@merge.dev",
"status": "COMPLETE",
"webhook_listener_url": "https://api.merge.dev/api/integrations/webhook-listener/IDS",
"is_duplicate": null,
"account_type": "PRODUCTION"
},
"data": {
"integration_name": "Ashby",
"integration_id": "ashby",
"synced_fields": ["first_name", "last_name"],
"sync_status": {
"model_name": "Candidate",
"model_id": "ats.Candidate",
"last_sync_start": "2023-09-27T20:50:47.490402Z",
"next_sync_start": "2023-09-11T23:24:52.242660Z",
"status": "SYNCING",
"last_sync_result": "DONE",
"last_sync_finished": "2023-09-27T20:53:47.490402Z",
"is_initial_sync": false
}
}
}Receive a payload containing the most recent data when selected Common Models are created or updated.
Event types include {common_model}.added and {common_model}.changed.
json
{
"hook": {
"id": "cb1fe0a7-c2a1-4bd6-8cf5-57e70c7f1d53",
"event": "Candidate.changed",
"target": "https://yoururl.com"
},
"linked_account": {
"id": "a3602c03-aba7-4d9d-a349-dbc338504092",
"integration": "Workday",
"integration_slug": "workday",
"category": "ats",
"end_user_origin_id": "",
"end_user_organization_name": "Test",
"end_user_email_address": "test@merge.dev",
"status": "COMPLETE",
"webhook_listener_url": "https://api.merge.dev/api/integrations/webhook-listener/IDS",
"is_duplicate": null,
"account_type": "PRODUCTION"
},
"data": {
"id": "22582af6-b45e-4d34-bed6-77bb40effa97",
"remote_id": "97134123",
"first_name": "Gil",
"last_name": "Feig",
"company": "Columbia Dining App.",
"title": "Software Engineer",
"remote_created_at": "2020-11-11T00:59:25.309761Z",
"remote_updated_at": "2020-11-11T00:59:25.309761Z",
"last_interaction_at": "2020-11-11T00:59:25.309761Z",
"is_private": true,
"can_email": true,
"locations": ["San Francisco", "New York", "Miami"],
"phone_numbers": [
{
"value": "+1234567890",
"phone_number_type": "MOBILE"
}
],
"email_addresses": [
{
"value": "hello@merge.dev",
"email_address_type": "PERSONAL"
}
],
"urls": [
{
"value": "http://alturl.com/p749b",
"url_type": "BLOG"
}
],
"tags": [
{
"remote_id": "4567",
"name": "High-Priority"
}
],
"applications": ["29eb9867-ce2a-403f-b8ce-f2844b89f078", "b4d08e5c-de00-4d64-a29f-66addac9af99"],
"attachments": ["bea08964-32b4-4a20-8bb4-2612ba09de1d"]
}
}Receive a payload containing the most recent data when selected Common Models are deleted.
Event types include {common_model}.removed.
json
{
"hook": {
"id": "c6fa014e-bbd1-4aff-b68c-74202a2833f4",
"event": "Candidate.removed",
"target": "https://yoururl.com"
},
"linked_account": {
"id": "a3602c03-aba7-4d9d-a349-dbc338504092",
"integration": "Workday",
"integration_slug": "workday",
"category": "ats",
"end_user_origin_id": "",
"end_user_organization_name": "Test",
"end_user_email_address": "test@merge.dev",
"status": "COMPLETE",
"webhook_listener_url": "https://api.merge.dev/api/integrations/webhook-listener/IDS",
"is_duplicate": null,
"account_type": "PRODUCTION"
},
"data": {
"id": "7f00a6cb-e85e-44b2-b804-15c8743ab19c",
"remote_id": "prospect_20230825184013_DVOVCDIZHASEY1CO",
"first_name": "Hello",
"last_name": "Merge",
"company": null,
"title": null,
"remote_created_at": "2023-08-25T00:00:00Z",
"remote_updated_at": null,
"last_interaction_at": null,
"is_private": null,
"can_email": null,
"locations": ["1 Main St New York "],
"phone_numbers": [],
"email_addresses": [
{
"value": "hello@merge.dev",
"email_address_type": null,
"modified_at": "2023-09-21T18:26:32.880870Z"
}
],
"urls": [],
"tags": [],
"applications": ["3556d531-213e-42aa-9e71-8458c4408d67"],
"attachments": [],
"remote_was_deleted": true,
"modified_at": "2023-09-22T20:20:06.709059Z",
"field_mappings": {
"organization_defined_targets": {},
"linked_account_defined_targets": {}
},
"remote_data": null
}
}Receive an alert when an Async Passthrough request has resolved.
json
{
"hook": {
"id": "32b4aa43-f48f-4fe3-8d09-583fe3b33748",
"event": "AsyncPassthrough.resolved",
"target": "https://yoururl.com"
},
"async_passthrough_receipt_id": "c90c595f-c62b-4b8f-946d-3fc48c84c7c5",
"status": "COMPLETED",
"data": {
"method": "GET",
"path": "/employees/1761",
"status": 200,
"response": { "id": "1761" },
"response_headers": {
"Date": "Fri, 22 Sep 2023 19:57:33 GMT",
"Content-Type": "application/json",
"Transfer-Encoding": "chunked",
"Connection": "close",
"Server": "nginx",
"Cache-Control": "no-cache, private",
"Strict-Transport-Security": "max-age=31536000; includeSubdomains;",
"Vary": "Authorization,User-Agent",
"X-Content-Type-Options": "nosniff"
},
"response_type": "JSON",
"headers": {
"User-Agent": "Mozilla/5.0",
"Accept-Encoding": "gzip, deflate, br",
"Accept": "",
"Connection": "keep-alive",
"Content-Type": "application/json",
"Content-Length": "20",
"Authorization": "Basic ",
"x-datadog-trace-id": "11959321019361632961",
"x-datadog-parent-id": "9757196969814365310",
"x-datadog-sampling-priority": "1"
}
}
}Receive an alert when an Async POST request has completed. See Async operations for supported models and availability.
json
{
"hook": {
"id": "32b4aa43-f48f-4fe3-8d09-583fe3b33748",
"event": "AsyncPost.completed",
"target": "https://yoururl.com"
},
"linked_account": {
"id": "a3602c03-aba7-4d9d-a349-dbc338504092",
"integration": "Sage Intacct",
"integration_slug": "sage-intacct",
"category": "accounting",
"end_user_origin_id": "12345",
"end_user_organization_name": "Test Organization",
"end_user_email_address": "test@merge.dev",
"status": "COMPLETE",
"webhook_listener_url": "https://api.merge.dev/api/integrations/webhook-listener/IDS",
"is_duplicate": null,
"account_type": "PRODUCTION",
"completed_at": "2025-08-14T12:30:00Z"
},
"data": {
"task_id": "c90c595f-c62b-4b8f-946d-3fc48c84c7c5",
"status": "COMPLETED",
"type": "POST",
"model": "Invoice",
"result": {
"status_code": 201,
"response": {
"model": {
"id": "7f00a6cb-e85e-44b2-b804-15c8743ab19c",
"remote_id": "INV-1042",
"created_at": "2025-08-14T12:30:22.348271Z",
"modified_at": "2025-08-14T12:30:22.348290Z",
"type": "ACCOUNTS_PAYABLE",
"contact": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"number": "INV-1042",
"company": "e4f5a6b7-c8d9-0e1f-2a3b-4c5d6e7f8a9b",
"issue_date": "2025-08-14T00:00:00Z",
"due_date": "2025-09-13T00:00:00Z",
"paid_on_date": null,
"currency": "USD",
"total_amount": 5400.00,
"balance": 5400.00,
"payments": [],
"line_items": [],
"remote_was_deleted": false,
"field_mappings": {
"organization_defined_targets": {},
"linked_account_defined_targets": {}
},
"remote_data": null
},
"warnings": [],
"errors": []
}
}
}
}Receive an alert when an Async Bulk POST request has completed. See Async operations for supported models and availability.
json
{
"hook": {
"id": "eacd7646-1284-498b-ab84-2872bdae94cb",
"event": "AsyncBulkPost.completed",
"target": "https://yoururl.com"
},
"linked_account": {
"id": "a3602c03-aba7-4d9d-a349-dbc338504092",
"integration": "Sage Intacct",
"integration_slug": "sage-intacct",
"category": "accounting",
"end_user_origin_id": "12345",
"end_user_organization_name": "Test Organization",
"end_user_email_address": "test@merge.dev",
"status": "COMPLETE",
"webhook_listener_url": "https://api.merge.dev/api/integrations/webhook-listener/IDS",
"is_duplicate": null,
"account_type": "PRODUCTION",
"completed_at": "2025-08-14T12:30:00Z"
},
"data": {
"batch_id": "2a75b0b3-679a-43cc-a1f5-cd2e41d2fc12",
"type": "BULK_POST",
"model": "Invoice",
"status": "PARTIAL_SUCCESS",
"total_count": 3,
"success_count": 2,
"failed_count": 1,
"results": [
{
"item_id": "INV-1000",
"status": "SUCCESS",
"response": {
"invoice_id": "a3f2b8c1-9d4e-4f6a-b7c2-1e8d5a9f0b3c"
}
},
{
"item_id": "INV-2000",
"status": "SUCCESS",
"response": {
"invoice_id": "b4e3c9d2-0e5f-5a7b-c8d3-2f9e6b0a1c4d"
}
},
{
"item_id": "INV-3000",
"status": "FAILED",
"response": {
"warnings": [],
"errors": [
{
"source": {
"pointer": "batch_items[2]/payload/company"
},
"title": "Model Not Found",
"detail": "CompanyInfo with id 30764020-fd12-483e-ae73-f74b7fb31d6b does not exist",
"problem_type": "MODEL_NOT_FOUND"
}
]
}
}
]
}
}Security
You'll want to ensure that your API endpoint is verifying that incoming POST requests are from Merge and not a malicious source, and that payloads haven't been altered in transit.
The best way to do that is to check that the
X-Merge-Webhook-Signature
field in the header of the incoming request matches an encoded combination of your organization's webhook signature and the payload attached to the incoming request.
In your
Webhooks
page under configuration, you should see a Security module with your signature key. This key is unique to your organization and can be regenerated if it ever becomes known by an untrusted third party.
Using this key, calculate the HMAC-SHA256 of the byte-formatted payload and encode it to Base64url to get a digest. Ensure that the digest matches the X-Merge-Webhook-Signature found in the headers of the incoming POST request to confirm that the request is valid.
python
import base64
import hashlib
import hmac
# Swap YOUR_WEBHOOK_SIGNATURE_KEY below with your webhook signature key from:
# https://app.merge.dev/configuration/webhooks
signature_key = "YOUR_WEBHOOK_SIGNATURE_KEY"
raw_request_body = request.body
# Reject any requests without a signature header present
try:
webhook_signature = request.headers["X-Merge-Webhook-Signature"]
except KeyError:
print('No signature sent, request did not originate from Merge.')
raise
# Encode request body in UTF-8 and generate an HMAC digest using the webhook signature
hmac_digest = hmac.new(signature_key.encode("utf-8"), raw_request_body.encode("utf-8"), hashlib.sha256).digest()
# The generated digest must be base64 encoded before comparing
b64_encoded = base64.urlsafe_b64encode(hmac_digest).decode()
# Use hmac.compare_digest() instead of string comparison to prevent against timing attacks
doesSignatureMatch = hmac.compare_digest(b64_encoded, webhook_signature)ruby
require 'base64'
require 'openssl'
require 'json'
# Swap YOUR_WEBHOOK_SIGNATURE_KEY below with your webhook signature from:
# https://app.merge.dev/configuration/webhooks
signature_key = 'YOUR_WEBHOOK_SIGNATURE_KEY'
payload = request.raw_post
signature = request.env['HTTP_X_MERGE_WEBHOOK_SIGNATURE']
raise("No signature sent, request did not originate from Merge") unless signature
# Replace special characters with Unicode escape sequences
payload.encode!("UTF-8")
# Generate an HMAC digest using the webhook signature
hmac = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), signature_key, payload)
# The generated digest must be base64 encoded before comparing
b64_encoded = Base64.urlsafe_encode64(hmac)
# Use OpenSSL.fixed_length_secure_compare or ActiveSupport::SecurityUtils.secure_compare to prevent against timing attacks
OpenSSL.fixed_length_secure_compare(b64_encoded, signature)javascript
const crypto = require("crypto");
// Swap YOUR_WEBHOOK_SIGNATURE_KEY below with your webhook signature key from:
// https://app.merge.dev/configuration/webhooks
// Assuming request was received using Express.
// Otherwise, substitute with equivalent methods from the API you're using.
app.use(
express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString();
}
})
);
const signature_key = "YOUR_WEBHOOK_SIGNATURE_KEY"
const rawRequestBody = request.rawBody;
const signature = request.headers["x-merge-webhook-signature"]
if (!signature) {
console.log("No webhook signature, webhook is not from Merge")
}
// Encode request body with UTF-8
const encoded_body = Buffer.from(rawRequestBody, 'utf-8').toString();
// Generate a base64 encoded HMAC digest using the webhook signature
const b64_encoded = crypto.createHmac("sha256", signature_key).update(encoded_body).digest("base64").replace(/\+/g, "-").replace(/\//g, "_");
// Use crypto.timingSafeEqual() instead of string comparison to prevent against timing attacks
// Note crypto.timingSafeEqual throws a RangeError if the buffers are not the same length
const a = Buffer.from(signature)
const b = Buffer.from(b64_encoded)
console.log(Buffer.byteLength(a) === Buffer.byteLength(b) && crypto.timingSafeEqual(a, b));Webhook visibility
All Webhooks are visible as Logs, and viewable from Merge's dashboard.