Destinations: send themes to Linear, GitHub, Slack and more
Last updated October 25, 2026
A destination sends themes from Camelus to another app: Vicunia, an n8n, Zapier or Make workflow (which can open a Linear, GitHub or Jira issue), a Slack or Discord channel, or your own endpoint. They're all the same thing underneath: a signed HTTPS webhook. Presets only fill in the form.
Add one
Settings → Destinations → Add destination, then pick a preset:
| Preset | Sends | Reports back |
|---|---|---|
| Vicunia | The theme as a Vicunia request | Build progress and "shipped", through Vicunia's source callback |
| n8n, Zapier, Make | The canonical JSON | Your workflow calls the status callback |
| Slack, Discord | A short message to an incoming-webhook URL | No |
| Webhook | The canonical JSON, to any HTTPS endpoint | Your code calls the status callback |
You choose:
- Products: all, or some.
- When to send: by hand with Send to… on a theme, automatically on events (Pro), or both.
- Filters for automatic theme events: a minimum score, a minimum number of requesters, types, paying requesters only.
- Auth header: an extra header such as
Authorization: Bearer …, stored encrypted.
The signing secret (whsec_…) is shown once when you create the destination. Rotate secret makes a new one; for 24 hours requests are signed with both, so you can switch your receiver over without missing anything.
Use Test to send a test.ping event. The delivery log shows every request with its status code, timing, body and the first 2 KB of the response, with Retry and Replay.
Events
| Event | When |
|---|---|
theme.sent |
Someone clicks Send to… on a theme |
theme.created |
A theme gets its second item (or an urgent bug) |
theme.rising |
A theme's trend turns to rising |
theme.threshold_reached |
A theme passes the destination's filters (once per theme) |
theme.status_changed |
A theme's status changes, in Camelus or through a callback |
theme.merged |
Two themes are merged (the payload is the surviving theme) |
feedback.received |
A feedback item is processed |
feedback.urgent |
An item is classified as high urgency |
digest.ready |
The daily or weekly digest is computed |
test.ping |
The Test button |
Payload
POST https://your-endpoint.example.com/camelus
Content-Type: application/json
User-Agent: Camelus-Webhooks/1
Camelus-Event: theme.sent
Camelus-Delivery: dlv_6f1c…
Camelus-Timestamp: 1792915200
Camelus-Signature: v1=5c2a…
Camelus-Version: 2026-10-01
{
"id": "evt_9b2e…",
"type": "theme.sent",
"created_at": "2026-10-25T08:00:00.000Z",
"workspace": { "id": "k17…" },
"product": { "id": "k57…", "slug": "habits", "name": "Habits", "domain": "habits.app" },
"data": {
"theme": {
"id": "k97d…",
"title": "CSV export ignores date locale",
"summary": "Exports use US dates whatever the user's locale.",
"type": "bug",
"status": "open",
"score": 4.2,
"trend": "rising",
"requesters": 17,
"paying_requesters": 6,
"mrr_affected_cents": null,
"item_count": 21,
"sentiment": { "negative": 82, "neutral": 18, "positive": 0 },
"last_item_at": "2026-10-25T07:41:00.000Z",
"url": "https://camelus.si/app/themes/k97d…",
"representative_items": [
{ "id": "k3a…", "summary_en": "CSV export uses US dates for a French user.", "source": "widget", "received_at": "2026-10-25T07:41:00.000Z" }
]
}
},
"callback": { "url": "https://api.camelus.si/v1/callbacks/k97d…", "token": "cbt_…" }
}
Payloads carry AI summaries and counts, never original text, names or emails. A workspace owner can turn on Include requester emails for one destination (for your own CRM, say); that choice is logged. mrr_affected_cents is null until a billing connector is set up: it's never estimated. Every schema is in the OpenAPI document, under webhooks.
Verify the signature
The signature is HMAC-SHA256, keyed with the whole signing secret, over {timestamp}.{raw body}. During a rotation the header holds two signatures, v1=…,v1=…; accept either.
import crypto from "node:crypto";
export function verifyCamelus(rawBody, headers, secret) {
const ts = headers["camelus-timestamp"];
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // older than 5 minutes: a replay
const expected = crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
return (headers["camelus-signature"] ?? "").split(",").some((part) => {
const sig = part.trim().replace(/^v1=/, "");
return sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
});
}
Use the raw body exactly as received, before any JSON parsing.
Delivery
- Answer with any
2xxwithin 10 seconds. Redirects are followed (at most 3). - Anything else is retried after about 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours: 7 attempts over a day.
410 Goneturns the destination off right away. - After 20 failed deliveries in a row, or 3 days of failures, the destination is paused and the owner gets an email. Resume it once the endpoint works.
- Order isn't guaranteed. Use
created_atto order events, andCamelus-Delivery(the same on every retry) to ignore repeats. - URLs must be public
https://addresses. Local and private networks are refused, also after DNS resolution and after redirects.
n8n
Two ready-made workflows: download, then Import from file in n8n.
- Camelus → Linear: a sent theme becomes a Linear issue; the callback reports it as planned, and as shipped when the issue is completed.
- Camelus → GitHub Issues: the same with a GitHub issue, shipped when it's closed.
After importing: paste your signing secret into the Verify node, pick your Linear team or GitHub repository and credentials, then copy the workflow's production webhook URL into a Camelus destination (preset n8n). The callback URL and token travel in a footer of the issue, so closing it reports back without a database.
Zapier
- Create a Zap with Webhooks by Zapier → Catch Raw Hook, and paste its URL into a destination (preset Zapier).
- Add a Code by Zapier step to check
Camelus-Signaturewith the snippet above (the raw body and headers are in the trigger's output). - Add Linear → Create Issue or GitHub → Create Issue, with the theme's title, summary and
url. Putcallback.urlandcallback.tokenin the description. - In a second Zap triggered when the issue is completed or closed, add Webhooks by Zapier → POST to the callback URL from the description, with the header
Authorization: Bearer <token>and the body{"status": "shipped", "external_id": "<issue id>", "external_url": "<issue url>"}.
Make
- Start a scenario with Webhooks → Custom webhook and paste its address into a destination (preset Make). Turn on Get request headers and JSON pass-through so the raw body is available for the signature.
- Add Linear → Create an issue or GitHub → Create an issue, mapping
data.theme.title,data.theme.summaryanddata.theme.url, plus the callback block. - In a second scenario watching for completed or closed issues, add HTTP → Make a request:
POSTto the callback URL, headerAuthorization: Bearer <token>, JSON body withstatus: shipped.
Slack and Discord
Create an incoming webhook in Slack (Apps → Incoming Webhooks) or Discord (Channel settings → Integrations → Webhooks) and paste its URL. By default they get new themes, rising themes, urgent feedback and the digest. Mentions in summaries never ping anyone.