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.

architecture
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.

bash
# Scaffold a full integration
npx @sdcms/cli init

# Validate connectivity & generated routes (CI-friendly)
npx @sdcms/cli check

Prefer to wire it up by hand? A single authenticated request:

bash
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:

text
https://cms.draivv.com

The 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:

ParameterTypeDescription
x-sdcms-contract-versionstringPublic contract in use, e.g. content-api.v1
x-sdcms-integration-modestringIntegration mode, e.g. content-api
x-sdcms-resourcestringResource served, e.g. content-detail
x-sdcms-recommended-contractstringRecommended 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:

ParameterTypeDescription
Public API keyx-api-keyRead published content. Sent as the x-api-key header on every Content API call.
Signed write accessx-api-key + signatureServer-to-server write/admin operations. Provisioned to partners and internal tooling — contact the team for access.
OAuth 2.1Bearer tokenToken-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.

bash
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.

ParameterTypeDescription
content:writecapabilityCreate and edit content (drafts, fields, SEO, tags). Does not include publishing.
content:publishcapabilityTransition content to PUBLISHED/SCHEDULED, and unpublish.
catalog:writecapabilityDefine and change the client's page types (content types).
knowledge:writecapabilityIngest, import and re-embed documents in the knowledge base (RAG).
site:provisioncapabilityProvision clients and sites (agent-driven onboarding).
destructivecapabilityIrreversible 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.

403 — missing capability
{
  "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

GET/api/content

Returns published content with cursor-based pagination. All entries omit the markdown body unless includeContent=true.

ParameterTypeDescription
limitnumberMax results. Default 20, max 100 (capped at 50 when includeContent=true).
typestringBLOG | LINKEDIN | INSTAGRAM | FACEBOOK | TWITTER
cursorstringPagination cursor returned in nextCursor.
includeContentbooleanInclude the markdown body (caps limit at 50).
previewbooleanServer-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.
tagsstringComma-separated tags (matches ANY).
200 OK
{
  "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

GET/api/content/:slug

Returns 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.

ts
// 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

GET/api/content/sitemap

Returns slugs, canonical URLs, and timestamps for sitemap generation, paginated by cursor.

ParameterTypeDescription
limitnumberMax results. Default 1000.
cursorstringCursor for the next page.
200 OK
{
  "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.

ts
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.

ParameterTypeDescription
content.publishedeventContent was published.
content.updatedeventContent was updated.
content.unpublishedeventContent returned to draft.
content.deletedeventContent 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.

src/app/api/sdcms/webhook/route.ts
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.

install in <head>
<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:

ParameterTypeDescription
paidcategoryUTM medium = cpc / paid.
emailcategoryUTM medium = email.
directcategoryNo referrer, no UTM.
aicategoryReferrer in ChatGPT, Perplexity, Claude, Gemini, Copilot.
organiccategoryReferrer in Google, Bing, DuckDuckGo.
socialcategoryReferrer in Instagram, LinkedIn, Facebook.
referralcategoryAny other referrer (fallback).

Public ingestion endpoints (called by the snippet, not by you):

POST/api/public/telemetry/session
POST/api/public/telemetry/pageview
POST/api/public/telemetry/heartbeat
POST/api/public/telemetry/click

Add 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.

ParameterTypeDescription
wa.me / api.whatsapp.comautowhatsapp
tel:autophone
mailto:autoemail
data-sd-track-clickattributeCustom category for any element.
html
<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.

html
<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>
POST/api/public/telemetry/section

Opening 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:

GET/api/lead-magnets/:publicId

Returns the public form configuration (title, description, schema, delivery). Only active forms are served.

POST/api/lead-magnets/:publicId/capture

This 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.

request body
{
  "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:

ts
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.

html
<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):

css
.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:

css
.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

GET/api/public/seo-metadata

Returns 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.

text
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 }
ParameterTypeDescription
content.publishedeventCreate or update the post.
content.updatedeventUpdate the post.
content.unpublishedeventSet the post status to draft.
content.deletedeventMove the post to trash.

The plugin exposes REST routes for health, webhooks, and manual sync:

GET/wp-json/sdcms/v1/health
POST/wp-json/sdcms/v1/webhook
POST/wp-json/sdcms/v1/manual-sync

Platform

CLI

@sdcms/cli scaffolds and manages integrations.

bash
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.

ts
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.

ParameterTypeDescription
200OKRequest succeeded.
400Bad RequestMalformed query, body, or unsupported parameter.
401 / 403UnauthorizedMissing/invalid x-api-key, bad HMAC signature, or stale timestamp (skew > 300s).
404Not FoundUnknown slug, or content not published.
429Too Many RequestsRate limit exceeded — back off and retry.
5xxServer ErrorTransient 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.

ParameterTypeDescription
CookiesZero. Uses sessionStorage (tab scope).
IPNever stored raw. Hashed with HMAC-SHA256.
PIIOnly with explicit consent (form checkbox).
RetentionSessions without a lead: 90 days. With a lead: 2 years.
ErasureDELETE endpoint for LGPD data-subject rights.

Reference

Support

Stuck on an integration? The fastest unblock is to scaffold and validate:

bash
npx @sdcms/cli init
npx @sdcms/cli check

For 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