Skip to content

Umbraco package

The CitationLab package for Umbraco

CitationLab.Umbraco connects an Umbraco site to AI Marketing Manager. Once paired, Manager can read your content and update SEO metadata, structured data, media and redirects, and your developers get a documented REST API on your own domain. Written for site owners, Umbraco developers and the agencies that run sites for clients.

Package at a glance

Package
CitationLab.Umbraco
Version
2.0.0
Umbraco
13 LTS
Runtime
.NET 8
License
MIT
dotnet add package CitationLab.Umbraco

Requirements

Check these before you install.

  • Umbraco 13 LTS on .NET 8

    This is the version the package targets.

  • Umbraco 14 and later: not yet

    Umbraco 14 changed how backoffice extensions are built, so the dashboard has to be rewritten before the package supports it. The API side is not affected by that change.

  • Outbound HTTPS

    The web server must be able to reach manager.citationlab.ai over HTTPS, or pairing fails.

  • An admin user

    Pairing and the feature switches live in a dashboard only users in Umbraco's admin group can see.

1. Install

Add the package to the project that runs your Umbraco site.

dotnet add package CitationLab.Umbraco
  1. 1

    Add the package

    From the folder of your Umbraco web project, run dotnet add package CitationLab.Umbraco.

  2. 2

    Rebuild

    Build the solution the way you normally do and deploy it.

  3. 3

    Restart the site

    On start-up the package creates its own database tables. No content or template is touched.

Optional configuration

You only need configuration to point the package at a different Manager, such as a staging environment. Without it, the package talks to manager.citationlab.ai.

appsettings.json
{
  "CitationLab": {
    "ManagerUrl": "https://manager.citationlab.ai"
  }
}

Errors from the package are written to the normal Umbraco log with the prefix [citationlab], so you can filter for them.

2. Pair with AI Marketing Manager

Pairing gives the site a permanent API key and tells Manager where the site lives. It takes a token from Manager and one click in the Umbraco backoffice.

  1. 1

    Generate a pairing token in Manager

    In AI Marketing Manager, open the project and go to Integrations → Umbraco → Connect, then choose “Generate pairing token”.

  2. 2

    Open the CitationLab dashboard

    Sign in to the Umbraco backoffice as an admin, open the Content section and select the “CitationLab” dashboard.

  3. 3

    Paste the token and click “Pair”

    The site sends the token, its own URL and basic site information to Manager, and receives its API key in return. The dashboard then shows the site as connected.

  • A token works once and expires after 15 minutes. If it runs out, generate a new one.
  • Pair from the backoffice on your public hostname. Manager stores the address you were on when you clicked Pair, so pairing from localhost or an internal name leaves Manager with an address it cannot use.
  • Pairing needs a backoffice login on purpose: it decides who may read and write the whole site, so knowing the site's URL is not enough.
  • One Umbraco site per project. Pairing a project that is already connected replaces the URL and key of that connection.
  • To disconnect, use “Disconnect” in the dashboard, which removes the key stored on the site, or remove the connection in Manager, which deletes the connection and Manager's cached copy of your content. Removing it in Manager does not change anything on the Umbraco site.

3. Choose what the package does

Installing the package changes no page on your site. Only the sync group is on from the start, so Manager can read your content. Everything that would change your HTML, handle requests or collect visitor data stays off until you turn it on.

3. Choose what the package does
GroupDefaultWhat it coversSwitch keys
SyncOnReading content, SEO fields, media, taxonomy and document types, and the package's audit log of every write.content_syncseo_syncmedia_accesstaxonomy_syncdocument_type_syncaudit_log
Page outputOffWhat the package writes into your pages and site files: meta and Open Graph tags, structured data, breadcrumbs, custom and verification tags, robots.txt, sitemap, llms.txt and other AI-facing files.meta_outputschema_outputog_outputbreadcrumbscustom_meta_tagsverification_tagsrobots_managementsitemapllms_txtaeo
Request handlingOffRedirects, a log of 404s, a link scanner and an integrity check.redirectsmonitor_404link_scannerintegrity_checker
Visitor dataOffTracking and bot tracking, server-side page views, tag manager and consent, GA4 e-commerce, A/B tests, heatmaps, scroll maps, Web Vitals and surveys.trackingbot_trackingserver_pageviewtag_managerconsentga4_ecommerceab_testingheatmapscrollmapweb_vitalssurveys
IntegrationsOffUmbraco Commerce, Umbraco Forms, members, the dictionary, webhooks, email alerts and Cloudflare.commerceformsmembersdictionarywebhooksemail_alertscloudflare

Where to switch features

An admin turns features on and off under Features in the CitationLab dashboard. Developers can do the same through the API with PUT features. Manager reads what is switched on through GET capabilities, which reports both what the installation can do and what is enabled. A call to a feature that is off answers 403 with the code feature_disabled.

4. API reference

The package serves a REST API from your own Umbraco site. This is the API Manager uses after pairing, and your developers can use it too. It is separate from Manager's own REST API, which is described under Resources → API.

Manager's REST API →

Base URL

Every route sits under this path on your site's own domain:

https://your-site.example/umbraco/citationlab/v1/

Authentication

  • Send the key from pairing as a bearer token: Authorization: Bearer cl_um_… The key is cl_um_ followed by 40 hexadecimal characters.
  • GET health is public and needs no key.
  • The pairing routes (pair, pairing-status, disconnect) do not accept the key. They need a signed-in backoffice session.

Conventions

  • Query parameters are snake_case: per_page, document_type_alias, parent_id, order_by, include_properties.
  • Request and response bodies are JSON in camelCase.
  • Lists are paged and return items, total, page, perPage and totalPages. per_page defaults to 100 and is capped at 500.
  • Content reads and writes take an optional culture parameter. One culture can be published on its own with POST content/{id}/translations/{culture}/publish.
  • A node's {id} is its GUID key or its numeric id.

Errors

Every error has the same shape: error is a readable message, code is one of the values below, and details is present when there is more to say.

Errors
CodeHTTPMeaning
invalid_request400The request is malformed, a parameter has the wrong value, or a required body is missing.
feature_disabled403The feature behind this route is switched off. details.feature names the switch.
not_found404The node, media item or record does not exist.
operation_failed500Umbraco could not complete the operation. The site's log has the details.
unsupported501The site cannot do this, for example because a required Umbraco add-on is not installed.
A call to a feature that is off
HTTP/1.1 403 Forbidden

{
  "error": "Feature 'redirects' is disabled on this site",
  "code": "feature_disabled",
  "details": { "feature": "redirects" }
}

Endpoints

The main routes in each group, relative to the base URL. Each group also lists further resources it contains.

Discovery

Discovery
MethodPathWhat it does
GEThealthIs the package running? Public, no key.
GETversionThe installed package version.
GETsite-infoSite name, URL and Umbraco version.
GETcapabilitiesWhat this installation can do and what is switched on.
GETfeaturesThe current feature switches.
PUTfeaturesTurn features on or off.

Content

Content
MethodPathWhat it does
GETcontentPaged list. Filters: status (published, draft, trashed), document_type_alias, search, culture, parent_id, order_by, direction, include_properties.
POSTcontentCreate a node.
GET · PUT · DELETEcontent/{id}Read, update or delete one node.
GETcontent/by-pathFind a node by its URL path.
GETcontent/{id}/childrenThe node's children.
POSTcontent/{id}/publishPublish.
POSTcontent/{id}/unpublishUnpublish.
POSTcontent/{id}/moveMove under another parent.
POSTcontent/{id}/copyCopy.
GETcontent/{id}/preview-urlA preview link for unpublished changes.
GETcontent/{id}/tagsThe node's tags.

Versions, scheduling and languages

Versions, scheduling and languages
MethodPathWhat it does
GETcontent/{id}/versionsVersion history.
GETcontent/{id}/versions/{versionId}One earlier version.
POSTcontent/{id}/versions/{versionId}/restoreRoll back to that version.
GET · PUTcontent/{id}/scheduleScheduled publish and unpublish dates.
GETlanguagesThe site's languages.
GETcontent/{id}/translationsWhich cultures a node exists in.
POSTcontent/{id}/translations/{culture}/publishPublish one culture only.

Bulk, search and document types

Bulk, search and document types
MethodPathWhat it does
POSTbulk/content/statusPublish or unpublish many nodes.
POSTbulk/content/updateUpdate many nodes.
POSTbulk/content/deleteDelete many nodes.
POSTbulk/seo-metaSet SEO fields on many nodes.
GETsearchSearch the site's own Examine index, in relevance order.
GETsearch/indexesThe available Examine indexes.
GETdocument-typesAll document types.
GETdocument-types/{alias}One document type and its properties.

SEO, structured data and AI files

SEO, structured data and AI files
MethodPathWhat it does
GET · PUTseo-meta/{id}Title, description, focus keyword, canonical and robots settings for a node.
GET · PUTcontent/{id}/ogOpen Graph and social card fields.
GET · PUTllmsThe site's llms.txt.
POSTllms/generatePropose an llms.txt from the site's structure. It is returned, not saved.
GET · PUTaeoSettings for the AI-facing files, and the address each one is served at.

Also in this group: meta-templates custom-meta-tags settings/verification-tags breadcrumbs term-meta seo-lock robots sitemap schema schema-graph entities citation-check ai-visibility

Redirects and 404s

Redirects and 404s
MethodPathWhat it does
GET · POSTredirectsList or create redirects.
GET · PUT · DELETEredirects/{id}Read, change or remove one redirect.
POSTredirects/importImport redirects.
GETredirects/exportExport redirects.
GET404-logLogged 404s, with routes to create redirects from them.

Also in this group: redirects/settings 404-log/stats link-scan integrity

Media

Media
MethodPathWhat it does
GET · POSTmediaList or upload media.
GET · PUT · DELETEmedia/{id}Read, change or delete a media item.
POSTmedia/{id}/fileReplace the file.
GETmedia/{id}/usageWhere the item is used.
POSTmedia/folderCreate a media folder.
GETmedia-typesThe site's media types.

Also in this group: alt-text/scan alt-text/stats alt-text/bulk-update

Operations

Operations
MethodPathWhat it does
GETaudit-logEvery write made through the package, with the previous value.
POSTaudit-log/{id}/restorePut the previous value back.
GET · PUTsettings/webhookWebhook receiver URL and secret. The secret is never returned.
POSTsettings/webhook/testSend a signed test delivery.

Also in this group: site-health alerts cloudflare migration compat tracking bot-log pageview tag-manager consent ga4-ecommerce

Umbraco-specific

Umbraco-specific
MethodPathWhat it does
GETdictionaryUmbraco dictionary items.
PUTdictionary/{key}Change a dictionary item.
GETmembersUmbraco members.
GETformsUmbraco Forms, when it is installed.
GETcommerce/productsUmbraco Commerce products, when it is installed.
GETcommerce/ordersUmbraco Commerce orders.

Also in this group: member-groups member-types commerce/customers commerce/categories

Umbraco Forms and Umbraco Commerce are read through a bridge that looks for them at runtime, so the package installs and runs on sites that do not have those licences. The routes then answer with unsupported.

Files served from the site root

With the matching page-output switches on, the package also answers these addresses on your domain:

/robots.txt/sitemap.xml/llms.txt/llms-full.txt/ai.json/.well-known/ai.json/ai-info

Examples

What can this site do, and what is on?
curl https://your-site.example/umbraco/citationlab/v1/capabilities \
  -H "Authorization: Bearer cl_um_YOUR_API_KEY"
Page 2 of the published English content, 50 per page
curl "https://your-site.example/umbraco/citationlab/v1/content?page=2&per_page=50&status=published&culture=en-US" \
  -H "Authorization: Bearer cl_um_YOUR_API_KEY"
The paging envelope
{
  "items": [ … ],
  "total": 412,
  "page": 2,
  "perPage": 50,
  "totalPages": 9
}
Update the SEO fields of one page
curl -X PUT "https://your-site.example/umbraco/citationlab/v1/seo-meta/NODE_KEY?culture=en-US" \
  -H "Authorization: Bearer cl_um_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Plumbing services in Oslo",
    "description": "Emergency and planned plumbing, 24/7.",
    "canonical": "https://your-site.example/services/plumbing/",
    "noindex": false
  }'

5. Webhooks

The package can send a signed notification to an HTTPS endpoint you choose whenever content or media changes: your own service, or an automation tool such as Zapier, Make or n8n.

  1. 1

    Turn on the webhooks switch

    The webhooks feature is off by default.

  2. 2

    Set the receiver

    PUT settings/webhook with a url (http or https) and a secret. GET settings/webhook shows the url and hasSecret, never the secret itself.

  3. 3

    Send a test

    POST settings/webhook/test delivers a signed test event so you can check the receiver before a real change.

Nothing is sent until the switch is on and both a url and a secret are set. Pairing already points the webhook at CitationLab Manager with its own secret, so turning the switch on is enough for Manager to see a published change within seconds. Set your own receiver and Manager leaves it in place.

Set the receiver
curl -X PUT https://your-site.example/umbraco/citationlab/v1/settings/webhook \
  -H "Authorization: Bearer cl_um_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://hooks.example.com/umbraco", "secret": "a-long-random-string" }'

Events

Events
EventSent when
content.savedA content node is saved.
content.publishedA content node is published.
content.unpublishedA content node is unpublished.
content.trashedA content node is moved to the recycle bin.
content.deletedA content node is deleted permanently.
media.savedA media item is saved.
media.deletedA media item is deleted.

What a delivery looks like

A JSON POST with event, siteUrl, timestamp (Unix seconds) and data. Fields that are null are left out. X-CL-Event repeats the event name, and X-CL-Signature carries the signature.

Example delivery
POST /umbraco HTTP/1.1
Host: hooks.example.com
Content-Type: application/json; charset=utf-8
X-CL-Event: content.published
X-CL-Signature: 3f1c…e9a0

{
  "event": "content.published",
  "siteUrl": "https://your-site.example",
  "timestamp": 1760000000,
  "data": { "id": "NODE_KEY", … }
}

Verify the signature

X-CL-Signature is the HMAC-SHA256 of the exact raw request body, keyed with your secret, as lowercase hex. Compute it over the raw bytes before you parse the JSON, and compare with a constant-time function. Re-serialising the parsed body changes the bytes and the check fails.

Node.js (Express)
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.CL_WEBHOOK_SECRET;

// Keep the raw bytes: the signature covers the body exactly as sent.
app.post("/umbraco", express.raw({ type: "application/json" }), (req, res) => {
  const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
  const received = req.get("X-CL-Signature") ?? "";

  const valid =
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  if (!valid) return res.sendStatus(401);

  const payload = JSON.parse(req.body.toString("utf8"));
  console.log(req.get("X-CL-Event"), payload.data);
  res.sendStatus(204);
});
C# (ASP.NET Core minimal API)
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

app.MapPost("/umbraco", async (HttpRequest request, IConfiguration config) =>
{
    // Keep the raw bytes: the signature covers the body exactly as sent.
    using var buffer = new MemoryStream();
    await request.Body.CopyToAsync(buffer);
    var body = buffer.ToArray();

    var key = Encoding.UTF8.GetBytes(config["CitationLab:WebhookSecret"]!);
    var expected = Convert.ToHexString(HMACSHA256.HashData(key, body)).ToLowerInvariant();
    var received = request.Headers["X-CL-Signature"].ToString();

    if (!CryptographicOperations.FixedTimeEquals(
            Encoding.ASCII.GetBytes(expected), Encoding.ASCII.GetBytes(received)))
        return Results.Unauthorized();

    using var payload = JsonDocument.Parse(body);
    return Results.NoContent();
});

Delivery and retries

  • Deliveries are queued and sent outside the save. Saving in the backoffice never waits for your receiver.
  • Any status below 400 counts as delivered.
  • After a network error, a timeout, a 5xx, 408 or 429, the package tries twice more: after 1 second and after 3 seconds. Other 4xx answers are not retried.
  • When all three attempts fail, the package gives up and writes a [citationlab] warning to the Umbraco log. Use the event as a signal to fetch the current state from the API, not as the only copy of the change.

Security and privacy

  • Pairing needs a backoffice login

    Only an admin signed in to your backoffice can pair a site. A URL alone cannot connect anything.

  • Where the key lives

    On the site, the key is stored in App_Data/CitationLab/apikey.json. Treat that file like a password and keep App_Data out of source control. Manager stores its copy encrypted.

  • Opt-in by default

    Only reading is on after install. Page output, request handling, visitor data and integrations each need a deliberate switch.

  • No raw IP addresses

    When tracking is on, the package never stores a visitor's raw IP address.

  • Every write is logged

    The package keeps its own audit log of every change made through it, with the previous value, and a change can be rolled back. Changes made from Manager are logged in Manager as well.

  • Secrets stay put

    The webhook secret is write-only through the API: reads return hasSecret, never the value.

Troubleshooting

Troubleshooting
What you seeWhat to do
Pairing token not foundCopy the token again from Manager, or generate a new one. If you have set CitationLab:ManagerUrl, the token must come from that Manager.
Pairing token already usedTokens work once. Generate a new one.
Pairing token expiredTokens are valid for 15 minutes. Generate a new one and pair straight away.
Could not reach CitationLab Manager: …The web server cannot make outbound HTTPS calls to manager.citationlab.ai. Allow it in the firewall or proxy and try again.
No CitationLab dashboard in the Content sectionYour user is not in the admin group, or the site runs Umbraco 14 or later, which the dashboard does not support yet.
403 with code feature_disabledThe route belongs to a feature that is off. details.feature names it. Turn it on under Features in the dashboard.
Manager shows a localhost or internal addressThe site was paired from that address. Pair again from the backoffice on the public hostname; it replaces the stored URL.
Webhooks never arriveCheck that the webhooks switch is on and that both url and secret are set, then send POST settings/webhook/test. Failed deliveries are logged with [citationlab].

Frequently asked questions

Running Umbraco for a client?

Tell us about the site and what you want Manager to handle, and we will help you get it paired and configured.

Was this page helpful?