Astrocal
Guides

Webhooks

Receive real-time HTTP notifications when booking events occur.

Webhooks let you receive real-time HTTP POST notifications when events occur in your Astrocal account. Instead of polling the API, register a webhook endpoint and Astrocal will push events to your server as they happen.

Event Types

EventTrigger
booking.createdA new booking is created (may be pending payment)
booking.confirmedA booking transitions to confirmed (after payment, if paid)
booking.cancelledAn existing booking is cancelled
booking.rescheduledAn existing booking is rescheduled
waitlist.promotedA waitlist entry is promoted when a booking is cancelled
waitlist.expiredA waitlist entry expires after its TTL

Creating a Webhook Endpoint

Register a URL to receive events:

curl -X POST https://api.astrocal.dev/v1/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/astrocal",
    "events": ["booking.created", "booking.cancelled"]
  }'
const response = await fetch("https://api.astrocal.dev/v1/webhooks", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://your-app.com/webhooks/astrocal",
    events: ["booking.created", "booking.cancelled"],
  }),
});
const data = await response.json();

Try it in the API playground →

The response includes a secret field. Save this immediately. The secret is only shown once at creation time and is used to verify webhook signatures.

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "url": "https://your-app.com/webhooks/astrocal",
  "events": ["booking.created", "booking.cancelled"],
  "secret": "whsec_abc123...",
  "active": true,
  "created_at": "2026-03-01T10:00:00.000Z",
  "updated_at": "2026-03-01T10:00:00.000Z"
}

Payload Format

Each webhook delivery sends a JSON payload with this structure:

{
  "event": "booking.created",
  "data": {
    "id": "770e8400-e29b-41d4-a716-446655440000",
    "organization_id": "550e8400-e29b-41d4-a716-446655440000",
    "event_type_id": "660e8400-e29b-41d4-a716-446655440000",
    "status": "confirmed",
    "start_time": "2026-03-15T14:00:00.000Z",
    "end_time": "2026-03-15T14:30:00.000Z",
    "invitee_name": "Jane Smith",
    "invitee_email": "jane@example.com",
    "invitee_timezone": "America/New_York",
    "notes": null,
    "created_at": "2026-03-01T10:00:00.000Z",
    "updated_at": "2026-03-01T10:00:00.000Z"
  },
  "created_at": "2026-03-01T10:00:00.123Z"
}

Verifying Signatures

Every webhook delivery includes an X-Astrocal-Signature header containing an HMAC-SHA256 signature of the request body. Always verify this signature to ensure the webhook came from Astrocal and wasn't tampered with.

The header format is v1=<hex-digest>.

Node.js / TypeScript Example

import crypto from "node:crypto";

function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean {
  const expectedSig = crypto.createHmac("sha256", secret).update(payload).digest("hex");

  const expected = `v1=${expectedSig}`;

  // Use timing-safe comparison to prevent timing attacks
  if (signature.length !== expected.length) return false;
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

// In your webhook handler:
app.post("/webhooks/astrocal", (req, res) => {
  const signature = req.headers["x-astrocal-signature"];
  const rawBody = req.body; // Must be the raw string, not parsed JSON

  if (!verifyWebhookSignature(rawBody, signature, WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(rawBody);
  // Handle the event...

  res.status(200).send("OK");
});

Headers

Each webhook delivery includes these headers:

HeaderDescription
Content-TypeAlways application/json
X-Astrocal-SignatureHMAC-SHA256 signature (v1=<hex>)
X-Astrocal-EventThe event type (e.g. booking.created)

Retry Behavior

If your endpoint returns a non-2xx status code or the request times out (10 seconds), Astrocal will retry with exponential backoff:

AttemptDelay After
1Immediate
21 minute
35 minutes
430 minutes
52 hours
624 hours

After 6 failed attempts, the delivery is marked as permanently failed. You can view delivery history via the API.

Managing Webhooks

You can manage webhooks from the dashboard or via the API.

Dashboard

The Webhooks page in the developer dashboard lets you:

  • Create, edit, and delete webhook endpoints
  • Toggle endpoints active/inactive
  • View delivery logs with status, HTTP response codes, and retry counts
  • Copy the signing secret on creation

Navigate to Dashboard > Webhooks to get started.

List Endpoints

curl https://api.astrocal.dev/v1/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY"
const response = await fetch("https://api.astrocal.dev/v1/webhooks", {
  headers: {
    Authorization: "Bearer YOUR_API_KEY",
  },
});
const data = await response.json();

Try it in the API playground →

Update an Endpoint

curl -X PATCH https://api.astrocal.dev/v1/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"active": false}'
const response = await fetch(
  "https://api.astrocal.dev/v1/webhooks/WEBHOOK_ID",
  {
    method: "PATCH",
    headers: {
      Authorization: "Bearer YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ active: false }),
  }
);
const data = await response.json();

Try it in the API playground →

Delete an Endpoint

curl -X DELETE https://api.astrocal.dev/v1/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer YOUR_API_KEY"
const response = await fetch(
  "https://api.astrocal.dev/v1/webhooks/WEBHOOK_ID",
  {
    method: "DELETE",
    headers: {
      Authorization: "Bearer YOUR_API_KEY",
    },
  }
);

Try it in the API playground →

View Delivery History

curl "https://api.astrocal.dev/v1/webhooks/WEBHOOK_ID/deliveries?status=failed" \
  -H "Authorization: Bearer YOUR_API_KEY"
const response = await fetch(
  "https://api.astrocal.dev/v1/webhooks/WEBHOOK_ID/deliveries?status=failed",
  {
    headers: {
      Authorization: "Bearer YOUR_API_KEY",
    },
  }
);
const data = await response.json();

Try it in the API playground →

Best Practices

  1. Always verify signatures. Never trust webhook payloads without signature verification.
  2. Respond quickly. Return a 2xx response within 10 seconds. Process the event asynchronously if needed.
  3. Handle duplicates. In rare cases, the same event may be delivered more than once. Use the event data.id for idempotency.
  4. Use HTTPS. Always use HTTPS URLs for your webhook endpoints in production.
  5. Monitor deliveries. Check the dashboard or the deliveries endpoint for failed deliveries.

On this page