xuly.io

Webhooks

Signed HTTP callbacks when things happen in your workspace. Subscribe to 40+ events.

Subscribing

Create webhook subscriptions in Settings → Webhooks or via the REST API endpoint POST /v1/webhooks. Each subscription has a URL, a set of event names, and a signing secret generated at creation.

Event catalog

  • sync.completed — a sync run finished, regardless of success.
  • sync.failed — a sync run failed after retries.
  • anomaly.detected — AI layer flagged a KPI outside baseline.
  • integration.created, integration.updated, integration.broken
  • stats.daily_ready — daily rollup completed, numbers are stable.
  • invoice.generated, invoice.paid, invoice.overdue
  • sub_affiliate.joined, sub_affiliate.paid
  • automation.fired — any rule fired.

Delivery guarantees

We retry failed deliveries with exponential backoff: 30s, 2m, 10m, 1h, 6h, 24h — 6 attempts total. If all fail, the delivery is marked dead and appears in Settings → Webhooks → Dead lettersfor manual replay. Successful delivery means your endpoint returned a 2xx within 10 seconds.

Signature verification

Every payload is signed with HMAC-SHA256 using your webhook's signing secret. The signature is sent in the X-xuly.io-Signature header as t=<timestamp>,v1=<hex>.

import crypto from 'node:crypto';

function verify(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((p) => p.split('=')),
  );
  const expected = crypto
    .createHmac('sha256', secret)
    .update(parts.t + '.' + rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(parts.v1),
  );
}

Example payload

{
  "id": "evt_01HZXY...",
  "type": "sync.completed",
  "created": "2026-04-22T14:03:11Z",
  "data": {
    "integration_id": "int_abc123",
    "brand_slug": "binance-affiliates",
    "status": "success",
    "rows_ingested": 48,
    "duration_ms": 2140
  }
}

Replaying events

Go to Settings → Webhooks → [Your endpoint] → History, filter to "Delivery failed", and click Retry on any row. Replays use the same event ID, so handlers should be idempotent.