Skip to main content

Webhooks

Webhooks send an HTTPS POST to your endpoint when something happens in your Matia account, such as a sync finishing or an integration being disabled. Use them to trigger downstream jobs, update your own UI, or alert your team without polling the Matia API. A webhook receives the events you choose for every integration and asset in your account.

Availability

Webhooks are enabled per account. To turn them on, contact Matia Support. Until they're enabled, every Webhooks API call returns 404.

Looking for issue alerts?

This page covers event webhooks, which you manage through the API. To send issue alerts to a webhook URL, see Notification Integrations.

Set up a webhook

1. Prepare your endpoint

Your endpoint must:

  • Use https://. Matia rejects http:// URLs.
  • Resolve only to public IP addresses. If any address the hostname resolves to is private, loopback, link-local, or otherwise reserved, the request is rejected.
  • Respond with a 2xx status within 10 seconds. Anything else counts as a failed delivery, including redirects (3xx), which Matia doesn't follow.

To test from your own machine, expose your local server through a tunnel such as ngrok or Cloudflare Tunnel.

If you'll set a secret, configure your endpoint with it first: the test request Matia sends when you create the webhook is already signed.

2. Create the webhook

curl -X POST https://api.matia.io/v1/webhooks/account \
-H "x-api-key: $MATIA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/matia/webhooks",
"events": ["syncEnd", "integrationDisable"],
"active": true,
"secret": "a-long-random-string"
}'
FieldRequiredDescription
urlYesYour HTTPS endpoint. Each URL can be used by only one webhook in your account.
eventsYesOne or more events, without duplicates.
activeYestrue to start delivering immediately. false creates the webhook disabled.
secretNoUsed to sign every request. Strongly recommended. Generate one with openssl rand -hex 32.

When active is true, Matia sends a test event to the URL before creating the webhook. If it fails, the webhook isn't created and the API returns a 400. To create the webhook before your endpoint is ready, set active to false and activate it later.

The response contains the new webhook. Matia never returns your secret: secret is "********" when one is set and null otherwise.

{
"code": "success",
"message": "Webhook created successfully",
"data": {
"id": "671a2f0c9e4b8d1a2c3e4f50",
"type": "account",
"url": "https://example.com/matia/webhooks",
"events": ["syncEnd", "integrationDisable"],
"active": true,
"secret": "********",
"createdAt": "2026-10-14T12:00:00.000Z",
"createdBy": "api"
}
}

3. Test the webhook

Call Test a webhook at any time to send a test event and see the result. It works on inactive webhooks too, so you can check a fix before reactivating.

{
"code": "success",
"message": "Webhook test completed",
"data": {
"succeed": false,
"status": 500,
"message": "Webhook endpoint test failed with HTTP 500"
}
}

A failed test still returns HTTP 200, with succeed: false. status is omitted when your endpoint didn't respond at all. Testing doesn't change the webhook or count toward automatic deactivation.

Events

EventSent whendata
syncStartA sync run starts.syncMode: how the run is expected to load tables. See below.
syncEndA sync run finishes, whatever the outcome.status: the outcome. See below.
loadedRows: the run's committed records, meaning rows written and confirmed in the destination.
message (optional): details reported for the run, such as an error.
integrationCreateSuccessA new integration finishes being created.{}
integrationCreateFailureCreating an integration fails after Matia has accepted the request. Requests rejected up front, for example for invalid settings, don't send this event.errorMessage (optional): why creation failed.
integrationEnableA disabled integration is enabled.{}
integrationDisableAn enabled integration is disabled.reason (optional): present only when Matia disabled the integration automatically because its syncs kept failing. Meant for display; don't parse it.
integrationDeleteAn integration is deleted.{}
integrationResyncA Historical Data Re-Sync of the whole integration starts. Sent just before the syncStart of the run that performs it, with the same syncId. Re-syncing individual tables doesn't send this event.{}
assetCreateAn asset is created, whether a source, a destination, or another connection such as a BI tool or dbt. Also sent when an asset saved as a draft is connected.{}

You can't subscribe to test. Matia sends it only to check your endpoint.

syncStart sync mode

syncMode is Matia's prediction, when the run starts, of how it will load tables. Individual tables can still be reloaded during the run. See Sync Modes.

ValueMeaning
incrementalEvery table loads only new or changed records.
fullRefreshEvery table is reloaded. This includes the first run of a new integration, since no table has a saved position yet.
mixedSome tables are reloaded and others load incrementally.

syncEnd status

ValueShown in Matia asMeaning
SUCCESSSucceededEvery table synced.
FAILUREFailedEvery table failed, or the run couldn't complete.
PARTIALIncompleteSome tables failed, or a post-run action failed.
COMPLETED_WITH_ERRORSCompleted with errorsThe run finished, but some records were rejected or invalid.
ABORTEDAbortedThe run was stopped.

Payloads

Every request has a JSON body. Top-level fields identify the event and what it's about. data holds the event-specific details.

On every event:

FieldDescription
idUnique ID of the event. Stays the same across retries, so use it to ignore duplicates.
eventThe event name.
createdWhen the event happened, in ISO 8601 UTC.
dataEvent-specific details. {} when there are none.

On every event except assetCreate and test:

FieldDescription
integrationId, integrationNameThe integration's ID and name.
sourceType, sourceIdThe source's connector type, such as shopify, and its asset ID.
destinationType, destinationIdThe destination's connector type, such as snowflake, and its asset ID.
syncIdID of the sync run. Only on syncStart, syncEnd, and integrationResync.

On assetCreate only:

FieldDescription
assetId, assetNameThe asset's ID and name.
assetTypeThe asset's connector type, such as snowflake.
assetRoleHow the asset is used: ETL_SOURCE, ETL_DESTINATION, RETL_SOURCE, RETL_DESTINATION, or MULTI_PURPOSE. null for assets that can't be an integration source, such as BI tools and dbt.

Example payloads

{
"id": "9b2f6c1e-5d4a-4f3b-8e7a-2c1d0f9e8b7a",
"event": "syncEnd",
"created": "2026-10-14T12:05:42.118Z",
"integrationId": "66f1b2c3d4e5f60718293a4b",
"integrationName": "Shopify to Snowflake",
"sourceType": "shopify",
"sourceId": "66f1a0b1c2d3e4f506172839",
"destinationType": "snowflake",
"destinationId": "66f1a0b1c2d3e4f50617283a",
"syncId": "671a3b5d7e9f0a1b2c3d4e5f",
"data": {
"status": "FAILURE",
"loadedRows": 0,
"message": "Authentication failed: the access token has expired."
}
}

Request headers

HeaderValue
Content-Typeapplication/json
X-Matia-EventThe event name, the same as event in the body.
X-Matia-Event-IdThe event ID, the same as id in the body.
X-Matia-Delivery-Attempt1 for the first attempt, then 2 to 6 for retries.
X-Matia-SignatureThe signature. Only sent when the webhook has a secret.

Verify signatures

When a webhook has a secret, every request, including test events, carries an X-Matia-Signature header:

X-Matia-Signature: t=1792065942,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t is the Unix time, in seconds, when the request was signed. Every attempt is signed afresh, so retries carry a current timestamp.
  • v1 is the hex-encoded HMAC-SHA256 of <t>.<raw request body>, keyed with your secret.

To verify a request:

  1. Split the header on , and read t and v1.
  2. Compute the HMAC-SHA256 of t, a period, and the raw request body exactly as received. Parsing and re-serializing the JSON changes the bytes and breaks the signature.
  3. Compare your result with v1 using a constant-time comparison.
  4. Reject the request if t is more than a few minutes old, so a captured request can't be replayed later.
import crypto from 'node:crypto';
import express from 'express';

const SECRET = process.env.MATIA_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;

function isValidSignature(rawBody, header) {
if (!header || !Buffer.isBuffer(rawBody)) return false;
const parts = Object.fromEntries(header.split(',').map((part) => part.split('=')));
const timestamp = Number(parts.t);
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;

const expected = crypto.createHmac('sha256', SECRET).update(`${parts.t}.`).update(rawBody).digest();
const received = Buffer.from(parts.v1 ?? '', 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

// express.raw keeps the body as the exact bytes Matia signed.
app.post('/matia/webhooks', express.raw({ type: 'application/json' }), async (req, res) => {
if (!isValidSignature(req.body, req.get('X-Matia-Signature'))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
if (event.event !== 'test') {
try {
await enqueue(event); // Process in a background job that skips event IDs it has already handled.
} catch {
return res.sendStatus(500); // Matia retries.
}
}
res.sendStatus(200);
});

To rotate a secret, update the webhook with a new secret. If the webhook is active, Matia tests your endpoint with the new secret before saving it. From then on, every request, including retries of earlier events, is signed with the new secret. Accept both secrets on your endpoint during the switch.

Delivery and retries

Retries

A delivery fails if your endpoint doesn't respond with a 2xx within 10 seconds or can't be reached. Matia retries it on a fixed schedule, measured from the first attempt:

AttemptTime after the first attempt
1Immediately
26 minutes
321 minutes
41 hour 45 minutes
56 hours 25 minutes
6 (last)24 hours

After the 6th failed attempt, Matia stops retrying that event. Each event is retried independently, so a failing delivery doesn't hold back later events.

What to expect

  • Duplicates. If your endpoint processes an event but doesn't respond with a 2xx in time, Matia sends it again. Use the event id to skip events you've already handled.
  • Any order. Events can arrive out of order, especially after retries. Use created to order them, and syncId to match a syncStart with its syncEnd.
  • Gaps. Webhooks are best-effort. For example, a syncStart isn't always followed by a syncEnd. For anything critical, confirm with the API, for example with Get the status of a specific integration run, passing integrationId and syncId as the run ID.
  • Respond fast. Verify the request, hand the event to a background job, and return a 2xx, as the samples above do. If queuing fails, return an error so Matia retries.

Automatic deactivation

If deliveries to a webhook keep failing for 72 hours without a single success, Matia deactivates the webhook and cancels its pending deliveries. Events that happen while a webhook is inactive aren't delivered later.

Matia doesn't notify you when this happens. If events stop arriving, check active with Get a webhook.

To reactivate, fix your endpoint, then update the webhook with "active": true. You can change the url in the same request. Matia tests the endpoint first and reactivates the webhook only if the test succeeds.

Disabling a webhook yourself ("active": false) also stops new events from being queued. A pending retry is cancelled if it comes due while the webhook is disabled; if you reactivate first, it's still delivered.

Manage webhooks

All requests go to https://api.matia.io/v1 and authenticate with your API key in the x-api-key header.

ActionEndpoint
List webhooksGET /webhooks
Create a webhookPOST /webhooks/account
Get a webhookGET /webhooks/{id}
Update a webhookPATCH /webhooks/{id}
Delete a webhookDELETE /webhooks/{id}
Test a webhookPOST /webhooks/{id}/test

Updates change only the fields you send. Matia sends a test event before saving an update that reactivates a webhook, or that changes the url or secret of an active one. Changing only events doesn't. Deleting a webhook also deletes its pending deliveries.

Troubleshooting

SymptomCauseFix
404 on every Webhooks API callWebhooks aren't enabled for your account.Contact Matia Support.
400 with BadRequest_WebhookEndpointTestFailedThe test event failed. If your endpoint responded, message includes the HTTP status, for example Webhook endpoint test failed with HTTP 401. If it didn't respond, message is just Webhook endpoint test failed: it timed out after 10 seconds, couldn't be reached, or resolves to a non-public address.Fix the endpoint and retry. A 401 usually means your endpoint doesn't have the webhook's secret yet.
409 with Conflict_WebhookUrlAnother webhook in your account already uses this URL.Update the existing webhook, or use a different URL.
Events stopped arrivingThe webhook is inactive, usually because it was deactivated after failing for 72 hours. Or the event was removed from events.Check active and events with Get a webhook. Fix the endpoint, then set "active": true.
Signature doesn't matchThe body was parsed or re-encoded before verifying, or the secret is wrong.See Verify signatures.