Skip to content

Webhooks

Receive signed events when a brand is viewed, downloaded, updated or asked for access, and how to verify and retry them.

A webhook is an address you register. Tarkle sends a signed POST to it when something happens to a brand in the workspace. Webhooks are open to every plan. All calls here need the webhooks:manage scope, and the token’s creator must be an owner or admin of the workspace named in X-Workspace-Key. The Settings > API page in the app manages the same endpoints.

Register an endpoint

curl -X POST -H "Authorization: Bearer $TARKLE_TOKEN" -H "X-Workspace-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/tarkle", "events": ["brand.updated", "brand.downloaded"]}' \
  https://tarkle.com/api/v1/webhooks

The answer (201) holds endpoint and secret. The secret (whsec_...) is shown once, in this answer only. Store it.

Call What it does
GET /webhooks List the endpoints.
POST /webhooks Add one. Body: url, events (optional) and description (optional). Up to 20 per workspace.
GET, PATCH, DELETE /webhooks/{id} Read, change or remove one. PATCH takes url, events, description and enabled. Turning an endpoint on clears its failure count.
POST /webhooks/{id}/rotate-secret Make a new secret. The old one also works for 24 hours.
POST /webhooks/{id}/test Send one webhook.test event, once. It never changes the endpoint’s state.
GET /webhooks/{id}/deliveries Every attempt, newest first. Kept for 7 days.
POST /webhooks/{id}/deliveries/{delivery}/redeliver Send a delivery again as a fresh run. The endpoint must be on.

The address. https, a public DNS name (no IP number, localhost or internal name), no username or password, port 443 or 1024 and above, up to 2,000 characters. Tarkle checks it when you add it and before every send. The connection goes to the checked address, redirects are not followed, and Tarkle waits 10 seconds for an answer.

Choose events

Send an events list of event types, or ["*"] for every event except page views. brand.viewed is only sent to an endpoint that names it, alone or next to "*".

Event When data besides brand
brand.viewed A visitor opens the brand page (a person, not a crawler, once per half hour). occurred_at. No person, address or country.
brand.downloaded A file or the whole kit is downloaded, from the page or with a kit link. file (name, format), source (page or kit_link), occurred_at. Never the visitor.
brand.updated The brand is edited in the app, its library is saved, a file is uploaded, or PATCH /brands/{brand} runs. changed (the names of what changed, never the values), source (app or api), occurred_at. An edit that changes nothing sends nothing.
brand.request.created A visitor asks for access or to download. The same person asking again within a day is one request. request (id, kind, name, company, email), created_at. The message is not sent.
brand.request.approved, brand.request.rejected A request is decided in the app, in Slack or through the API. The same request, and decided_at.

data.brand always holds id, slug and name. kind is view or download.

What your server receives

A POST with Content-Type: application/json and this body:

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody, header, secret) {
  const pairs = header.split(",").map((part) => part.split("="));
  const t = pairs.find(([key]) => key === "t")?.[1];
  if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  // During a rotation there are two v1 values; one match is enough.
  return pairs
    .filter(([key]) => key === "v1")
    .some(([, hex]) => {
      const given = Buffer.from(hex ?? "", "hex");
      return given.length === expected.length && timingSafeEqual(given, expected);
    });
}

For 24 hours after a rotation the header carries two v1 values, one for the new secret and one for the old. Accept either.

Retries and health

Answer with any 2xx. Anything else (a redirect too), a timeout or a connection error is a failed try. Tarkle tries up to 8 times: after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 8 hours and 8 hours, about 24 hours in all.

  • 410 Gone stops at once and switches the endpoint off.
  • An endpoint is also switched off after 5 days without one success, or when 20 deliveries in a row used all their tries. The workspace owner gets an email and disabled_reason says why.
  • Turn it back on with PATCH { "enabled": true }.

Type to search…

↑↓ navigate↵ selectEsc close