Webhooks
Create incoming actions and inspect provider deliveries.
Incoming hooks trigger a saved deployment or job action. Provider webhooks receive signed events from services such as GitHub. Both record deliveries, but their authentication protocols differ.
Create an incoming hook
const hook = await ship.webhooks.create("proj_123", {
name: "Deploy site",
actionType: "deploy",
authMode: "token",
});
console.log(hook.id, hook.url);curl -X POST "$OPENSHIP_URL/api/projects/proj_123/incoming-webhooks" \
-H "Authorization: Bearer $OPENSHIP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Deploy site","actionType":"deploy","authMode":"token"}'| Field | Use |
|---|---|
actionType | Required: deploy or job. |
name | Display name; defaults to Webhook. |
authMode | token (default), hmac, or none; none is not allowed for jobs. |
actionConfig.serviceId / serviceIds | Restrict a deploy action to services within this project. |
actionConfig.jobKey | Required for a job action. |
The returned hook includes its URL and, where authorized, a secret. Store that secret securely. Job hooks require project-write, job-write, and administration access to every job target server.
Invoke an authorized action
Use this operation when your integration is already authenticated with Openship:
const result = await ship.webhooks.invoke("proj_123", "hook_123");
console.log(result);curl -X POST "$OPENSHIP_URL/api/projects/proj_123/incoming-webhooks/hook_123/invoke" \
-H "Authorization: Bearer $OPENSHIP_TOKEN"An external sender instead calls the hook's returned URL using its token or HMAC authentication. The dispatch rechecks the saved execution identity, current membership, and any saved credential limits. It does not replace the caller with the organization owner.
Hooks that lack saved authority return requiresReauthorization: true. An authorized editor can rotate
the hook or update it with { enabled: true } to reauthorize it. Authority changes rotate the credential;
update external senders to use the new secret.
Read deliveries
deliveries(projectId) lists a project's events. hookDeliveries(projectId, hookId) selects one hook;
listDeliveries() lists events visible across the selected organization. Pagination filters access
before selecting the page.
Dispatch a provider webhook
POST /api/webhooks/:providerThere is no Openship-defined request body: the body is the provider's raw event payload, read verbatim so the signature can be checked byte-for-byte. GitHub deliveries carry these headers, which the dispatcher relies on:
| Header | Purpose |
|---|---|
X-Hub-Signature-256 | HMAC-SHA256 of the raw body (sha256=…). Required — a missing or wrong value is rejected. |
X-GitHub-Event | Event type (push, installation, check_run, ping, …). Selects the handler. |
X-GitHub-Delivery | Unique delivery id. Used for idempotency — a redelivered id is claimed once and dropped on repeat. |
The dispatcher:
- Looks up the provider named in the path. Unregistered →
404 { "error": "Webhook provider '…' is not configured" }. - Verifies the signature. Invalid or missing →
401. - Parses the JSON body. Malformed →
400. - Runs the handler and always responds
200for a verified delivery — even if the handler throws, the error is returned in the body ({ "success": false, "error": "…" }) rather than as a4xx/5xx. Returning an error status would make GitHub retry and risk duplicate deployments.
# GitHub sends this automatically. Illustrative manual delivery — the signature
# must be a valid HMAC-SHA256 of the exact body using the project's webhook secret:
curl -X POST "$OPENSHIP_URL/api/webhooks/github" \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: ping" \
-H "X-GitHub-Delivery: $(uuidgen)" \
-H "X-Hub-Signature-256: sha256=<hmac-of-body>" \
-d '{"zen":"Keep it simple."}'Signature verification (GitHub)
GitHub deliveries are verified with a per-project secret. The dispatcher peeks at repository.full_name
in the payload, finds the project(s) linked to that owner/repo, and checks the signature against the
project's stored webhookSecret. If no per-project secret resolves (legacy hooks, installation/ping events
that have no owning project), it falls back to the instance's GITHUB_WEBHOOK_SECRET. A delivery with no
resolvable secret cannot be verified and is rejected — there is no unsigned path, even self-hosted.
Provider registration
github is registered on the dispatcher at startup, so POST /api/webhooks/github is always live. stripe
is an accepted :provider name but is not wired to the unified dispatcher by default; Stripe billing
events are received at /api/billing/webhook/stripe in cloud mode instead. Posting to
/api/webhooks/stripe when no Stripe provider is registered returns 404 … is not configured.
How a GitHub push becomes a deploy
When a verified push event arrives, the handler maps it to deployments like this:
- Branch pushes only. The
refmust start withrefs/heads/; tag pushes and branch deletions are acknowledged and ignored. - Match projects by repo + branch. It finds every project linked to the pushed
owner/repo, then keeps those where auto-deploy is enabled and the project's configured branch (or, if unset, the repository's default branch) equals the pushed branch. - Attribute to the org owner. Webhooks have no human actor, so the deployment is audited against the organization owner (who owns the GitHub App installation).
- Smart per-service routing. For monorepo/compose projects, the changed files in the push decide which services rebuild; unaffected pushes can be skipped entirely. Single-app projects always rebuild. A pending "rebuild everything next time" flag forces a full deploy once.
- Idempotency. The
X-GitHub-Deliveryid is claimed persistently, so an at-least-once redelivery of the same push is dropped instead of deploying twice. - Cloud forwarding. On a self-hosted box, if no local project matches but a linked cloud project owns the repo/branch, the push is forwarded to Openship Cloud so the cloud copy redeploys.
Deep dive on wiring and edge cases: Auto-deploy on push.
Errors
401 — invalid or missing signature
The X-Hub-Signature-256 header is absent, or the HMAC doesn't match the stored secret. This is expected
for hand-crafted requests; genuine GitHub/Stripe deliveries are signed. If real deliveries fail, the webhook
secret on the project (or GITHUB_WEBHOOK_SECRET) no longer matches what the provider is signing with —
re-link the repository to reset it.
404 — unknown or unconfigured provider
The :provider isn't github/stripe, or the named provider isn't registered on this instance (e.g.
stripe on a self-hosted box). Check the path and use /api/webhooks/github for GitHub.
400 — invalid JSON body
The signature verified but the body didn't parse as JSON. Confirm the provider is sending
Content-Type: application/json and the payload isn't truncated (the 5 MB body cap applies).
200 with success: false
A verified delivery whose handler threw still returns 200 — on purpose, so GitHub doesn't retry into a
duplicate deploy. The failure detail is in the response body; check your instance logs for the
[Webhook] github handler error entry.
Operations
Resource methods
| Operation | SDK | REST API |
|---|---|---|
| List the project’s incoming webhooks. | webhooks.list(id) | GET /api/projects/:id/incoming-webhooksproject:read |
| Create an incoming hook with an authorized action and authentication mode. | webhooks.create(id, input) | POST /api/projects/:id/incoming-webhooksproject:write |
| List project webhook deliveries. | webhooks.deliveries(id, input?) | GET /api/projects/:id/webhook-deliveriesproject:read |
| Update a hook and reauthorize its action when requested. | webhooks.update(parentId, id, input) | PATCH /api/projects/:id/incoming-webhooks/:hookIdproject:write |
| Rotate hook credentials and reauthorize execution. | webhooks.rotate(parentId, id) | POST /api/projects/:id/incoming-webhooks/:hookId/rotateproject:write |
| Remove an incoming hook. | webhooks.remove(parentId, id) | DELETE /api/projects/:id/incoming-webhooks/:hookIdproject:write |
| Read one hook’s delivery history. | webhooks.hookDeliveries(parentId, id, input?) | GET /api/projects/:id/incoming-webhooks/:hookId/deliveriesproject:read |
| Invoke the hook’s saved action through the SDK. | webhooks.invoke(parentId, id) | POST /api/projects/:id/incoming-webhooks/:hookId/invokeproject:write |
| Read webhook deliveries visible in the organization. | webhooks.listDeliveries(input?) | GET /api/settings/webhook-deliveriessettings:read |
HTTP endpoints
| Operation | SDK | REST API |
|---|---|---|
Receive a provider webhook (github or stripe), verify its signature, and dispatch it. | HTTP only | POST /api/webhooks/:providerHandler authentication |
| Trigger a backup from an inbound webhook (bearer token is the credential). | HTTP only | POST /api/webhooks/backupHandler authentication |
| Authenticate an incoming hook and dispatch its saved action. | HTTP only | POST /api/webhooks/incoming/:idHandler authentication |