API Webhook (B2)
The decoupled option: Vireloop POSTs the finished article to an ingest endpoint on your site, authenticated with an access token. Your app stores it however it likes. This is the recommended long-term contract.
1. Create the integration
- Onboarding Integration step → API Webhook.
- Enter an Integration Name, your Webhook Endpoint (e.g.
https://yoursite.com/api/ingest-post), and an Access Token — a long random string you choose. - Click Send Test to verify the endpoint answers, then Create. The token is stored in Vault.
2. Add the token to your site
.env
# .env on your site
VIRELOOP_INGEST_KEY=<the access token you set in Vireloop>3. Install the ingest route
Add this route to your site. It checks the token on the x-api-key header, answers a ping for the connection test, and stores the article. Adapt the storage block to your data model.
app/api/ingest-post/route.ts
import { NextResponse } from "next/server";
import { revalidatePath } from "next/cache";
import { createClient } from "@supabase/supabase-js";
// The canonical article shape Vireloop sends.
type IngestBody = {
ping?: boolean;
status?: "draft" | "publish";
title: string;
slug: string;
metaTitle: string;
metaDescription: string;
bodyHtml: string;
bodyMarkdown: string;
featuredImageUrl: string;
featuredImageAlt: string;
keyword: string;
faq: { q: string; a: string }[];
schemaJsonLd: Record<string, unknown>;
};
export async function POST(req: Request) {
// 1) Auth — shared secret.
const key = req.headers.get("x-api-key");
if (!key || key !== process.env.VIRELOOP_INGEST_KEY) {
return NextResponse.json({ ok: false, error: "unauthorized" }, { status: 401 });
}
const body = (await req.json()) as IngestBody;
// 2) Health check ping (used by "Send Test").
if (body.ping) return NextResponse.json({ ok: true, pong: true });
// 3) Store. Adapt this block to THIS app's data model.
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // server-only
);
const { error } = await supabase.from("posts").upsert(
{
title: body.title,
slug: body.slug,
content: body.bodyHtml,
excerpt: body.metaDescription,
meta_title: body.metaTitle,
meta_description: body.metaDescription,
cover_image: body.featuredImageUrl,
cover_image_alt: body.featuredImageAlt,
faq: body.faq,
schema_jsonld: body.schemaJsonLd,
status: body.status === "publish" ? "published" : "draft",
published_at: body.status === "publish" ? new Date().toISOString() : null,
},
{ onConflict: "slug" },
);
if (error) {
return NextResponse.json({ ok: false, error: error.message }, { status: 500 });
}
// 4) Rebuild the affected routes.
revalidatePath("/blog");
revalidatePath(`/blog/${body.slug}`);
return NextResponse.json({ ok: true, url: `/blog/${body.slug}` });
}Payload
Vireloop sends the canonical article: title, slug, metaTitle, metaDescription, bodyHtml (sanitized), bodyMarkdown, featuredImageUrl, featuredImageAlt, keyword, faq, and schemaJsonLd, plus status (draft or publish). It expects { ok: true, url } back.
Other connectors: WordPress · Next.js Blog (B1)