Skip to content

Guide

Delivery API

Everything a receiving system needs in order to accept content from BrandKind: what arrives, how it is authenticated, what BrandKind does when your endpoint is unhappy, and what you must implement to be a well-behaved receiver.

Which way the traffic goes

BrandKind calls you. There is no API you call — no key to issue, no endpoint to hit, no rate limit to plan around on your side. You stand up an HTTP endpoint, register it as a destination, and BrandKind POSTs finished pieces to it.

A delivery is triggered either by a person choosing Send to… on a piece, or by the automatic retry sweep re-attempting an earlier failure. Both look identical from your side.

The request

Always a POST to the exact URL you registered, with a single piece in the body. There is no batching — one request carries one piece.

Shape of the request
POST /webhooks/brandkind HTTP/1.1
Host: cms.example.org
Content-Type: application/json; charset=utf-8
Authorization: Bearer <your secret>

{ …the JSON payload above… }
Request properties
PropertyValue
MethodPOST
Schemehttp or https. https is required whenever a secret is configured.
PortsStandard web ports only. Non-standard ports are rejected before sending.
Content-TypeSet from the destination’s wire format — see the next section.
BodyOne piece. Never an array, never multiple pieces.
TimeoutBrandKind waits 10 seconds per attempt, per redirect hop.

Wire formats

A destination is configured with one of three wire formats, chosen when you create it. The format decides both the Content-Type header and the entire body.

FormatContent-TypeBody
htmltext/html; charset=utf-8The piece’s rendered HTML, and nothing else. No wrapper, no metadata.
markdowntext/markdown; charset=utf-8The piece as Markdown. Falls back to the plain-text extract if the document produces no Markdown, so the body is never silently empty.
jsonapplication/json; charset=utf-8A structured envelope carrying all three renderings plus metadata. Documented below.

Choosing one

Pick json unless you have a reason not to. It is the only format that carries the piece id, the delivery id, the scores and the image — and the only one you can extend against later without reconfiguring the destination.

The JSON payload

Example
{
  "delivery_id": "9f1c2e64-3a55-4d1b-9d0e-7c2a1b8f4e33",
  "piece_id": "1b7d90aa-42c8-4a2f-8f61-0c5e9d3a77b1",
  "title": "Winter gritting routes explained",
  "format": "blog",
  "content_html": "<h1>Winter gritting routes explained</h1><p>…</p>",
  "content_markdown": "# Winter gritting routes explained\n\n…",
  "content_text": "Winter gritting routes explained\n\n…",
  "content_meta": {},
  "humanise": {
    "reading_age_target": 12,
    "ai_score_pre": 91,
    "ai_score_post_claude": 44,
    "ai_score_post_gpthuman": 8,
    "humanised_at": "2026-08-03T09:41:22.318Z"
  },
  "image": {
    "url": "https://…/piece-images/…?token=…",
    "layout": "banner"
  }
}
Top-level fields
FieldTypeMeaning
delivery_idUUIDThis delivery attempt. Stable across every retry of the same delivery — use it as your idempotency key.
piece_idUUIDThe piece. Stable for the life of the piece, across every delivery of it, to every destination.
titlestringThe piece’s title as it stands now.
formatstringOne of the five piece formats. Not to be confused with the destination’s wire format.
content_htmlstringRendered HTML. Empty string if the piece has none — never null.
content_markdownstringMarkdown rendering, with the same plain-text fallback as the markdown wire format.
content_textstringPlain-text extract, useful for search indexing and previews.
content_metaobjectPer-piece metadata. Always an object; empty when there is none. Treat unknown keys as additive — new ones may appear.
humaniseobjectThe scoring record for this piece. Detailed below.
imageobject or nullThe feature image, or null when the piece has none.

The humanise object

These are the raw detection numbers, on a 0–100 scale where higher means more likely to be flagged as machine-written. Note that this is the opposite direction to the Human score shown in the interface, which is inverted so that higher is better. If you surface these numbers to your own users, invert them or you will show people a score that reads backwards.

FieldMeaning
reading_age_targetThe reading age the piece was aimed at, or null.
ai_score_preDetection score before any humanising pass ran.
ai_score_post_claudeDetection score after the first rewriting pass.
ai_score_post_gpthumanDetection score after the final pass. This is the number the interface inverts into the Human score.
humanised_atWhen the humanising pass completed, or null if it never ran.

The image object

Present only on json destinations, and only when the piece has a feature image selected. It carries a url and a layout describing how the image was meant to sit against the text.

The image URL expires

The URL is signed and valid for 24 hours. Download the image on receipt and store your own copy. A receiver that saves the URL and serves it later will start returning broken images the next day.

Authentication

A destination carries one of three authentication modes. BrandKind holds the secret; you set it once and can rotate or clear it, but nobody — including workspace admins — can read it back afterwards.

ModeWhat BrandKind sends
noneNo authentication header at all.
bearerAuthorization: Bearer <secret>
custom_headerA header of your choosing carrying the bare secret as its value — for example X-Webhook-Token: <secret>. The header name must be a valid HTTP token.

Two rules worth knowing before you debug

A destination set to bearer or custom_header with no secret saved sends no authentication header at all — the request arrives unauthenticated rather than carrying an empty credential. If your endpoint is rejecting BrandKind with a 401, check that the secret is actually set.

Storing a secret on a destination whose URL is plain http is refused outright. A credential never travels unencrypted, and the delivery fails permanently rather than falling back.

Redirects

BrandKind follows redirects, but not blindly. Up to three hops are allowed, and every hop is re-checked against the same address rules as the original URL before it is followed.

Your credential is dropped across origins

If a redirect sends the request to a different origin, the authentication header is removed before the next hop. This is deliberate — a redirect must never be able to hand your secret to another host. If your infrastructure redirects between hostnames, register the final URL directly instead.

What counts as success

Any 2xx response marks the delivery sent. The response body is not read and not stored — return whatever you like, or nothing.

Anything else — a non-2xx code, a connection failure, a timeout — marks the delivery failed and schedules a retry. The HTTP code you returned is recorded against the delivery so whoever sent it can see what happened.

Delivery statuses
StatusMeaning
queuedAccepted but not yet attempted, or deferred because a quota was reached. No attempt has been spent.
sentYour endpoint returned 2xx. Terminal — nothing further happens.
failedThe attempt did not succeed. Retried on the schedule below unless retries are exhausted or the failure is permanent.

Retries

A failed delivery is retried automatically on a widening schedule. After the fifth attempt BrandKind stops and the delivery stays failed — it is never retried again without someone sending it manually.

Retry schedule
Attempts so farNext attempt in
11 minute
25 minutes
315 minutes
460 minutes
5Never — retries exhausted

That gives your endpoint roughly 81 minutes of grace across five attempts. An outage shorter than that resolves itself with nothing lost.

Duplicates are possible, so be idempotent

BrandKind retries when it cannot confirm success, which includes the case where your endpoint processed the request and then timed out replying. Key on delivery_id and ignore one you have already handled. This is the one piece of work a receiver genuinely must do.

Failures that are not retried

Some failures are configuration problems that retrying cannot fix. These are recorded once with a plain explanation and no retry is scheduled.

  • The destination is paused. Nothing is sent — deliberately, and visibly, rather than silently dropped.
  • A secret is configured against a plain http URL.
  • The custom header name is not a valid HTTP header name.

Rate limits and quotas

Each workspace has a per-minute and a per-day delivery budget. They protect both your endpoint and the platform from a runaway loop. They behave differently, and the difference matters if you are reading delivery records:

Limit reachedWhat happens
Per-day budgetThe delivery is deferred to the start of the next day window and stays queued. No attempt is spent, so it does not eat into the five retries.
Per-minute budgetThe delivery is marked failed and retried on the normal schedule. An attempt is spent.
Budget cannot be checkedThe delivery is deferred for a few minutes and retried. BrandKind will not send what it cannot account for.

Which addresses are allowed

Destination URLs are validated before every send, not just when you save them. Addresses that resolve to private, loopback, link-local or otherwise reserved ranges are refused, as are non-standard ports and schemes other than http and https.

This is why a destination pointing at something inside your own network — a machine onlocalhost, or a private address — will never work, no matter how the firewall is set up. Expose a public endpoint, or use a tunnel that gives you a public hostname.

Implementing a receiver

A minimal correct receiver is short. In order of what actually matters:

  1. Accept POST and reply fast.

    Return 2xx as soon as you have safely stored the payload. Do the slow work — image download, indexing, publishing — afterwards. You have 10 seconds; use one of them.
  2. Check the credential.

    Compare the incoming secret against the one you configured, and reject anything else with a 401. Compare in constant time if your language makes that easy.
  3. Deduplicate on delivery_id.

    If you have seen it, return 2xx and do nothing else. This is what makes retries safe.
  4. Fetch the image now, if there is one.

    The signed URL is good for 24 hours and no longer. Store your own copy.
  5. Tolerate new fields.

    Ignore keys you do not recognise rather than failing on them. The payload gains fields over time and never removes them without notice.
  6. Return a real status code when you fail.

    A 500 tells BrandKind to retry, which is usually what you want. Returning 2xx on failure loses the content permanently.

Testing your endpoint

Create a destination pointing at your endpoint, set it to json, then use Send to… on any piece. The delivery record shows the status, the HTTP code you returned, the attempt count and the reason for any failure — which is enough to debug most integrations without instrumenting your own side.

To test the failure path, return a 500 deliberately and watch the retry schedule play out. To test the permanent-failure path, pause the destination and send again.