Skip to content
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

FieldTypeDescription
hookObjectThe webhook that was triggered
linked_accountObjectThe user whose data has changed
dataObjectThe 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.

Unofficial documentation reference. Built for internal use.