202 after authentication and admission checks.
This endpoint uses webhook-secret authentication only — not session or CLI token auth. Provide exactly one of
X-Signature (preferred) or X-Webhook-Secret (deprecated).Manage incoming endpoints in Integrations
Open Integrations → Incoming webhooks to see routine and agent endpoints in one list. The explorer filters targets by Routines, Agents, or Pages; search, collapse, and the mobile drawer work like the other Integrations tabs. Selecting a target opens its endpoints and recent receipts. The selection is linkable, for example/integrations?tab=incoming§ion=routine&target=review-pr.
Use Add integration → Incoming webhook (or Add endpoint in a target)
to select an existing target and create its endpoint. The default routine endpoint accepts signed JSON requests; credentials are generated
automatically. To connect GitHub pull requests, open Advanced settings → Webhook
format and choose GitHub pull requests. This uses GitHub’s signature and PR
payload adapter. Collapsing the settings does not change the selected format.
Routines can create, update, or comment on Issues, so Issues do not have a
second, identical target list. Agent webhooks start background runs; they do
not append a message to an existing chat. Page endpoints target a compatible
webhook or script panel on that Page.
Outgoing notifications send Crewship events to another service. Incoming
webhooks receive direct HTTP requests. Tools → Triggers manages
subscriptions through connected app accounts; it is a separate integration
mechanism.
The overview counts routine and configured agent endpoints. Page endpoints are
loaded only when opening a Page and are explicitly excluded from these totals.
Receiving means enabled with credentials, not that delivery was probed.
Received · 24h displays — because the API does not provide that aggregate.
Refresh reloads target lists, endpoints, and the open receipt list.
Receiving URLs for routines and Pages contain a secret token: copy the URL
when it is created; subsequent views show a masked path. The signing secret
is also shown once. Rotating a routine’s signing secret leaves the receiving
URL unchanged and cannot recover a lost URL; create a replacement endpoint
if necessary. An agent’s URL contains only public identifiers and remains
copyable; its signing secret is still shown once.
Routine endpoints support enable/disable, secret rotation, and deletion.
Agent endpoints currently support secret rotation, without a separate
endpoint disable/delete operation. Page endpoints can be revoked with
Delete; rotation requires creating a replacement. Existing work and
receipts are retained when an endpoint is deleted. Recent agent receipts show
admission decisions, not execution results; routine receipts show the run
record’s status when available. Pages expose last-fire information, but no
receipt-history API. All mutations remain subject to backend permissions.
Trigger Agent via Webhook
Authentication
The endpoint accepts two header-based schemes. Provide one of them; if both are absent the request is rejected with401.
X-Signature (preferred). A hex-encoded HMAC-SHA256 of the raw request body, keyed by the agent’s per-agent webhook secret (no sha256= prefix):
X-Timestamp header (unix
seconds). When present, the signature must cover "{timestamp}.{body}" and the
timestamp must be within 5 minutes of the server clock — a captured signed
request replayed after that window is rejected with 400, and the timestamp
itself can’t be swapped because it is part of the signed material:
X-Timestamp keep the body-only scheme above (backward
compatible). When a signature is present it is also used as the dedup identity
(instead of any client-supplied Idempotency-Key), so a replay can’t force a
second run by supplying a fresh key.
Enforcing timestamps per agent. The timestamped scheme is optional by
default so un-migrated senders don’t break — but a body-only signature stays
replayable indefinitely (bounded only by the dedup window). Once an agent’s
sender emits X-Timestamp, flip the agent to require it:
PATCH /api/v1/agents/{id} with {"webhook_require_timestamp": true};
the current value is echoed on GET /api/v1/agents/{id} and in
crewship agent get.) With it on, a body-only HMAC and the deprecated
plaintext X-Webhook-Secret are both rejected with 400 pointing at the
{timestamp}.{body} scheme — closing the replay window entirely for that
agent.
X-Webhook-Secret (deprecated). The per-agent secret sent in plaintext:
Deprecation and Sunset: Thu, 31 Dec 2026 23:59:59 GMT response headers.
Both schemes validate using constant-time comparison. This endpoint does not use session or CLI token authentication.
Path Parameters
Request Headers
Request Body
The payload is passed to the agent as context. The structure is flexible — include whatever data is relevant to the agent’s task.Response
The webhook returns as soon as the delivery is durably recorded. Agent execution happens afterwards, asynchronously, and is tracked as a work item.202 Accepted — the delivery and the work item it produced were committed
in one transaction before the response was written:
status is the work item’s state at acceptance (queued; see
States for what it becomes). A
redelivery of an accepted delivery — same source id, same body — answers
202 again with the original delivery_id and work_id and
duplicate: true; no second run starts. This also holds while the agent’s
rate limit is exhausted: a duplicate is never refused for fullness, only new
work is.
200 OK — a ping event ({"event":"ping",…}) is recorded and
acknowledged without starting an agent:
What Happens Behind the Scenes
Webhook execution pipeline
Webhook execution pipeline
- Auth validation — the agent’s webhook secret is read crew-scoped from the database (the plaintext secret never crosses an IPC hop); an
X-Signatureis verified as an HMAC of the body (or of"{timestamp}.{body}"whenX-Timestampis present, which must also be within the freshness window), otherwise a plaintextX-Webhook-Secretis compared (constant-time) - Delivery identity — the dedup identity is the verified signature (scoped by agent), else the sender’s
Idempotency-Key, else a hash of the payload; the body’s SHA-256 is recorded next to it so a reused identity with different bytes is a409, not a silent overwrite - Durable acceptance — inside one transaction the delivery row and a
queuedwork item (source: webhook,class: background) are written to the ledger; the202carries their ids. Rate and capacity gates run before this step and answer429, a ledger fault answers503— nothing is started that was not recorded first - Response, then dispatch — the receipt is flushed to the sender before anything outside the database runs, so a cold crew’s image pull no longer counts against GitHub’s ten-second delivery deadline
- Container startup — the crew’s container runtime is ensured running by the work dispatcher, which claims the item and records an attempt
- Ingress trust fence — the payload (
event,source,data) is external, attacker-controlled input, so it is wrapped in a nonce-delimited<untrusted …>block and injection-scanned before it becomes the agent’s user message. The agent’s system prompt instructs it to treat fenced blocks as pure data, never as instructions. See Ingress trust fence. - Agent execution — the agent runs under a webhook-scoped session (
webhook-{agentId}-{runId}), with log streaming to the log collector and WebSocket broadcast onworkspace:{workspaceId} - Completion and recovery — the work item moves to a terminal state (
succeeded,failed,cancelled, …); a crash between acceptance and completion leaves a claimable item, not a lost event. Inspect withcrewship work get <work_id>/crewship work deliveries get <delivery_id>, replay withcrewship work replay
Prerequisites
The webhook endpoint is only registered when all of the following are configured:- Orchestrator is available
- Container provider (Keeper) is available
- Log writer is configured
- Internal token is set
If any prerequisite is missing the route is never registered, so the endpoint returns
404.Use Cases
Monitoring Webhook Runs
After triggering, use these endpoints to monitor execution:Getting the Webhook Secret
The webhook signing secret follows show-once semantics — the same convention as CLI tokens and notification-channel secrets. No API, CLI, or UI surface ever returns a stored secret back. To obtain one, rotate:webhook_secret exactly once — store it in the external system (GitHub/Grafana/… webhook config) immediately. Deliveries signed with the previous secret are rejected as soon as the rotation lands. Rotation requires agent-edit rights (OWNER/ADMIN, or MANAGER for agents they created).
GET /api/v1/agents/{agentId} and the GET /api/v1/agents list expose only a webhook_secret_set boolean so clients can tell whether a secret is configured; the plaintext is never returned after the reveal.
The former internal endpoint
GET /api/v1/internal/agents/{agentId}/webhook-secret was removed — the trigger endpoint validates signatures against the database directly, and the plaintext secret never crosses an IPC hop.Pipeline webhooks
Pipeline-webhook administration is workspace-authenticated. Dispatch is public: the high-entropy token in the URL plus its HMAC signature are the credentials.
Create accepts either
target_pipeline_slug or target_pipeline_id:
name defaults to the target slug. If signing_secret is omitted, the server
generates one. The 201 response includes signing_secret exactly once;
list responses include signing_secret_set, not the secret. The response also
contains token, target identifiers/version, inputs_template, enabled,
rate-limit and last-fire telemetry. A zero or negative configured rate uses the
server floor of 600 fires/minute. Creation requires encrypted credential storage;
misconfiguration is 500. Invalid target/body is 400, and insufficient role
is 403.
Pipeline dispatch
X-Crewship-Timestamp: <unix-seconds>, sign
<timestamp>.<raw-body>; the timestamp must be fresh. The body may be any JSON
(or raw bytes): the routine receives request-derived event, raw, and
headers inputs. inputs_template adds literal values; it does not evaluate
templates or merge payload fields into top-level inputs, and it cannot override
those three fields. Declared form inputs are validated before accepting a new
delivery: invalid inputs return 400 without reserving a delivery ID or run ID.
A malformed target definition returns 409. These signed, rate-limited
rejections are recorded as failed deliveries and increment the webhook’s failure
streak; repeated failures can alert managers, and a successful delivery resets
the streak. Delivery health includes sender configuration/data errors, not only
server execution errors.
Idempotency-Key (or X-Crewship-Event-ID) can select the
deduplication identity; identical signed deliveries also deduplicate.
Success returns 202:
delivery_id is the receipt written before the
response. A duplicate returns 202 with status: "DEDUPED", the original
run_id and delivery_id, deduped: true and duplicate: true. The run may
later be COMPLETED, FAILED, CANCELLED, or WAITING; poll
/api/v1/workspaces/{workspaceId}/pipeline-runs/{runId}.
Dispatch does not accept session/JWT authentication, and it never falls back to
an unsigned request. From the CLI,
crewship routine webhooks fire <url> --secret … --body … signs and sends a delivery exactly this way (see
webhooks fire).
Routine webhook receipts
A routine delivery is executed by the pipeline engine and never produces a work item. What it leaves behind is a receipt: its identity, the body’s fingerprint and the run it produced. Two read routes answer “did you receive this one, and what ran” without re-sending the delivery.
Receipt identity is
(workspace, webhook, source delivery id) — the
Idempotency-Key / X-Crewship-Event-ID the sender supplied, or the synthetic
key derived from the signed bytes. Passing ?webhook_id= and
?source_delivery_id= asks that identity question and answers with a page of
zero or one; ?source_delivery_id= on its own is a 400.
run_id is the real pipeline run (poll
/api/v1/workspaces/{workspaceId}/pipeline-runs/{runId}); run_status is that
run’s current status and is null when no run record is available. The
receipt is written and the 202 sent before execution starts, so null can
mean the run has not started yet just as well as a dispatch that failed before
a record was written, or a record removed by run retention — the absence does
not say which, and neither does the API. The raw payload is never returned;
body_sha256 and body_bytes identify it.
Dedup window and retention. A receipt deduplicates redeliveries of its
identifier for 30 days from acceptance (dedup_expires_at). A sweeper removes
expired receipts at startup and then daily, so downtime cannot extend the
window; from then on the same identifier is new work and
runs again — the contract promises no replay suppression past the window.
Receipts recorded before the window existed were dated from the upgrade and
keep the full 30 days from there.
POST /api/v1/webhooks/{token}/github-pull-request
crewship routine webhooks create --slug <routine> --ingress-profile github.
The creation API accepts ingress_profile: "github"; the default is crewship.
The profile cannot be changed by PATCH: create a new endpoint when changing
sender protocols. Copy the URL and signing secret at creation; neither can be
recovered from the list. Rotating the secret preserves the URL you saved.
Authentication uses the token in the path and X-Hub-Signature-256, containing
sha256= plus the HMAC-SHA256 of the exact request body. It does not use a user
session. Configure GitHub with content type application/json, the shown secret
and Pull requests events. The existing hostname and HTTPS ingress suffice;
a separate webhook DNS name is not required. The base URL without the suffix
also enforces the stored GitHub profile, so a legacy signature cannot bypass it.
The JSON request can contain the full GitHub payload. Execution requires a
pull_request object and action equal to opened, synchronize or reopened:
X-GitHub-Delivery is required for these actions. X-GitHub-Event: ping and
unsupported actions are acknowledged without starting a routine. The target
routine is fixed by the endpoint configuration, and the action is read from
signed JSON rather than chosen by the unsigned event header. The routine receives
the event as its webhook inputs and must implement the intended issue or review
steps; choosing a routine does not add those steps automatically.
Response statuses:
GitHub delivery IDs are unsigned. Within this endpoint, identical signed bytes
therefore also deduplicate atomically, including concurrent requests with different
IDs. The receipt records profile
github and source ID github:<delivery-id>.
The same 30-day receipt retention applies; once expired receipts are removed, the
same signed payload can start new work. No permanent replay protection is promised.
This endpoint does not publish PR reviews or enforce a repository/branch allowlist.
Connect a dedicated review routine only after configuring those decisions there.
To exercise the endpoint before pointing GitHub at it,
crewship routine webhooks fire <url>/github-pull-request --secret … --body @pull_request.json --delivery-id <id>
signs the payload with X-Hub-Signature-256 and prints the receipt; crewship routine webhooks receipts list --webhook <id> --source-id github:<id> reads it
back.