mxHeadless
REST API gateway for headless frontends on MODX 3. Resources, objects, OpenAPI, API keys, and OAuth

Tags in meta.revalidate help invalidate headless frontend cache after MODX changes.
MODX mutation → outbox → worker POST → your /api/revalidate → Next.js / Nuxt cache purgemxHeadless does not call the frontend synchronously inside the API HTTP request. Delivery is async via the webhook worker.
Strings in meta.revalidate:
| Tag | Invalidate when |
|---|---|
mxheadless:resources | Any resource change |
mxheadless:resources:{id} | One resource |
mxheadless:uri:{path} | Page by URI |
mxheadless:context:{key} | Context content |
mxheadless:resources:list | Resource deleted (lists) |
mxheadless:resources:{parentId} | Child changed (parent menu) |
Map tags to router paths in your revalidate handler.
app/api/revalidate/route.ts:
import { revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
import { createHmac, timingSafeEqual } from 'crypto';
export async function POST(request: NextRequest) {
const secret = process.env.MXHEADLESS_WEBHOOK_SECRET ?? '';
const rawBody = await request.text();
const signature = request.headers.get('x-mxheadless-signature') ?? '';
if (secret && !verifySignature(secret, rawBody, signature)) {
return NextResponse.json({ error: 'invalid signature' }, { status: 401 });
}
const event = JSON.parse(rawBody) as {
type: string;
meta?: { revalidate?: string[] };
};
for (const tag of event.meta?.revalidate ?? []) {
revalidateTag(tag);
}
return NextResponse.json({ revalidated: true, type: event.type });
}
function verifySignature(secret: string, body: string, header: string): boolean {
const expected = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(header);
return a.length === b.length && timingSafeEqual(a, b);
}The subscription secret in MODX and the frontend env must match.
mxheadless_webhook_subscriptionshttps://frontend.example/api/revalidateresources.* or *