A complete walkthrough - create a project, register a webhook endpoint, trigger an event, and verify the signature.
By the end of this guide you'll have a project that emits events and a local endpoint that receives and verifies them - the core loop of every Meridian integration.
test environment (get one here)@meridian/sdk installed, or curlcurl -X POST https://api.meridian.dev/v2/projects \
-H "Authorization: Bearer $MERIDIAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "first-integration", "region": "us-east"}'import { Meridian } from "@meridian/sdk";
const client = new Meridian(process.env.MERIDIAN_API_KEY);
const project = await client.projects.create({
name: "first-integration",
region: "us-east",
});
// => prj_9kLm2xVb7nQefrom meridian import Meridian
import os
client = Meridian(os.environ["MERIDIAN_API_KEY"])
project = client.projects.create(
name="first-integration",
region="us-east",
)
# => prj_9kLm2xVb7nQeSave the returned id - every subsequent call references it.
Webhook endpoints must be HTTPS in live, but test allows plain HTTP tunnels. Start a tiny server that logs incoming events:
import http from "node:http";
import { verifyWebhookSignature } from "@meridian/sdk/webhooks";
const SECRET = process.env.WEBHOOK_SECRET; // whsec_…
http
.createServer((req, res) => {
let body = "";
req.on("data", (c) => (body += c));
req.on("end", () => {
const signature = req.headers["meridian-signature"];
const event = verifyWebhookSignature(body, signature, SECRET);
console.log("received:", event.type, event.data.id);
res.writeHead(200).end("ok");
});
})
.listen(3000);import os
from flask import Flask, request
from meridian.webhooks import verify_signature
app = Flask(__name__)
SECRET = os.environ["WEBHOOK_SECRET"] # whsec_…
@app.post("/webhooks")
def receive():
event = verify_signature(
request.data, request.headers["Meridian-Signature"], SECRET
)
print("received:", event["type"], event["data"]["id"])
return "ok", 200
app.run(port=3000)ngrok http 3000
# Forwarding https://a1b2c3.ngrok.app -> http://localhost:3000Register that URL as a webhook endpoint subscribed to the events you care about:
curl -X POST https://api.meridian.dev/v2/webhooks \
-H "Authorization: Bearer $MERIDIAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"project_id": "prj_9kLm2xVb7nQe",
"url": "https://a1b2c3.ngrok.app/webhooks",
"events": ["project.updated", "sync.completed"]
}'const endpoint = await client.webhooks.create({
projectId: "prj_9kLm2xVb7nQe",
url: "https://a1b2c3.ngrok.app/webhooks",
events: ["project.updated", "sync.completed"],
});
// endpoint.secret => whsec_… (shown once - store it)endpoint = client.webhooks.create(
project_id="prj_9kLm2xVb7nQe",
url="https://a1b2c3.ngrok.app/webhooks",
events=["project.updated", "sync.completed"],
)
# endpoint.secret => whsec_… (shown once - store it)The response contains secret (whsec_…) - the signing secret for this endpoint. It's shown once; put it in WEBHOOK_SECRET.
Update the project to emit project.updated:
curl -X PATCH https://api.meridian.dev/v2/projects/prj_9kLm2xVb7nQe \
-H "Authorization: Bearer $MERIDIAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "first-integration-v2"}'Within a second or two your server should log:
received: project.updated evt_3kR8mPx2vLEvery delivery is recorded, including failures and retries:
curl "https://api.meridian.dev/v2/webhooks/wh_2nQ8kLm4xP/deliveries?limit=5" \
-H "Authorization: Bearer $MERIDIAN_API_KEY"{
"object": "list",
"data": [
{
"id": "dlv_9fH3kQm7xT",
"event_id": "evt_3kR8mPx2vL",
"status": "succeeded",
"response_status": 200,
"duration_ms": 182,
"attempts": 1,
"delivered_at": "2026-09-17T10:41:22Z"
}
]
}