Diil Docs
  1. Docs
  2. Getting started

Quickstart: first request in five minutes

Updated:

Five minutes, one page, zero admin panels. By the end of this guide a headline on your site will come from the Diil CRM — and your editors will be able to change it by clicking on it. Grab a coffee; you might not finish it.

You will need:

  • access to a site in the Diil CRM (you can create a fresh one for experiments);
  • a terminal with curl, or any tool that can send an HTTP request;
  • any frontend: a plain HTML file, React, Next.js, Nuxt — your call.

Set up content and a key

  1. Create some content in the CRM

    Open your site in the CRM and make sure the languages you need are added (say, English). Then create:

    • a page with the marker home;
    • inside it, a section with the marker hero;
    • inside that, a text block with the marker hero_title — type a headline into it.

    Markers are the names your code uses to find content: Latin letters, digits and underscores, 2–40 characters, case-sensitive. Pick them like variable names — they will live in your code for a long time. Here is the full picture of pages, sections and blocks.

  2. Get a site key

    Go to Settings → Content API keys and create a key. It looks like pk_ followed by 32 hex characters and is bound to this one site.

    A new key usually works right away; in the worst case give it a few minutes. Revoking works the same way — revoked keys get 401 invalid_key.

  3. Make your first request

    Ask for the home page in English:

    curl "https://back.sitecog.com/content/v1/pages/home?lang=en" \
      -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"
    200 OKjson
    {
      "id": 26,
      "marker": "home",
      "name": "Home",
      "href": "/",
      "index": 0,
      "params": {},
      "content": {
        "hero": {
          "id": 41,
          "marker": "hero",
          "name": "Hero",
          "index": 0,
          "show": true,
          "content": {
            "hero_title": {
              "id": 95,
              "marker": "hero_title",
              "name": "Hero title",
              "type": "text",
              "multilang": true,
              "updatedAt": "2026-09-20T16:33:23.000Z",
              "content": { "en": "Earbuds that mute the city" }
            }
          }
        }
      }
    }

    See the path to your headline? content.hero.content.hero_title.content.en — page → section → block → language. Every page has exactly this shape; the details are in Pages.

Render it on your page

Same request, four flavours. Pick yours — they all do the same thing: fetch the page and put the headline into an <h1>.

<h1 id="hero-title"></h1>

<script type="module">
  const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
    headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
  });
  const page = await res.json();
  const hero = page.content.hero.content;

  document.getElementById('hero-title').textContent = hero.hero_title.content.en;
</script>

Where to keep the key

Put the key in an environment variable rather than in the code — not because it is a secret, but because it makes switching sites or rotating keys a one-line change.

.env.localbash
# .env.local (Next.js) — server-only, never sent to the browser
CRM_KEY=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40

# Browser code needs a public variable. That's fine: the key is read-only.
NEXT_PUBLIC_CRM_KEY=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40
# Vite: VITE_CRM_KEY=…   Nuxt: NUXT_PUBLIC_CRM_KEY=…

In a server component (like the Next.js tab above) use the server-only variable: the key never leaves your server. For browser code a public variable is perfectly fine.

Add a language helper

Text blocks come as language maps: { "en": "…", "de": "…" }. The API never swaps one language for another: if a translation is missing, it is simply not there (or it is an empty string if the editor left the field blank). Choosing a fallback is your call — and this tiny helper makes it a one-liner:

// lib/t.js
// Pick a translation: requested language → fallback language → first non-empty → ''
export function t(map, lang, fallback = 'en') {
  if (!map) return '';
  return map[lang] || map[fallback] || Object.values(map).find(Boolean) || '';
}
Usagejs
// Ask for the visitor's language and the fallback in one request
const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=de,en', {
  headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
});
const page = await res.json();
const hero = page.content.hero.content;

t(hero.hero_title.content, 'de');       // "Kopfhörer, die die Stadt stummschalten"
t(hero.hero_title.content, 'de', 'en'); // German not filled in yet? → the English text

Every code in ?lang= must be an active language of the site, otherwise you get 400 unknown_lang with the list of available ones. Leave lang out and you get all active languages at once. The whole story is in Languages & fallbacks.

Switch on live editing

Now the fun part. Your page already shows content from the CRM; let's let editors change it right on the page, without hunting for the right field in a form.

  1. Add the widget

    One script, once per page, right before </body>. It loads the editor only when your site is opened inside the CRM, so regular visitors don't download a single byte of it.

    <script src="https://widget.sitecog.com/widget.js" defer></script>
  2. Tell the editor which element shows which block

    Add data-crm-text with the block marker to the element that renders it. Keep rendering the text from the API as before — the attribute just connects the element with the block.

    <body>
      <h1 data-crm-text="hero_title">Earbuds that mute the city</h1>
    
      <!-- once per page, right before </body> -->
      <script src="https://widget.sitecog.com/widget.js" defer></script>
    </body>

    Images, videos, objects and arrays have their own attributes (data-crm-image, data-crm-video, data-crm-object, data-crm-array) — see Widget & markup. The attributes are harmless for visitors, so leave them in production.

  3. Allow the CRM to frame your site

    Live mode opens your site inside the CRM, in an iframe. Your server has to allow that with frame-ancestors, and must not send X-Frame-Options: DENY or SAMEORIGIN. Otherwise the CRM will tell you that the site forbids embedding.

    // next.config.js
    module.exports = {
      async headers() {
        return [{
          source: '/:path*',
          headers: [
            { key: 'Content-Security-Policy', value: "frame-ancestors 'self' https://sitecog.com" },
          ],
        }];
      },
    };
  4. Click, type, save

    Open your site in the CRM in Live mode, click the headline, change it and save. The CRM writes the block, the content version goes up, your page fetches fresh content — and the new headline is right there. 🎉

    Bonus: if you put data-crm-text="promo_note" on an element before such a block exists, the editor can create the block right from the site.

Skip the cache for editors

Our side never serves stale content in Live mode. But your cache can: with revalidate: 60 an editor might save and still see the old text for up to a minute. Inside the CRM frame the URL gets ?crm_live=1 — use it to fetch with cache: 'no-store':

// app/page.tsx — skip every cache while an editor is looking
const API = 'https://back.sitecog.com/content';

export default async function Home({ searchParams }: { searchParams: Promise<{ crm_live?: string }> }) {
  const live = (await searchParams).crm_live === '1';

  const res = await fetch(API + '/v1/pages/home?lang=en', {
    headers: { 'x-crm-key': process.env.CRM_KEY! },
    ...(live ? { cache: 'no-store' as const } : { next: { revalidate: 60 } }),
  });
  const page = await res.json();
  const hero = page.content.hero.content;

  return <h1 data-crm-text="hero_title">{hero.hero_title.content.en}</h1>;
}

What next

You have content flowing and editors clicking. Here is where to go deeper: