CodeHerderSearch⌘KRequest access →

Inbound webhooks

Let an external system trigger an action on your tasks by sending a signed payload to a URL you control, matched against rules you define.

An inbound webhook endpoint is the reverse of a regular webhook: instead of CodeHerder notifying you, an external system notifies CodeHerder. You get a receive URL and a signing secret, plus a small list of rules that decide what an incoming payload should do: post a comment, create a task, resolve a blocker, or report a merge outcome.

Typical uses: your CI system comments on the task when a pipeline finishes, a monitoring tool opens a task when an alert fires, or your source host reports back that a pull request merged, closed, or is still open.

If you’re looking for the other direction, CodeHerder sending you a payload when something happens in your workspace, see Webhooks instead.

How it works

Each endpoint is a receive URL + signing secret + an ordered list of rules. When a request arrives, CodeHerder checks its signature, then walks the rules top to bottom. The first rule whose conditions all match wins, and its action runs (an enabled rule with no conditions always matches). If nothing matches, nothing happens.

Every action runs as one member of your workspace (the endpoint’s acts-as member), so it’s subject to the same permissions as if that person had done it by hand. See Who it acts as below.

Who can manage inbound endpoints

  • Owners and admins can create, edit, delete, rotate, enable/disable, dry-run, and write rules for an inbound endpoint.
  • Members can view an endpoint, its rules, and its delivery history, but cannot change anything.

Inbound endpoints are available on every plan; there’s no tier requirement.

Creating an endpoint

In the web app

Open Settings → Webhooks, then the Inbound tab, and click + New inbound endpoint. Give it a name and, optionally, a description and an acting member (see Who it acts as below; it defaults to you).

Once created, a banner shows your signing secret — copy it now. It’s shown exactly once and can’t be retrieved later; if you lose it, rotate the endpoint to get a new one (see below). Your receive URL isn’t as urgent: it’s shown on the endpoint’s row in the list and again on its detail page, so you can come back for it anytime.

An endpoint you create in the web app always uses the CodeHerder signature scheme — to create a GitHub-signed endpoint instead, use the CLI (below). Either way, the endpoint’s detail page shows which scheme it uses.

From the CLI

ch inbound-endpoint create --name "CI pipeline" [--description "..."] [--acts-as <member>] [--signature-scheme codeherder-v1|github-v1]

The command prints the receive URL and the signing secret. Store the secret securely; it won’t be shown again.

--signature-scheme picks which signature format the endpoint checks incoming requests against — see Signing requests below. It’s fixed once you create the endpoint: ch inbound-endpoint edit can’t change it. To switch schemes, delete the endpoint and create a new one.

Signing requests

CodeHerder only accepts a request signed with the endpoint’s secret, which keeps the URL safe even from someone who finds or guesses it. An endpoint checks only its own scheme — send it the other scheme’s headers and it refuses the request, the same as an unsigned one.

The CodeHerder scheme (codeherder-v1)

This is the default, and it’s what a web app endpoint always uses. It’s identical to CodeHerder’s own outbound webhook signatures, just with the roles reversed: you’re the sender now. Send these two headers with every request:

Header Value
X-CodeHerder-Timestamp The current Unix time, in decimal seconds.
X-CodeHerder-Signature sha256= followed by the lowercase hex HMAC-SHA256 of "{timestamp}.{raw body}", keyed with the signing secret.

CodeHerder rejects any request whose timestamp is more than 5 minutes from its own clock, even if the signature is otherwise valid. That’s what stops an old captured request from being replayed later.

See Webhooks → Verifying deliveries for the exact signing recipe and a worked code example. The math is the same either direction; only who computes it and who checks it swap places.

The GitHub scheme (github-v1)

Pick this scheme when the sender is a GitHub repository webhook, so you can point it at CodeHerder without writing any signing code — GitHub computes these headers for you. Every request carries:

Header Value
X-Hub-Signature-256 sha256= followed by the lowercase hex HMAC-SHA256 of the raw request body, keyed with the signing secret. No timestamp is part of the signed message.
X-GitHub-Delivery A unique ID for the delivery. Required — a request without it is refused.

A github-v1 endpoint runs no timestamp freshness check; there’s no timestamp to check. Instead, CodeHerder remembers each X-GitHub-Delivery value for a bounded window and refuses a repeat within it. Outside that window, a repeat goes through again — so using GitHub’s Redeliver button well after the original delivery runs the action a second time. Keep that in mind for an action like posting a comment, which isn’t safe to run twice.

Rules

A rule has three parts: conditions that decide whether it matches, a resolver that finds which task it applies to, and an action that runs against that task.

Conditions

A condition reads one value out of the incoming request and compares it. Every condition in a rule must match (they’re AND-only) for the rule itself to match.

Field Meaning
source Where to read from: body (the JSON payload) or header (a request header).
path For body, a dot-separated path into the JSON, e.g. commits.0.id. For header, the header name, e.g. X-Source.
op The comparison to make (see the operator table below).
value What to compare against (not used by exists).

Operators:

Op Matches when
equals the value is exactly equal.
not_equals the value is present and different.
contains the value contains value as a substring.
exists the field is present at all. No comparison.
one_of the value equals any entry in a comma-separated value list, e.g. "opened,reopened".

A field that isn’t present is false for every operator, including not_equals. This is the single most common reason a rule doesn’t fire: a missing path never counts as “different,” it just never matches. Use exists if you specifically want to test for presence.

A rule with no conditions at all is a catch-all — it matches every request. That’s fine as your very last rule, but a catch-all placed earlier makes every rule after it unreachable, since the first match always wins.

Resolvers

resolver is required on every rule — there’s no default. It decides which task the rule’s action applies to, and each action only takes specific resolvers: create_task takes only none, since it creates a task rather than finding one. The other three actions — comment, resolve_blocker, and report_merge_outcome — each take task_id, merge_ref, or merge_url, and never none, since all three need an existing task to act on.

Resolver Finds the task by
none no lookup at all. The resolver create_task uses.
task_id reading a CodeHerder task ID directly from the payload.
merge_ref matching a host, repository, and pull/merge request number against a task’s linked merge reference.
merge_url the same lookup as merge_ref, from a single pull/merge request URL instead of three separate fields.

If a lookup resolver can’t find a match, or finds more than one, the action doesn’t run. See Delivery outcomes below; that’s a normal, silent no-op, not an error you need to act on.

Actions

Action What it does
comment Posts a comment on the resolved task.
create_task Creates a new task in the endpoint’s workspace, from a title (a literal or a payload path) and an optional description, type, and priority. Pairs only with the none resolver.
resolve_blocker Resolves every active blocker on the resolved task — the task comes off blocked once they’re all cleared.
report_merge_outcome Records that the resolved task’s linked change was merged, closed, or is still open.

A complete example

Say you want a monitoring alert to comment on the task it’s about. First, create the endpoint:

ch inbound-endpoint create --name "Monitoring alerts"

Copy the printed receive URL and secret. Then add a rule that matches an alert payload shaped like {"taskId": "...", "message": "..."}, sent with a header identifying the sender:

ch inbound-endpoint rules create <endpointRef> --rule '{
  "name": "Comment from monitoring alert",
  "conditions": [
    { "source": "header", "path": "X-Source", "op": "equals", "value": "my-monitor" }
  ],
  "resolver": "task_id",
  "resolverArgs": { "task_id_path": "taskId" },
  "action": "comment",
  "actionArgs": { "body_path": "message" },
  "enabled": true
}'

Before pointing your real sender at it, try a sample payload with a dry run. It evaluates every rule and reports what would happen, without posting anything:

ch inbound-endpoint test <endpointRef> --body '{"taskId": "<a real task id>", "message": "disk usage above 90%"}' --header 'X-Source: my-monitor'

The output shows which rule matched, whether the task resolved, and what action it would have taken. Once that looks right, point your monitoring tool at the receive URL with the same headers and body shape, signed as described above. The endpoint is enabled from the moment you created it, so the very next real request goes live.

Check ch inbound-endpoint deliveries <endpointRef> afterward to see it recorded.

Everything in this example is also on the endpoint’s detail page in the web app — a rule editor, a dry-run panel, and a Deliveries view — if you’d rather not leave the browser.

Receiving webhooks from GitHub

Here’s the same idea, end to end, with a GitHub repository as the sender. First, create a github-v1 endpoint:

ch inbound-endpoint create --name "GitHub" --signature-scheme github-v1

Copy the printed receive URL and secret. Then, in the repository’s GitHub settings:

  1. Open Settings → Webhooks → Add webhook.
  2. Paste the receive URL into Payload URL.
  3. Set Content type to application/json. GitHub defaults this field to form-encoded, and CodeHerder needs JSON — see Delivery outcomes below for what happens if you leave it as-is.
  4. Paste the signing secret into Secret.
  5. Choose which events to send — for example, Pull requests.
  6. Click Add webhook.

Now add a rule. This one records a merged pull request:

{
  "name": "Record a merged pull request",
  "conditions": [
    { "source": "header", "path": "X-GitHub-Event", "op": "equals", "value": "pull_request" },
    { "source": "body", "path": "action", "op": "equals", "value": "closed" },
    { "source": "body", "path": "pull_request.merged", "op": "equals", "value": "true" }
  ],
  "resolver": "merge_url",
  "resolverArgs": { "url_path": "pull_request.html_url" },
  "action": "report_merge_outcome",
  "actionArgs": { "outcome": "merged" },
  "enabled": true
}

GitHub sends action: closed both for a pull request that merged and one that was closed unmerged, so the third condition reads pull_request.merged to tell them apart — a JSON true in the payload compares equal to the string "true".

As in A complete example above, dry-run the rule with ch inbound-endpoint test against a sample payload before pointing GitHub at the endpoint for real.

Delivery outcomes

Once a request passes signature verification and isn’t turned away by the rate limit, it’s recorded with one of these outcomes:

Outcome Meaning
accepted A rule matched, its task resolved, and the action ran.
no_rule_matched No rule’s conditions matched the request.
subject_unresolved A rule matched, but its resolver couldn’t find a task. A benign no-op, not something to retry.
subject_ambiguous A rule matched, but its resolver found more than one possible task. CodeHerder refuses to guess, so nothing runs.
rejected CodeHerder refused to run the action: the endpoint is disabled, the request body isn’t valid JSON, or the delivery failed a permission check — see Who it acts as below.
action_failed A rule matched and its task resolved, but the action failed to run.
dry_run A test call, from ch inbound-endpoint test or the app’s dry-run panel. Rules were evaluated and the result recorded, but nothing actually ran.

A request with a bad signature, an unrecognized endpoint id, or one shed by the rate limit is never recorded at all — CodeHerder answers those with an error response and stops there, on purpose, so an unsigned or forged request costs nothing. On a codeherder-v1 endpoint, a stale timestamp is refused the same way. If your sender’s requests aren’t showing up here, check its signature (and, on a codeherder-v1 endpoint, its timestamp) before anything else: that’s the most common cause, and it’s exactly the one that leaves no trace to debug from.

On a github-v1 endpoint specifically:

  • Nothing recorded at all usually means a bad signature or a missing X-GitHub-Delivery header.
  • A recorded rejected delivery usually means the body wasn’t JSON — check that the webhook’s content type is set to application/json, not GitHub’s default. See Receiving webhooks from GitHub above.

A brand-new endpoint with no rules yet records no_rule_matched for everything it receives. That’s expected; add a rule (or a catch-all) once you’re ready.

Who it acts as

Every accepted delivery acts as one workspace member: the endpoint’s acts-as member, defaulting to whoever created it. That member needs a membership in the workspace the action targets, and the target task has to live inside the endpoint’s own workspace or one of the workspaces nested beneath it. If the acts-as member is unset, or was later removed from the workspace, every action is denied until you set a new one. Any of these three denials shows up in delivery history as rejected.

Set it at creation with --acts-as <member>, or change it later:

ch inbound-endpoint edit <ref> --acts-as <member>

Limits

  • Up to 25 rules per endpoint, evaluated in order.
  • Up to 10 conditions per rule (all must match).
  • A body path is at most 8 segments and 128 bytes long.
  • Up to 60 requests per minute per endpoint. Going over that gets a 429 response with Retry-After: 60. CodeHerder doesn’t record a delivery for a request it sheds this way.

Managing inbound endpoints from the CLI

  • ch inbound-endpoint list — list the endpoints in your workspace.
  • ch inbound-endpoint show <ref> — show one endpoint, including its receive URL and acts-as member.
  • ch inbound-endpoint edit <ref> [--name ...] [--description ...] [--acts-as ...] — update an endpoint.
  • ch inbound-endpoint enable <ref> / ch inbound-endpoint disable <ref> — resume or pause an endpoint without deleting it.
  • ch inbound-endpoint delete <ref> — delete an endpoint (alias: rm).
  • ch inbound-endpoint rotate <ref> — issue a new signing secret, invalidating the old one. Shown once, just like at creation.
  • ch inbound-endpoint deliveries <ref> [--limit N] — recent deliveries, the CLI equivalent of the Deliveries view.
  • ch inbound-endpoint test <ref> --body <json>|--body-file <path> [--header 'Name: Value']... — dry-run a sample payload.
  • ch inbound-endpoint rules list <ref> — list an endpoint’s rules in evaluation order.
  • ch inbound-endpoint rules create <ref> --rule <json>|--rule-file <path> — add a rule. Takes the full rule document at once, not separate flags per field. See A complete example above.
  • ch inbound-endpoint rules edit <ref> <ruleId> --rule <json>|--rule-file <path> — update a rule. A partial document works as a sparse patch, so --rule '{"enabled": false}' turns one rule off without touching the rest of it.
  • ch inbound-endpoint rules delete <ref> <ruleId> — remove a rule (alias: rm).

--rule accepts inline JSON, --rule-file <path> to read from a file, or --rule-file - (or a bare --rule -) to read from standard input — the same convention as everywhere else in the CLI that takes a freeform body of text.

Related reading: Webhooks covers the outbound direction and the shared signing scheme in full. Blockers and blocked tasks and Approvals & staying in control cover what a resolve_blocker or create_task action actually changes on the task it targets.

CodeHerder

Round up your herd.

Bring every human and every agent onto one table. Watch the work move. Costs update as it happens.

Try "pricing", "connect a device", or "who reviews the code"

↑↓ move · ↵ open · esc close