Skip to main content

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

  1. Register an endpoint: POST /webhooks with your HTTPS URL and desired event types.
  2. Save the signing secret returned in the response (it is only shown once, but can be retrieved via GET /webhooks/{id}/secret or rotated via POST /webhooks/{id}/rotate-secret).
  3. Implement signature verification on your server (see below).
  4. Send a test delivery with POST /webhooks/{id}/test to verify connectivity.

Event Types

EventTrigger
sync.completedA sync finished successfully
sync.failedA sync failed (will be retried up to 3 times)
sync.dead_letteredA sync exhausted all retries and was permanently failed
schema.changedNew 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:

HeaderDescription
Content-Typeapplication/json
X-Landed-SignatureHMAC-SHA256 hex digest of the request body
X-Landed-DeliveryUnique delivery ID (matches id in payload)
User-AgentLanded-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

  1. Read the raw request body as bytes.
  2. Compute HMAC-SHA256(secret, body) and hex-encode the result.
  3. Compare with the X-Landed-Signature header 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 2xx status 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:

AttemptDelay
15 seconds
230 seconds
32 minutes
410 minutes
530 minutes
61 hour
72 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:

  1. Call POST /webhooks/{id}/rotate-secret -- returns the new secret.
  2. Update your server to accept both the old and new secrets (verify with either).
  3. Once confirmed, remove the old secret from your server.

The old secret is invalidated immediately upon rotation.