@better-webhook/ragie
v0.3.2
Published
Ragie module for better-webhook
Maintainers
Readme
@better-webhook/ragie
Handle Ragie webhooks with full type safety.
No more guessing payload shapes. No more manual signature verification. Just beautiful, typed webhook handlers for your Ragie RAG workflows.
import { ragie } from "@better-webhook/ragie";
const webhook = ragie().event("document_status_updated", async (payload) => {
// ✨ Full autocomplete for payload.document_id, payload.status, etc.
console.log(`Document ${payload.document_id} is now ${payload.status}`);
});Features
- 🔒 Automatic signature verification — HMAC-SHA256 verification using
X-Signatureheader - 📝 Fully typed payloads — TypeScript knows every field on every event
- ✅ Schema validated — Malformed payloads are caught and rejected
- 🎯 Multiple events — Handle document updates, sync events, and more
- 🔄 Idempotency support — Built-in
noncefield for preventing duplicate processing
Installation
npm install @better-webhook/ragie @better-webhook/core
# or
pnpm add @better-webhook/ragie @better-webhook/core
# or
yarn add @better-webhook/ragie @better-webhook/coreYou'll also need a framework adapter:
# Pick one:
npm install @better-webhook/nextjs # Next.js App Router
npm install @better-webhook/express # Express.js
npm install @better-webhook/nestjs # NestJSQuick Start
Next.js
// app/api/webhooks/ragie/route.ts
import { ragie } from "@better-webhook/ragie";
import { toNextJS } from "@better-webhook/nextjs";
const webhook = ragie()
.event("document_status_updated", async (payload) => {
console.log(`Document ${payload.document_id} is ${payload.status}`);
if (payload.status === "ready") {
// Document is fully indexed and ready for retrieval
await notifyDocumentReady(payload.document_id);
}
})
.event("connection_sync_finished", async (payload) => {
console.log(`Sync ${payload.sync_id} complete!`);
console.log(`Created: ${payload.total_creates_count}`);
console.log(`Updated: ${payload.total_contents_updates_count}`);
console.log(`Deleted: ${payload.total_deletes_count}`);
});
export const POST = toNextJS(webhook);Express
import express from "express";
import { ragie } from "@better-webhook/ragie";
import { toExpress } from "@better-webhook/express";
const app = express();
const webhook = ragie()
.event("document_status_updated", async (payload) => {
console.log(`Document ${payload.document_id} status: ${payload.status}`);
})
.event("connection_sync_started", async (payload) => {
console.log(
`Sync ${payload.sync_id} started for connection ${payload.connection_id}`,
);
});
app.post(
"/webhooks/ragie",
express.raw({ type: "application/json" }),
toExpress(webhook),
);
app.listen(3000);NestJS
import { Controller, Post, Req, Res } from "@nestjs/common";
import { Response } from "express";
import { ragie } from "@better-webhook/ragie";
import { toNestJS } from "@better-webhook/nestjs";
@Controller("webhooks")
export class WebhooksController {
private webhook = ragie()
.event("document_status_updated", async (payload) => {
console.log(`Document ${payload.document_id} is ${payload.status}`);
})
.event("connection_sync_finished", async (payload) => {
console.log(`Sync completed: ${payload.total_creates_count} documents`);
});
@Post("ragie")
async handleRagie(@Req() req: any, @Res() res: Response) {
const result = await toNestJS(this.webhook)(req);
return res.status(result.statusCode).json(result.body);
}
}Supported Events
| Event | Description |
| --------------------------- | ----------------------------------------------- |
| document_status_updated | Document enters indexed, ready, or failed state |
| document_deleted | Document is deleted |
| entity_extracted | Entity extraction completes |
| connection_sync_started | Connection sync begins |
| connection_sync_progress | Periodic sync progress updates |
| connection_sync_finished | Connection sync completes |
| connection_limit_exceeded | Connection page limit exceeded |
| partition_limit_exceeded | Partition document limit exceeded |
Event Examples
Document Status Updates
ragie().event("document_status_updated", async (payload) => {
const { document_id, status, external_id, partition, nonce } = payload;
// Use nonce for idempotency
if (await isProcessed(nonce)) {
console.log("Already processed this webhook");
return;
}
switch (status) {
case "indexed":
console.log(`Document ${document_id} indexed (semantic search ready)`);
break;
case "keyword_indexed":
console.log(`Document ${document_id} keyword indexed`);
break;
case "ready":
console.log(`Document ${document_id} fully ready`);
// All retrieval features are now functional
await notifyUserDocumentReady(external_id);
break;
case "failed":
console.error(`Document ${document_id} failed to index`);
await alertTeam(document_id);
break;
}
await markProcessed(nonce);
});Connection Sync Events
ragie()
.event("connection_sync_started", async (payload) => {
console.log(`📥 Sync started for connection ${payload.connection_id}`);
console.log(`Sync ID: ${payload.sync_id}`);
console.log(`Partition: ${payload.partition}`);
// Store sync start time
await db.syncs.create({
id: payload.sync_id,
connectionId: payload.connection_id,
startedAt: new Date(),
metadata: payload.connection_metadata,
});
})
.event("connection_sync_progress", async (payload) => {
console.log(`📊 Sync progress for ${payload.sync_id}`);
console.log(
` Created: ${payload.total_creates_count} (+${payload.created_count})`,
);
console.log(
` Updated: ${payload.total_contents_updates_count} (+${payload.contents_updated_count})`,
);
console.log(
` Deleted: ${payload.total_deletes_count} (+${payload.deleted_count})`,
);
// Update progress in database
await db.syncs.update(payload.sync_id, {
createdCount: payload.total_creates_count,
updatedCount: payload.total_contents_updates_count,
deletedCount: payload.total_deletes_count,
});
})
.event("connection_sync_finished", async (payload) => {
console.log(`✅ Sync completed: ${payload.sync_id}`);
console.log(`Final stats:`);
console.log(` - ${payload.total_creates_count} documents created`);
console.log(` - ${payload.total_contents_updates_count} contents updated`);
console.log(` - ${payload.total_metadata_updates_count} metadata updated`);
console.log(` - ${payload.total_deletes_count} documents deleted`);
// Mark sync as complete
await db.syncs.update(payload.sync_id, {
completedAt: new Date(),
finalCounts: {
created: payload.total_creates_count,
updated: payload.total_contents_updates_count,
deleted: payload.total_deletes_count,
},
});
// Notify users
await notifyUsersOfSyncCompletion(payload.connection_id);
});Entity Extraction
ragie().event("entity_extracted", async (payload) => {
console.log(`Entities extracted from document ${payload.document_id}`);
// Fetch the extracted entities via Ragie API
const entities = await ragieClient.getEntities(payload.document_id);
// Process entities
for (const entity of entities) {
await processEntity(entity);
}
});Limit Exceeded Events
ragie()
.event("connection_limit_exceeded", async (payload) => {
console.warn(`⚠️ Connection ${payload.connection_id} exceeded page limit`);
// Alert team about limit
await alertTeam({
type: "connection_limit",
connectionId: payload.connection_id,
syncId: payload.sync_id,
});
})
.event("partition_limit_exceeded", async (payload) => {
console.warn(`⚠️ Partition ${payload.partition} exceeded document limit`);
// Take action
await createNewPartition(payload.partition);
});Idempotency
Ragie includes a nonce field in all webhook payloads to help you implement idempotency:
const processedNonces = new Set<string>();
ragie().event("document_status_updated", async (payload) => {
// Check if we've already processed this webhook
if (processedNonces.has(payload.nonce)) {
console.log("Duplicate webhook, skipping");
return;
}
// Process the webhook
await processDocument(payload);
// Mark as processed
processedNonces.add(payload.nonce);
// In production, store nonces in a database with TTL
await redis.setex(`webhook:${payload.nonce}`, 86400, "1");
});Error Handling
Handle errors gracefully with built-in hooks:
const webhook = ragie()
.event("document_status_updated", async (payload) => {
await riskyOperation(payload);
})
.onError((error, context) => {
console.error(`Error handling ${context.eventType}:`, error);
console.error(`Delivery ID: ${context.deliveryId}`);
// Send to error tracking
Sentry.captureException(error, {
tags: { webhook: "ragie", event: context.eventType },
extra: { deliveryId: context.deliveryId },
});
})
.onVerificationFailed((reason, headers) => {
console.warn("Signature verification failed:", reason);
// Alert security team
alertSecurityTeam({
reason,
deliveryId: headers["x-ragie-delivery"],
});
});Configuration
Webhook Secret
Set your Ragie webhook secret via environment variable (recommended):
RAGIE_WEBHOOK_SECRET=your-signing-secret-hereYou can find your signing secret in the Ragie app under "Webhooks" after creating an endpoint.
Or pass it explicitly:
// At provider level
const webhook = ragie({ secret: "your-signing-secret" }).event(
"document_status_updated",
handler,
);
// Or at adapter level
export const POST = toNextJS(webhook, { secret: "your-signing-secret" });Success Callback
Get notified when webhooks are processed successfully:
export const POST = toNextJS(webhook, {
onSuccess: (eventType) => {
metrics.increment("webhook.ragie.success", { event: eventType });
},
});TypeScript Types
All payload types are exported for advanced use cases:
import type {
RagieDocumentStatusUpdatedEvent,
RagieConnectionSyncStartedEvent,
RagieConnectionSyncProgressEvent,
RagieConnectionSyncFinishedEvent,
RagieDocumentDeletedEvent,
RagieEntityExtractedEvent,
RagieConnectionLimitExceededEvent,
RagiePartitionLimitExceededEvent,
} from "@better-webhook/ragie";
function handleDocument(payload: RagieDocumentStatusUpdatedEvent) {
// Full type safety
}Schemas are also exported if you need them:
import {
RagieDocumentStatusUpdatedEventSchema,
RagieConnectionSyncFinishedEventSchema,
} from "@better-webhook/ragie";
// Use for custom validation
const result = RagieDocumentStatusUpdatedEventSchema.safeParse(data);Development Tips
Local Testing
Use tools like ngrok or localtunnel to expose your local server:
# Terminal 1: Start your app
npm run dev
# Terminal 2: Expose it
npx localtunnel --port 3000Then add the generated URL to your Ragie webhook endpoints.
Testing Webhooks
You can simulate webhooks in the Ragie app by clicking "Test endpoint" on any webhook endpoint.
Resources
License
MIT
