Draivv API · v1
Build on Draivv.
Draivv runs a multi-tenant headless CMS and content-intelligence platform. Pull published content over a REST API, receive HMAC-signed webhooks, stream first-party analytics, and capture leads — from Next.js, WordPress, or any stack that speaks HTTP.
Overview
Introduction
The Draivv platform separates where content is produced (the Draivv dashboard) from where it is consumed (your website). Your site reads published content through the public Content API v1, stays in sync through webhooks, and can optionally install telemetry and lead-capture forms.
Your site Draivv backend
+---------------------+ +------------------------------+
| Next.js / WordPress | fetch | GET /api/content |
| Content API v1 | ------------> | GET /api/content/:slug |
| @sdcms/cli | | GET /api/content/sitemap |
| | webhook | POST webhook (HMAC) |
| | <------------ | revalidateTag / revalidate |
| | | |
| telemetry.js (~3KB) | sendBeacon | POST /telemetry/session |
| | ------------> | POST /telemetry/pageview |
| | | POST /telemetry/click |
| | | |
| widget/lead.js | fetch | GET /api/lead-magnets/{id} |
| | ------------> | POST /api/lead-magnets/{id} |
+---------------------+ +------------------------------+Everything is additive and versioned. Public API evolution never removes fields, changes canonical semantics, or breaks published URLs without a migration plan — so an integration you ship today keeps working.
Getting started
Quick start
The fastest path for a Next.js site is the official scaffolder. It detects your project, prompts for credentials, and generates a typed fetch client, blog pages, sitemap, robots, a health endpoint, and an HMAC-verified webhook handler.
# Scaffold a full integration
npx @sdcms/cli init
# Validate connectivity & generated routes (CI-friendly)
npx @sdcms/cli checkPrefer to wire it up by hand? A single authenticated request:
curl -H "x-api-key: $CMS_API_KEY" \
"https://cms.draivv.com/api/content?limit=5&type=BLOG"Keep your key server-side
CMS_API_KEY is a secret. Call the Content API only from Server Components, Route Handlers, or Server Actions — never from the browser.
Conventions
Base URL & versioning
All requests are made against the canonical host:
https://cms.draivv.comThe legacy host https://sdcms-web.vercel.app remains supported for existing installs. Every Content API response carries diagnostic headers so you can inventory your integration:
| Parameter | Type | Description |
|---|---|---|
| x-sdcms-contract-version | string | Public contract in use, e.g. content-api.v1 |
| x-sdcms-integration-mode | string | Integration mode, e.g. content-api |
| x-sdcms-resource | string | Resource served, e.g. content-detail |
| x-sdcms-recommended-contract | string | Recommended contract for new installs |
Official clients should send x-sdcms-client and x-sdcms-client-version on server-side calls so the platform can map consumers ahead of any assisted migration.
Security
Authentication
Draivv exposes three authentication tiers depending on the surface:
| Parameter | Type | Description |
|---|---|---|
| Public API key | x-api-key | Read published content. Sent as the x-api-key header on every Content API call. |
| Signed write access | x-api-key + signature | Server-to-server write/admin operations. Provisioned to partners and internal tooling — contact the team for access. |
| OAuth 2.1 | Bearer token | Token-based access for partner and team integrations, via PKCE. |
For the public Content API, pass your key on each request. Responses are always wrapped in a { data: ... } envelope.
curl -H "x-api-key: $CMS_API_KEY" \
"https://cms.draivv.com/api/content/my-post-slug"Where do keys come from?
CMS_API_KEY (public) and CMS_WEBHOOK_SECRET are issued per site from the Draivv dashboard. Private keys (CMS_PRIVATE_API_KEY and CMS_PRIVATE_HMAC_SECRET) are scoped and revocable.
Security
Agents & capabilities
Draivv is an agent-first platform: AI agents connect over MCP or the private API to provision sites, define page types, write content and feed the knowledge base. Each API key carries a capability allowlist so an agent only holds the permissions its job requires.
| Parameter | Type | Description |
|---|---|---|
| content:write | capability | Create and edit content (drafts, fields, SEO, tags). Does not include publishing. |
| content:publish | capability | Transition content to PUBLISHED/SCHEDULED, and unpublish. |
| catalog:write | capability | Define and change the client's page types (content types). |
| knowledge:write | capability | Ingest, import and re-embed documents in the knowledge base (RAG). |
| site:provision | capability | Provision clients and sites (agent-driven onboarding). |
| destructive | capability | Irreversible operations: delete content or clients, purge knowledge, rotate health tokens. |
An empty list means the key is unrestricted within its scope — every pre-existing key keeps working unchanged. A non-empty list is a strict allowlist: anything not listed returns 403 with code: "missing_capability". That code is stable — treat it as "request a key with more permission or delegate this step", never as a transient error.
{
"error": "Esta chave de API não tem a capability \"content:publish\".",
"code": "missing_capability"
}Writing and publishing are separate on purpose
The recommended pipeline gives a content agent content:write only: it drafts freely, and publishing goes through a key (or person) holding content:publish. Any payload setting status: PUBLISHED or SCHEDULED — including bulk updates — is gated. Every write, human or agent, is auditable in the dashboard's Activity feed.
Keys and their capabilities are managed in the Draivv dashboard under Settings → API keys. Enforcement lives in the private API — the single write path — so MCP (stdio and remote), OAuth connectors, the CLI and direct integrations all inherit the same rules by construction.
Content API
List content
/api/contentReturns published content with cursor-based pagination. All entries omit the markdown body unless includeContent=true.
| Parameter | Type | Description |
|---|---|---|
| limit | number | Max results. Default 20, max 100 (capped at 50 when includeContent=true). |
| type | string | BLOG | LINKEDIN | INSTAGRAM | FACEBOOK | TWITTER |
| cursor | string | Pagination cursor returned in nextCursor. |
| includeContent | boolean | Include the markdown body (caps limit at 50). |
| preview | boolean | Server-side only: include drafts for preview routes (e.g. Next.js draftMode). Responses are never cached (no-store) and carry x-sdcms-preview: true. Omit for the strict PUBLISHED contract. |
| tags | string | Comma-separated tags (matches ANY). |
{
"data": [
{
"id": "uuid",
"title": "Post title",
"slug": "post-title",
"status": "PUBLISHED",
"type": "BLOG",
"excerpt": "Summary…",
"seoImage": "https://…",
"tags": ["tag1", "tag2"],
"publishedAt": "2024-01-01T12:00:00Z",
"client": { "slug": "client", "name": "Client Name" }
}
],
"nextCursor": "1704110400000|uuid",
"hasMore": true,
"count": 20
}To page through everything, keep calling with the previous nextCursor until hasMore is false.
Content API
Get a post
/api/content/:slugReturns the full post, including the markdown content and a ready-to-inject jsonLd object (schema.org BlogPosting). A missing or unpublished slug returns 404 — handle it by rendering your not-found page.
// src/lib/cms.ts
export async function getPostBySlug(slug: string): Promise<BlogPost | null> {
const res = await fetch(`${process.env.SDCMS_API_URL}/api/content/${slug}`, {
headers: { "x-api-key": process.env.CMS_API_KEY! },
next: { revalidate: 300, tags: ["sdcms:content"] },
})
if (res.status === 404) return null
if (!res.ok) throw new Error(`SDCMS ${res.status}`)
const { data } = await res.json()
return data
}Render safely
Sanitize markdown with DOMPurify before injecting it, and inject post.jsonLd as a <script type="application/ld+json"> tag for structured data.
Content API
Sitemap
/api/content/sitemapReturns slugs, canonical URLs, and timestamps for sitemap generation, paginated by cursor.
| Parameter | Type | Description |
|---|---|---|
| limit | number | Max results. Default 1000. |
| cursor | string | Cursor for the next page. |
{
"data": [
{
"slug": "post-title",
"canonicalUrl": "https://your-site.com/blog/post-title",
"publishedAt": "2024-01-01T12:00:00Z",
"updatedAt": "2024-01-02T08:30:00Z"
}
],
"nextCursor": "…",
"hasMore": false
}Content API
TypeScript types
Drop these into your project to type every response end to end.
interface BlogPost {
id: string
title: string
slug: string
status: string
type: string
excerpt: string | null
content: string | null
seoTitle: string | null
seoDescription: string | null
seoImage: string | null
tags: string[]
publishedAt: string | null
jsonLd: Record<string, unknown> | null
client: { slug: string; name: string }
}
interface ListPostsResponse {
data: BlogPost[]
nextCursor: string | null
hasMore: boolean
count: number
}
interface SitemapEntry {
slug: string
canonicalUrl: string
publishedAt: string
updatedAt: string
}
interface WebhookPayload {
event:
| "content.published"
| "content.updated"
| "content.unpublished"
| "content.deleted"
eventId: string
schemaVersion: number
emittedAt: string
clientId: string
siteId: string
post: {
id: string
slug: string | null
title: string
type: string
publishedAt: string | null
updatedAt: string | null
seoTitle: string | null
seoDescription: string | null
seoImage: string | null
canonicalUrl: string | null
}
previousSlug?: string | null
}Sync
Webhooks
Draivv POSTs a webhook to your endpoint whenever content is published, updated, unpublished, or deleted. Use it to revalidate caches and pages so your site reflects edits within seconds.
| Parameter | Type | Description |
|---|---|---|
| content.published | event | Content was published. |
| content.updated | event | Content was updated. |
| content.unpublished | event | Content returned to draft. |
| content.deleted | event | Content was deleted. |
Every delivery is HMAC-SHA256 signed over `${timestamp}.${rawBody}`. Read the raw body before parsing — the signature is over the exact bytes — and reject a clock skew greater than 300 seconds.
import crypto from "node:crypto"
import { revalidatePath, revalidateTag } from "next/cache"
export async function POST(request: Request) {
const rawBody = await request.text()
const timestamp = request.headers.get("x-sdcms-timestamp")
const signature = request.headers.get("x-sdcms-signature")
// 1. Reject stale deliveries (replay protection)
const age = Math.abs(Date.now() / 1000 - Number(timestamp))
if (!timestamp || age > 300) return new Response("Stale", { status: 403 })
// 2. Verify the HMAC over `timestamp.rawBody`
const expected = crypto
.createHmac("sha256", process.env.CMS_WEBHOOK_SECRET!)
.update(`${timestamp}.${rawBody}`)
.digest("hex")
const ok =
signature &&
crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex"),
)
if (!ok) return new Response("Invalid", { status: 403 })
// 3. Revalidate
const payload = JSON.parse(rawBody)
revalidateTag("sdcms:content")
revalidatePath("/blog")
revalidatePath("/sitemap.xml")
if (payload.post?.slug) revalidatePath(`/blog/${payload.post.slug}`)
if (payload.previousSlug) revalidatePath(`/blog/${payload.previousSlug}`)
return new Response("OK", { status: 200 })
}Dedupe deliveries
Webhooks are at-least-once. Use the unique eventId (scoped per siteId) to ignore retries you have already processed.
Analytics
Telemetry
A first-party, cookieless, LGPD-native analytics engine. Two lines of HTML give you realtime pageviews, AI-aware source classification, and per-lead attribution — no cookie banner required.
<script>window.__SD_SITE_ID__="PUBLIC_SITE_ID"</script>
<script src="https://cms.draivv.com/telemetry.js" defer></script>The ~3KB snippet uses sessionStorage (tab scope, zero cookies) and ships events with sendBeacon. Source classification runs server-side, so the rules improve without a site redeploy:
| Parameter | Type | Description |
|---|---|---|
| paid | category | UTM medium = cpc / paid. |
| category | UTM medium = email. | |
| direct | category | No referrer, no UTM. |
| ai | category | Referrer in ChatGPT, Perplexity, Claude, Gemini, Copilot. |
| organic | category | Referrer in Google, Bing, DuckDuckGo. |
| social | category | Referrer in Instagram, LinkedIn, Facebook. |
| referral | category | Any other referrer (fallback). |
Public ingestion endpoints (called by the snippet, not by you):
/api/public/telemetry/session/api/public/telemetry/pageview/api/public/telemetry/heartbeat/api/public/telemetry/clickAdd data-sd-track to a <form> and the snippet injects a hidden __sd_session_id__ field on submit, linking the lead to its telemetry session.
data-sd-track alone does not create a lead
It only injects the session id. For a lead to appear in Site Analytics, the form data must reach the capture API — via the widget, a form posting directly to it, or your backend forwarding it. See Lead Magnets.
Analytics
Click tracking
The snippet auto-detects clicks on known link patterns and batches them (5 items or on pagehide). No personal data is captured — only category, label, and page path.
| Parameter | Type | Description |
|---|---|---|
| wa.me / api.whatsapp.com | auto | |
| tel: | auto | phone |
| mailto: | auto | |
| data-sd-track-click | attribute | Custom category for any element. |
<a href="/quote"
data-sd-track-click="cta"
data-sd-track-label="header-quote">
Request a quote
</a>
<button data-sd-track-click="demo" data-sd-track-label="hero-demo">
Book a demo
</button>Rate limit: 60 clicks per minute per session.
Analytics
Proposal tracking
A specialized script for commercial proposal pages. It measures section views (via IntersectionObserver), dwell time, scroll depth, and active engagement.
<script>window.__SD_SITE_ID__="PUBLIC_PROPOSAL_ID"</script>
<script src="https://cms.draivv.com/proposal-telemetry.js" defer></script>
<section data-sd-section="pricing">…</section>
<section data-sd-section="scope">…</section>/api/public/telemetry/sectionOpening a proposal fires a domain event that emails the owning agency user. Archived proposals stop accepting new sessions.
Lead capture
Lead Magnets API
Public form module for capturing leads. Two endpoints:
/api/lead-magnets/:publicIdReturns the public form configuration (title, description, schema, delivery). Only active forms are served.
/api/lead-magnets/:publicId/captureThis is the only endpoint that creates a lead
For a lead to show in Site Analytics, data must reach here — from the widget, an HTML form posting directly, or your backend forwarding it.
{
"data": { "name": "Ana", "email": "ana@example.com" },
"hp": "",
"__sd_session_id__": "telemetry-session-uuid"
}__sd_session_id__ is optional but required for source attribution. hp is a honeypot — submissions with it filled are ignored. Forward from your own backend like this:
export async function POST(request: Request) {
const form = await request.formData()
// …handle your own logic (save, email)…
await fetch("https://cms.draivv.com/api/lead-magnets/PUBLIC_ID/capture", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
data: {
name: form.get("name"),
email: form.get("email"),
phone: form.get("phone"),
},
hp: "",
__sd_session_id__: form.get("__sd_session_id__") || undefined,
}),
})
return new Response("OK")
}Protections: server-side schema validation, honeypot, per-IP rate limiting, and IP hashed at rest (HMAC-SHA256). Delivery can be an immediate message, a redirect, or a temporary signed asset URL.
Lead capture
Embed widget
A drop-in widget for any site. It infers the CMS origin from its own script src and handles capture end to end.
<div data-sdcms-lead="PUBLIC_ID"></div>
<script src="https://cms.draivv.com/widget/lead.js" async></script>The widget never inherits color from the host page — it ships a self-contained light card that stays legible on dark sites. To match your brand, override its CSS variables (no data-sdcms-no-style needed):
.sdcms-lead {
--sdcms-lead-scheme: dark; /* native controls (select, autofill) */
--sdcms-lead-bg: #0f1b21; /* card background */
--sdcms-lead-ink: #f4f7fb; /* primary text */
--sdcms-lead-muted: #9ca5b4; /* description, hints, consent */
--sdcms-lead-line: rgba(255,255,255,.10);
--sdcms-lead-field-bg: rgba(255,255,255,.04);
--sdcms-lead-accent: #3fcb9a; /* button + focus ring */
--sdcms-lead-accent-ink: #05090b; /* button label */
}Full token list in the docs. For total control, opt out of injected CSS with data-sdcms-no-style="1" and style the stable class names yourself:
.sdcms-lead { max-width: 520px; font-family: system-ui, sans-serif; }
.sdcms-lead__card { border: 1px solid rgba(0,0,0,.12); border-radius: 14px; padding: 16px; }
.sdcms-lead__input,
.sdcms-lead__select,
.sdcms-lead__textarea { width: 100%; box-sizing: border-box; }
.sdcms-lead__btn { width: 100%; border: 0; border-radius: 10px; padding: 11px 12px; }Platform
SEO Metadata API
/api/public/seo-metadataReturns consolidated SEO metadata for a client — default tags, JSON-LD schemas (Organization, WebSite), posts with canonical URLs, sitemap entries, and client data. Consumed by the @sdcms/nextjs SDK.
Legacy contract
This endpoint is kept for backward compatibility and returns x-sdcms-legacy: true. For new headless blogs, prefer the Content API v1 above.
Platform
WordPress plugin
The official SDCMS Connect plugin syncs content into WordPress (6.4+, PHP 8.1+). It connects via a one-time token handshake and keeps posts in sync over webhooks.
1. POST /api/private/wp/connect-token → generate a one-time token
2. Plugin → POST /api/public/wp/connect → exchanges token
3. Draivv returns { api_key, webhook_secret, site_id, client_id }| Parameter | Type | Description |
|---|---|---|
| content.published | event | Create or update the post. |
| content.updated | event | Update the post. |
| content.unpublished | event | Set the post status to draft. |
| content.deleted | event | Move the post to trash. |
The plugin exposes REST routes for health, webhooks, and manual sync:
/wp-json/sdcms/v1/health/wp-json/sdcms/v1/webhook/wp-json/sdcms/v1/manual-syncPlatform
CLI
@sdcms/cli scaffolds and manages integrations.
npx @sdcms/cli init # scaffold the project
npx @sdcms/cli check # validate env, API, routes (CI)
npx @sdcms/cli content list # list published content
npx @sdcms/cli content get <slug> # fetch a post (--format json|markdown)
npx @sdcms/cli content publish <id> # publish via the private API (--unpublish)content publish requires SDCMS_API_URL, SDCMS_API_KEY, and SDCMS_HMAC_SECRET.
Platform
Next.js SDK
@sdcms/nextjs wraps SEO metadata, JSON-LD, sitemap, and React hooks with an in-memory cache. New integrations should prefer npx @sdcms/cli init (which consumes Content API v1 directly); the SDK currently targets the legacy SEO Metadata contract.
import { createSDCMSClient } from "@sdcms/nextjs"
export const sdcms = createSDCMSClient({
apiUrl: "https://cms.draivv.com",
clientSlug: "your-client-slug",
// revalidate: 3600,
})Reference
Errors
Success responses use the { data: ... } envelope and a 2xx status. Failures return a non-2xx status; authentication failures on signed surfaces return a plain 403.
| Parameter | Type | Description |
|---|---|---|
| 200 | OK | Request succeeded. |
| 400 | Bad Request | Malformed query, body, or unsupported parameter. |
| 401 / 403 | Unauthorized | Missing/invalid x-api-key, bad HMAC signature, or stale timestamp (skew > 300s). |
| 404 | Not Found | Unknown slug, or content not published. |
| 429 | Too Many Requests | Rate limit exceeded — back off and retry. |
| 5xx | Server Error | Transient backend error. Retry with backoff. |
Reference
Rate limits
The API is rate limited per key. Defaults are 300 requests/minute with a burst of 60; click ingestion is limited to 60/minute per session. On 429, back off exponentially and retry.
Cache, don't poll
Use ISR (revalidate: 300) plus cache tags and let webhooks invalidate on change — never cache: "no-store". You will rarely come close to the limit.
Reference
LGPD compliance
The platform is LGPD-native by design.
| Parameter | Type | Description |
|---|---|---|
| Cookies | — | Zero. Uses sessionStorage (tab scope). |
| IP | — | Never stored raw. Hashed with HMAC-SHA256. |
| PII | — | Only with explicit consent (form checkbox). |
| Retention | — | Sessions without a lead: 90 days. With a lead: 2 years. |
| Erasure | — | DELETE endpoint for LGPD data-subject rights. |
Reference
Support
Stuck on an integration? The fastest unblock is to scaffold and validate:
npx @sdcms/cli init
npx @sdcms/cli checkFor API keys, scoped credentials, or webhook secrets, head to the Draivv dashboard. For anything else, reach the team at contato@draivv.com.
Built with the Draivv content-intelligence platform · Content API v1 · https://cms.draivv.com