Skip to main content

Serverless handler for Sequel → Contentful

Learn how to setup Sequel 1-click using a serverless handler

Updated over a week ago

Sequel can POST to your endpoint whenever an event is created, updated, or deleted. Your handler receives the payload, writes to Contentful via the Contentful Management API.


What you’ll set up

  1. One HTTPS route (serverless is fine) that accepts JSON POST.

  2. In Sequel Dashboard → Integrations → Custom CMS Integration, set the three URLs (Create / Update / Delete) to this route (or three separate routes). Sequel API docs

  3. (Recommended) After creating an entry, call Sequel:
    POST /api/v3/events/{eventId}/wpcms with { postId: <entryId>, postUrl: <public URL> }.


Prerequisites

  • A Contentful space & environment.

  • A Content Management API token (CMA) for writes. You’ll use the contentful-management SDK to getSpace() → getEnvironment() → create/update entries & assets → publish.


Field mapping

From Sequel webhook details to your Contentful entry fields:

  • titleeventName

  • slug ← slugified eventName

  • descriptiondescription (Rich Text: see note below)

  • startDatestartDate (UTC ISO)

  • endDateendDate (UTC ISO)

  • timezonetimezone

  • sequelEventIdeventId

  • externalCMSItemId ← Contentful entry ID (stored back to Sequel via /wpcms)

  • banner (Asset link) ← from bannerUrl (create + process + publish asset, then link)


Serverless handler (Node/Express style)

Works on Vercel/Netlify/AWS Lambda/Cloudflare (adjust export syntax/body parsing per platform).

// package.json deps:
// "contentful-management": "^11",
// "node-fetch": "^3",
// "slugify": "^1"

import contentful from 'contentful-management';
import fetch from 'node-fetch';
import slugify from 'slugify';

const client = contentful.createClient({ accessToken: process.env.CTF_CMA_TOKEN });

const SPACE_ID = process.env.CTF_SPACE_ID;
const ENV_ID = process.env.CTF_ENV_ID || 'master';
const CONTENT_TYPE_ID = process.env.CONTENT_TYPE_ID || 'event';
const DEFAULT_LOCALE = process.env.DEFAULT_LOCALE || 'en-US';
const PUBLIC_EVENT_URL_BASE = process.env.PUBLIC_EVENT_URL_BASE;

// Express-style handler signature; adapt for your platform
export default async function handler(req, res) {
try {
// --- 1) Basic auth: shared secret ---
const given =
req.headers['x-shared-secret'] ||
new URL(req.url, 'http://localhost').searchParams.get('secret');
if (given !== process.env.WEBHOOK_SHARED_SECRET) {
return res.status(401).json({ error: 'unauthorized' });
}

// --- 2) Determine action: create | update | delete ---
const action =
(req.headers['x-sequel-action'] || '').toLowerCase() ||
new URL(req.url, 'http://localhost').searchParams.get('action') ||
''; // set per webhook URL in Sequel

// --- 3) Parse payload ---
// Ensure your platform gives you parsed JSON (e.g., Vercel/Next API routes do; otherwise add a JSON body parser)
const body = req.body || {};
const details = body.details || {};
if (!details.eventId) return res.status(400).json({ error: 'missing eventId' });

const space = await client.getSpace(SPACE_ID);
const env = await space.getEnvironment(ENV_ID);

// --- Helpers ---
const toSlug = (name, fallback) =>
slugify(name || fallback, { lower: true, strict: true });

async function ensureBannerAsset(url, title) {
if (!url) return null;
// Create asset that points to a remote URL; then process and publish
const asset = await env.createAsset({
fields: {
title: { [DEFAULT_LOCALE]: title || 'Event banner' },
file: {
[DEFAULT_LOCALE]: {
upload: url, // remote URL
fileName: `banner-${details.eventId}.jpg`,
contentType: 'image/jpeg' // adjust if not JPG
}
}
}
});
await asset.processForAllLocales(); // wait/process binary
const processed = await env.getAsset(asset.sys.id);
return await processed.publish(); // publish asset
}

async function findEntryByExternalId(externalId) {
const q = await env.getEntries({
content_type: CONTENT_TYPE_ID,
[`fields.externalCMSItemId`]: externalId,
limit: 1
});
return q.items[0] || null;
}

// --- 4) DELETE flow ---
if (action === 'delete') {
const externalId = details.externalCMSItemId || details.eventId;
const entry = await findEntryByExternalId(externalId);
if (entry) {
try { if (entry.isPublished()) await entry.unpublish(); } catch {}
await env.deleteEntry(entry.sys.id);
}
return res.json({ ok: true, deleted: !!entry });
}

// --- 5) CREATE / UPDATE flow ---
const slug = toSlug(details.eventName, details.eventId);
const banner = await ensureBannerAsset(details.bannerUrl, details.eventName);

// Build field payload (mind locales)
const fields = {
title: { [DEFAULT_LOCALE]: details.eventName || '' },
slug: { [DEFAULT_LOCALE]: slug },
description: { [DEFAULT_LOCALE]: details.description || '' }, // Rich Text? see note below
startDate: { [DEFAULT_LOCALE]: details.startDate || null },
endDate: { [DEFAULT_LOCALE]: details.endDate || null },
timezone: { [DEFAULT_LOCALE]: details.timezone || '' },
sequelEventId: { [DEFAULT_LOCALE]: details.eventId },
externalCMSItemId: { [DEFAULT_LOCALE]: (details.externalCMSItemId || details.eventId) },
...(banner
? { banner: { [DEFAULT_LOCALE]: { sys: { type: 'Link', linkType: 'Asset', id: banner.sys.id } } } }
: {})
};

// If we already stored the Contentful entry id back into Sequel, updates will carry externalCMSItemId
let entry = null;
if (action === 'update' && (details.externalCMSItemId || details.eventId)) {
entry = await findEntryByExternalId(details.externalCMSItemId || details.eventId);
}

if (!entry) {
entry = await env.createEntry(CONTENT_TYPE_ID, { fields }); // creates as draft
} else {
entry.fields = { ...entry.fields, ...fields };
entry = await entry.update();
}

// Publish (Contentful requires a separate publish step)
if (!entry.isPublished()) entry = await entry.publish();

// --- 6) Round-trip to Sequel once (after first create) ---
if (!details.externalCMSItemId && PUBLIC_EVENT_URL_BASE) {
const publicUrl = `${PUBLIC_EVENT_URL_BASE}/${slug}`;
await fetch(`https://api.introvoke.com/api/v3/events/${details.eventId}/wpcms`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.SEQUEL_API_TOKEN}`
},
body: JSON.stringify({ postId: entry.sys.id, postUrl: publicUrl })
});
}

return res.json({ ok: true, entryId: entry.sys.id, published: true });
} catch (err) {
console.error(err);
return res.status(500).json({ error: err.message });
}
}

Why these calls?

  • Create/Update/Publish entries and create/process/publish assets are the standard CMA flows; publishing is a separate step.


Configure Sequel webhooks

In Sequel Dashboard → Integrations → Custom CMS Integration, set:

  • Create URLhttps://your-endpoint?action=create + header x-shared-secret: <secret>

  • Update URLhttps://your-endpoint?action=update + same header

  • Delete URLhttps://your-endpoint?action=delete + same header

Sequel’s docs list the payload shape (e.g., eventId, eventName, description, startDate, endDate, bannerUrl, timezone, and externalCMSItemId on updates/deletes) and the recommended /wpcms round-trip.


Notes & gotchas

  • Locales: Replace 'en-US' with your default locale.

  • Rich Text fields: If your description is Rich Text, convert incoming HTML/Markdown to Contentful Rich Text JSON before setting; or store plain text in a long text field. (CMA supports Rich Text values via the management SDK.)

  • Assets from URL: The sample uses upload with a remote URL, then processForAllLocales() and publish()—this is the correct sequence for assets.

  • Rate limits & retries: Contentful CMA enforces limits; handle 429s with backoff. (SDK bubbles HTTP errors—retry on 429s.)


Quick test plan

  1. Point Create URL at your deployed route with ?action=create.

  2. Create a test event in Sequel → confirm Contentful entry appears and is published.

  3. Verify the Sequel /wpcms call stored your Contentful postId/postUrl (future webhooks now include externalCMSItemId).

  4. Update the event title in Sequel → confirm entry updates & republishes.

  5. Delete the event → confirm entry is unpublished and deleted in Contentful.


References

  • Sequel docs — Using Custom CMS Webhooks (payloads, /wpcms, setup). Sequel API docs

  • Contentful — Content Management API (entries, assets, publish). Contentful

  • contentful-management SDK — Environment/Entry/Asset APIs (getSpace → getEnvironment, createEntry, publish, createAsset + processForAllLocales).

Did this answer your question?