🎣better-webhook
SDK

Providers

Documentation for GitHub and Ragie webhook providers with all supported events.

Providers

Providers define the webhook source, including event types, payload schemas, and signature verification.

GitHub

bash npm install @better-webhook/github
bash pnpm add @better-webhook/github
bash yarn add @better-webhook/github
import { github } from "@better-webhook/github";

const webhook = github({ secret: process.env.GITHUB_WEBHOOK_SECRET }).event(
  "push",
  async (payload) => {
    console.log(`Push to ${payload.repository.full_name}`);
  },
);

Events

push

Triggered when commits are pushed to a repository branch or tag.

.event("push", async (payload) => {
  console.log(payload.ref);              // "refs/heads/main"
  console.log(payload.repository.name);  // "my-repo"
  console.log(payload.commits.length);   // Number of commits
  console.log(payload.pusher.name);      // Who pushed

  for (const commit of payload.commits) {
    console.log(commit.message);
    console.log(commit.author.email);
  }
})

Key payload fields:

  • ref — Full git ref (e.g., refs/heads/main)
  • before / after — Commit SHAs before and after push
  • commits — Array of commit objects
  • repository — Repository information
  • pusher — User who pushed

pull_request

Triggered when a pull request is opened, closed, merged, edited, etc.

.event("pull_request", async (payload) => {
  console.log(payload.action);                    // "opened", "closed", etc.
  console.log(payload.number);                    // PR number
  console.log(payload.pull_request.title);        // PR title
  console.log(payload.pull_request.state);        // "open" or "closed"
  console.log(payload.pull_request.merged_at);    // Merge timestamp (if merged)

  if (payload.action === "opened") {
    // New PR opened
  }
})

Key payload fields:

  • action — What happened (opened, closed, synchronize, labeled, etc.)
  • number — Pull request number
  • pull_request — Full PR object with title, body, state, head, base
  • repository — Repository information
  • sender — User who triggered the event

issues

Triggered when an issue is opened, closed, edited, labeled, etc.

.event("issues", async (payload) => {
  console.log(payload.action);           // "opened", "closed", etc.
  console.log(payload.issue.number);     // Issue number
  console.log(payload.issue.title);      // Issue title
  console.log(payload.issue.state);      // "open" or "closed"
  console.log(payload.issue.labels);     // Array of labels
})

Key payload fields:

  • action — What happened (opened, closed, edited, labeled, etc.)
  • issue — Full issue object with number, title, body, state, labels
  • repository — Repository information
  • sender — User who triggered the event

installation

Triggered when a GitHub App is installed, uninstalled, or has permissions changed.

.event("installation", async (payload) => {
  console.log(payload.action);                      // "created", "deleted", etc.
  console.log(payload.installation.id);             // Installation ID
  console.log(payload.installation.account.login);  // Account name
  console.log(payload.repositories);                // Repos (for "created" action)
})

Key payload fields:

  • action — created, deleted, suspend, unsuspend, new_permissions_accepted
  • installation — Installation details with id, account, permissions
  • repositories — Array of accessible repos (only for created action)

installation_repositories

Triggered when repositories are added to or removed from an installation.

.event("installation_repositories", async (payload) => {
  console.log(payload.action);                // "added" or "removed"
  console.log(payload.repositories_added);    // Repos added
  console.log(payload.repositories_removed);  // Repos removed
})

Signature Verification

GitHub uses HMAC-SHA256 signatures sent in the X-Hub-Signature-256 header. The SDK verifies this automatically when a secret is configured.


Ragie

bash npm install @better-webhook/ragie
bash pnpm add @better-webhook/ragie
bash yarn add @better-webhook/ragie
import { ragie } from "@better-webhook/ragie";

const webhook = ragie({ secret: process.env.RAGIE_WEBHOOK_SECRET }).event(
  "document_status_updated",
  async (payload) => {
    console.log(`Document ${payload.document_id} is now ${payload.status}`);
  },
);

Ragie webhooks use an envelope structure where the event type is in body.type and the actual payload is in body.payload. Ragie also includes a body.nonce for idempotency — the SDK attaches this onto the unwrapped payload as payload.nonce for convenience.

Events

document_status_updated

Triggered when a document enters indexed, keyword_indexed, ready, or failed state.

.event("document_status_updated", async (payload) => {
  console.log(payload.document_id);   // Document ID
  console.log(payload.status);        // "indexed", "ready", "failed", etc.
  console.log(payload.name);          // Document name
  console.log(payload.external_id);   // Your external ID (if provided)
  console.log(payload.error);         // Error message (if status is "failed")
})

Key payload fields:

  • document_id — Unique document identifier
  • nonce — Unique idempotency key for this webhook delivery
  • status — indexed, keyword_indexed, ready, or failed
  • name — Document name
  • partition — Partition key
  • metadata — User-defined metadata (nullable)
  • external_id — Your external ID (nullable)
  • connection_id — Connection ID if created via connection (nullable)
  • sync_id — Sync ID if part of a sync (nullable)
  • error — Error message if status is failed (nullable)

document_deleted

Triggered when a document is deleted.

.event("document_deleted", async (payload) => {
  console.log(payload.document_id);
  console.log(payload.name);
  console.log(payload.external_id);
})

Key payload fields:

  • document_id — Unique document identifier
  • name — Document name
  • partition — Partition key
  • metadata — User-defined metadata (nullable)
  • external_id — Your external ID (nullable)
  • connection_id — Connection ID (nullable)
  • sync_id — Sync ID (nullable)

entity_extracted

Triggered when entity extraction completes for a document.

.event("entity_extracted", async (payload) => {
  console.log(payload.entity_id);           // Extracted entity ID
  console.log(payload.document_id);         // Source document ID
  console.log(payload.document_name);       // Source document name
  console.log(payload.instruction_id);      // Extraction instruction ID
  console.log(payload.data);                // Extracted entity data
})

Key payload fields:

  • entity_id — Unique identifier for the extracted entity
  • document_id — Source document ID
  • instruction_id — Instruction ID used for extraction
  • document_name — Source document name
  • document_external_id — External ID of source document
  • document_metadata — Metadata from source document
  • partition — Partition key
  • sync_id — Sync ID (nullable)
  • data — The extracted entity data object

connection_sync_started

Triggered when a connection sync begins.

.event("connection_sync_started", async (payload) => {
  console.log(payload.connection_id);
  console.log(payload.sync_id);
  console.log(payload.partition);
  console.log(`Will create ${payload.create_count} documents`);
  console.log(`Will delete ${payload.delete_count} documents`);
})

Key payload fields:

  • connection_id — Connection identifier
  • sync_id — Sync identifier
  • partition — Partition key
  • create_count — Number of documents to be created
  • update_content_count — Number of documents with content updates
  • update_metadata_count — Number of documents with metadata updates
  • delete_count — Number of documents to be deleted

connection_sync_progress

Triggered periodically during a sync to report progress.

.event("connection_sync_progress", async (payload) => {
  console.log(`Created: ${payload.created_count}/${payload.create_count}`);
  console.log(`Deleted: ${payload.deleted_count}/${payload.delete_count}`);
  console.log(`Errors: ${payload.errored_count}`);
})

Key payload fields:

  • connection_id — Connection identifier
  • sync_id — Sync identifier
  • partition — Partition key
  • create_count / created_count — Total to create / created so far
  • update_content_count / updated_content_count — Content updates total / completed
  • update_metadata_count / updated_metadata_count — Metadata updates total / completed
  • delete_count / deleted_count — Total to delete / deleted so far
  • errored_count — Number of documents with errors

connection_sync_finished

Triggered when a connection sync completes.

.event("connection_sync_finished", async (payload) => {
  console.log(`Sync ${payload.sync_id} finished`);
  console.log(`Connection: ${payload.connection_id}`);
})

Key payload fields:

  • connection_id — Connection identifier
  • sync_id — Sync identifier
  • partition — Partition key

connection_limit_exceeded

Triggered when a connection exceeds its page limit.

.event("connection_limit_exceeded", async (payload) => {
  console.log(`Connection ${payload.connection_id} hit ${payload.limit_type} limit`);
})

Key payload fields:

  • connection_id — Connection identifier
  • partition — Partition key
  • limit_type — Type of limit exceeded (e.g., "page_limit")

partition_limit_exceeded

Triggered when a partition exceeds its document limit.

.event("partition_limit_exceeded", async (payload) => {
  console.log(`Partition ${payload.partition} hit limit`);
})

Key payload fields:

  • partition — Partition key
  • limit_type — Type of limit exceeded (if provided)
  • nonce — Unique idempotency key for this webhook delivery

Signature Verification

Ragie uses HMAC-SHA256 signatures sent in the X-Signature header.


Custom Providers

For webhook sources not covered by built-in providers, you can create custom providers using @better-webhook/core:

import { createProvider, createHmacVerifier, z } from "@better-webhook/core";

const OrderSchema = z.object({
  orderId: z.string(),
  status: z.enum(["pending", "completed", "cancelled"]),
  amount: z.number(),
});

const myProvider = createProvider({
  name: "my-ecommerce",
  schemas: {
    "order.created": OrderSchema,
    "order.updated": OrderSchema,
  },
  getEventType: (headers) => headers["x-event-type"],
  getDeliveryId: (headers) => headers["x-delivery-id"],
  verify: createHmacVerifier({
    algorithm: "sha256",
    signatureHeader: "x-signature",
    signaturePrefix: "sha256=",
  }),
});

The createHmacVerifier helper supports common signature formats. For custom verification logic, provide your own verify function.

Envelope Payloads

Some webhook providers wrap the actual payload in an envelope structure. For example, a provider might send:

{
  "type": "order.created",
  "payload": { "orderId": "123", "status": "pending", "amount": 99.99 },
  "timestamp": "2024-01-01T00:00:00Z"
}

Use getEventType with the optional body parameter and getPayload to handle this:

const envelopeProvider = createProvider({
  name: "my-envelope-provider",
  schemas: {
    "order.created": OrderSchema,
    "order.updated": OrderSchema,
  },
  // Extract event type from body instead of headers
  getEventType: (headers, body) => {
    if (body && typeof body === "object" && "type" in body) {
      return (body as { type: string }).type;
    }
    return undefined;
  },
  // Extract the actual payload from the envelope
  getPayload: (body) => {
    if (body && typeof body === "object" && "payload" in body) {
      return (body as { payload: unknown }).payload;
    }
    return body;
  },
  verify: createHmacVerifier({
    algorithm: "sha256",
    signatureHeader: "x-signature",
  }),
});

With this configuration, your event handlers receive the unwrapped payload directly, and schema validation applies to the inner payload object.

On this page