Webhooks
Landed can send webhook notifications to your HTTPS endpoints when sync events occur. This document covers the payload format, event types, signature verification, and retry behavior.
Setup
- Register an endpoint:
POST /webhookswith your HTTPS URL and desired event types. - Save the signing secret returned in the response (it is only shown once, but can be retrieved via
GET /webhooks/{id}/secretor rotated viaPOST /webhooks/{id}/rotate-secret). - Implement signature verification on your server (see below).
- Send a test delivery with
POST /webhooks/{id}/testto verify connectivity.
Event Types
| Event | Trigger |
|---|---|
sync.completed | A sync finished successfully |
sync.failed | A sync failed (will be retried up to 3 times) |
sync.dead_lettered | A sync exhausted all retries and was permanently failed |
schema.changed | New fields detected or field types changed during sync |
When creating an endpoint, specify which events to subscribe to via the event_types array. Default: ["sync.completed", "sync.failed"].
Payload Format
All webhook deliveries use the same envelope structure:
{
"id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"event_type": "sync.completed",
"created_at": "2026-03-28T12:00:00+00:00",
"data": {
// Event-specific data (see below)
}
}
sync.completed / sync.failed
{
"id": "delivery-uuid",
"event_type": "sync.completed",
"created_at": "2026-03-28T12:00:00+00:00",
"data": {
"connector_id": "uuid",
"connector_name": "My Stripe Connector",
"connector_type": "stripe",
"sync_id": "uuid",
"stream": "charges",
"status": "success",
"rows_extracted": 1500,
"rows_written": 1500,
"started_at": "2026-03-28T11:55:00+00:00",
"finished_at": "2026-03-28T12:00:00+00:00"
}
}
schema.changed
{
"id": "delivery-uuid",
"event_type": "schema.changed",
"created_at": "2026-03-28T12:00:00+00:00",
"data": {
"connector_id": "uuid",
"connector_name": "My Stripe Connector",
"stream": "charges",
"changes": [
{
"field": "metadata_custom_field",
"change_type": "added",
"new_type": "string"
},
{
"field": "amount",
"change_type": "type_changed",
"old_type": "integer",
"new_type": "float"
}
]
}
}
HTTP Headers
Each webhook delivery includes these headers:
| Header | Description |
|---|---|
Content-Type | application/json |
X-Landed-Signature | HMAC-SHA256 hex digest of the request body |
X-Landed-Delivery | Unique delivery ID (matches id in payload) |
User-Agent | Landed-Webhooks/1.0 |
Signature Verification
Every delivery is signed with HMAC-SHA256 using your endpoint's signing secret. You must verify the signature to ensure the payload was sent by Landed and was not tampered with.
Algorithm
- Read the raw request body as bytes.
- Compute
HMAC-SHA256(secret, body)and hex-encode the result. - Compare with the
X-Landed-Signatureheader using a timing-safe comparison.
Python Example
import hashlib
import hmac
def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode("utf-8"),
body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
# In your endpoint handler:
body = request.body()
signature = request.headers["X-Landed-Signature"]
if not verify_webhook(body, signature, WEBHOOK_SECRET):
return Response(status_code=401)
Node.js Example
const crypto = require('crypto');
function verifyWebhook(body, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);
}
Response Requirements
- Return a
2xxstatus code to acknowledge receipt. Any non-2xx response is treated as a failure. - Respond within 5 seconds. Longer responses will time out and count as a failure.
- Redirects are not followed (
allow_redirects=False).
Retry Behavior
Failed deliveries are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | 5 seconds |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 30 minutes |
| 6 | 1 hour |
| 7 | 2 hours |
After 7 failed attempts, the delivery is marked as permanently failed.
If an endpoint accumulates too many consecutive failures, it is automatically disabled. You can re-enable it via PATCH /webhooks/{id} with {"enabled": true}.
SSRF Prevention
Webhook URLs are validated at registration and again at delivery time:
- Must use HTTPS.
- Cannot point to localhost, private IPs, or reserved IP ranges.
- DNS is re-resolved at delivery time to prevent DNS rebinding attacks.
Endpoint Limits
Each customer can register up to 10 webhook endpoints.
Delivery Log
View recent deliveries for debugging:
GET /webhooks/{id}/deliveries?limit=50
Each delivery record includes attempt count, last status code, error message, and timestamps.
Secret Rotation
Rotate your signing secret without downtime:
- Call
POST /webhooks/{id}/rotate-secret-- returns the new secret. - Update your server to accept both the old and new secrets (verify with either).
- Once confirmed, remove the old secret from your server.
The old secret is invalidated immediately upon rotation.