Integrate AI Docs with other systems using webhooks for automated workflows and notifications.
Webhooks are a fundamental mechanism for enabling real-time automation and integration between AI Docs and external services. They allow AI Docs to react instantly to events happening in your connected GitHub repositories or billing providers, ensuring your documentation stays up-to-date and your subscription status is accurately managed without manual intervention.
AI Docs leverages GitHub webhooks to automatically synchronize your documentation with changes in your codebase. This ensures that your generated documentation always reflects the latest state of your project, saving you time and effort in manual updates.
When you connect a GitHub repository to AI Docs, especially for Pro/Enterprise plans, AI Docs attempts to automatically configure a webhook in your repository settings. This webhook is set up to notify AI Docs whenever specific events occur in your repository, primarily push events to your default branch.
The process for handling GitHub push events involves several key steps:
POST request to the AI Docs webhook endpoint (/api/webhooks/github) whenever code is pushed to your repository.webhookSecret generated and stored securely. This secret is used to compute an HMAC SHA-256 signature of the webhook payload, which is then compared against the x-hub-signature-256 header provided by GitHub. This step is crucial for security, preventing spoofed or malicious requests.ref), and crucially, the list of added, modified, and removed files within the commits.main) are processed to avoid unnecessary documentation regeneration from feature branches or temporary commits.For more details on how AI Docs connects with GitHub and the automatic webhook setup, refer to GitHub Integration and Creating a New Project. The subsequent AI-powered generation process is detailed in AI-Powered Generation and How AI Docs Works.
The following TypeScript code illustrates the core logic for handling GitHub push webhooks, including signature verification and triggering the incremental sync.
// app/api/webhooks/github/route.ts
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { projects } from "@/lib/db/schema";
import { eq, and } from "drizzle-orm";
import { inngest } from "@/lib/inngest/client";
import { getUserGitHubToken } from "@/lib/github/user-token";
export async function POST(req: Request) {
const event = req.headers.get("x-github-event");
const signature = req.headers.get("x-hub-signature-256");
const rawBody = await req.text();
let payload: any;
// Only handle push events
if (event !== "push") {
return NextResponse.json({ message: "Event ignored" }, { status: 200 });
}
try {
payload = JSON.parse(rawBody);
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
const repoOwner = payload.repository?.owner?.login || payload.repository?.owner?.name;
const repoName = payload.repository?.name;
if (!repoOwner || !repoName) {
return NextResponse.json({ error: "Missing repository info" }, { status: 400 });
}
// Look up the project by repo owner + name to get its webhook secret
const project = await db.query.projects.findFirst({
where: and(
eq(projects.repoOwner, repoOwner),
eq(projects.repoName, repoName)
),
});
if (!project) {
return NextResponse.json({ error: "Project not found" }, { status: 404 });
}
// Verify webhook signature
if (!project.webhookSecret || !signature) {
return NextResponse.json({ error: "Webhook not configured or missing signature" }, { status: 401 });
}
const isValid = await verifySignature(rawBody, signature, project.webhookSecret);
if (!isValid) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
// Only process pushes to the default branch
const defaultBranch = project.defaultBranch || "main";
if (payload.ref !== `refs/heads/${defaultBranch}`) {
return NextResponse.json(
{ message: `Ignored push to non-default branch (${payload.ref})` },
{ status: 200 }
);
}
// Extract changed files from the push payload
const changedFiles = new Set<string>();
const removedFiles = new Set<string>();
for (const commit of payload.commits || []) {
for (const file of commit.added || []) changedFiles.add(file);
for (const file of commit.modified || []) changedFiles.add(file);
for (const file of commit.removed || []) {
removedFiles.add(file);
changedFiles.delete(file); // If added then removed in same push, treat as removed
}
}
if (changedFiles.size === 0 && removedFiles.size === 0) {
return NextResponse.json({ message: "No file changes detected" }, { status: 200 });
}
// Get user's GitHub token for API calls
const ghToken = await getUserGitHubToken(project.userId);
// Trigger incremental sync via Inngest
await inngest.send({
name: "project.sync",
data: {
projectId: project.id,
owner: repoOwner,
repo: repoName,
changedFiles: Array.from(changedFiles),
removedFiles: Array.from(removedFiles),
ghToken,
},
});
return NextResponse.json({ message: "Sync triggered" });
}
// Helper function for signature verification (omitted for brevity, but uses HMAC SHA-256)
async function verifySignature(body: string, signatureHeader: string, secret: string): Promise<boolean> {
// ... implementation details ...
return true; // Placeholder
}
AI Docs integrates with Dodo Payments (a hypothetical payment provider) using webhooks to manage user subscriptions and billing events in real-time. This integration ensures that your subscription status, plan changes, and payment cycles are accurately reflected within AI Docs, enabling seamless access to features based on your plan.
Dodo Payments sends webhook events to AI Docs' dedicated billing webhook endpoint (/api/webhooks/dodo) whenever a significant event related to a subscription occurs.
The processing of Dodo billing webhooks involves:
POST request to the AI Docs webhook endpoint.DODO_WEBHOOK_SECRET environment variable. This ensures that only legitimate events from Dodo Payments are processed.type. Common event types include:
subscription.active: A new subscription has become active. AI Docs creates or updates the user's subscription record in its database, setting the plan, status, and billing period.subscription.renewed: An existing subscription has been successfully renewed. The currentPeriodEnd and updatedAt fields are updated.subscription.plan_changed: A user has upgraded or downgraded their plan. The subscription record is updated with the new plan details.subscription.cancelled: A subscription has been cancelled. The status is updated to cancelled, and cancelAtPeriodEnd is set to true.subscription.on_hold: A subscription is temporarily on hold (e.g., due to payment issues).subscription.expired: A subscription has fully expired, often resulting in a downgrade to the "free" plan.subscriptions record in its database, ensuring that the user's access and features align with their current subscription status.For more information on subscription plans, managing your subscription, and billing-related APIs, refer to Subscription Plans, Managing Your Subscription, and Billing API and Webhooks.
The following TypeScript code demonstrates how AI Docs handles various Dodo Payments webhook events to update user subscription data.
// app/api/webhooks/dodo/route.ts
import { NextRequest, NextResponse } from "next/server";
import { dodo } from "@/lib/billing/dodo"; // Dodo SDK client
import { db } from "@/lib/db";
import { subscriptions } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import { upsertSubscription } from "@/lib/billing/subscription";
import { env } from "@/lib/env";
type WebhookEvent = ReturnType<typeof dodo.webhooks.unwrap>;
export async function POST(req: NextRequest) {
const body = await req.text();
const webhookSecret = process.env.DODO_WEBHOOK_SECRET;
if (!webhookSecret) {
console.error("DODO_WEBHOOK_SECRET is not set");
return NextResponse.json(
{ error: "Webhook not configured" },
{ status: 500 }
);
}
let event: WebhookEvent;
try {
// Verify webhook signature using the Dodo SDK
event = dodo.webhooks.unwrap(body, {
headers: headersToRecord(req.headers), // Helper to convert headers
key: webhookSecret,
});
} catch (err) {
console.error("Webhook signature verification failed:", err);
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
}
try {
switch (event.type) {
case "subscription.active": {
const d = event.data;
const userId = d.metadata?.user_id; // Assuming user_id is passed in metadata
if (!userId) {
console.error("No user_id in subscription.active metadata", d);
break;
}
// Determine plan based on product ID
const plan =
d.product_id === env.DODO_PRO_MONTHLY_PRODUCT_ID ||
d.product_id === env.DODO_PRO_ANNUAL_PRODUCT_ID
? "pro"
: "free";
await upsertSubscription({
userId,
dodoSubscriptionId: d.subscription_id,
dodoCustomerId: d.customer.customer_id,
plan: plan,
status: "active",
billingPeriod: d.payment_frequency_interval?.toLowerCase(),
currentPeriodEnd: d.next_billing_date ? new Date(d.next_billing_date) : undefined,
cancelAtPeriodEnd: d.cancel_at_next_billing_date ?? false,
});
break;
}
case "subscription.renewed": {
const d = event.data;
await db
.update(subscriptions)
.set({
status: "active",
currentPeriodEnd: d.next_billing_date ? new Date(d.next_billing_date) : undefined,
updatedAt: new Date(),
})
.where(eq(subscriptions.dodoSubscriptionId, d.subscription_id));
break;
}
case "subscription.cancelled": {
const d = event.data;
await db
.update(subscriptions)
.set({
status: "cancelled",
cancelAtPeriodEnd: true,
updatedAt: new Date(),
})
.where(eq(subscriptions.dodoSubscriptionId, d.subscription_id));
break;
}
// ... other cases like subscription.plan_changed, subscription.on_hold, subscription.expired ...
default:
// Silently ignore unknown or unneeded events
break;
}
} catch (err) {
console.error(`Error handling webhook ${event.type}:`, err);
// Return 500 so Dodo retries the webhook
return NextResponse.json({ error: "Handler failed" }, { status: 500 });
}
return NextResponse.json({ received: true });
}
// Helper function to convert NextRequest headers to a plain record
function headersToRecord(headers: Headers): Record<string, string> {
const result: Record<string, string> = {};
headers.forEach((value, key) => {
result[key] = value;
});
return result;
}
DODO_WEBHOOK_SECRET, webhookSecret for GitHub) securely in environment variables or a secrets management system. Never hardcode them.2xx HTTP status code. If your webhook handler encounters an error, return a 500 status code to signal the provider to retry the event later.