API

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"}'
FieldUse
actionTypeRequired: deploy or job.
nameDisplay name; defaults to Webhook.
authModetoken (default), hmac, or none; none is not allowed for jobs.
actionConfig.serviceId / serviceIdsRestrict a deploy action to services within this project.
actionConfig.jobKeyRequired 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/:provider

There 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:

HeaderPurpose
X-Hub-Signature-256HMAC-SHA256 of the raw body (sha256=…). Required — a missing or wrong value is rejected.
X-GitHub-EventEvent type (push, installation, check_run, ping, …). Selects the handler.
X-GitHub-DeliveryUnique delivery id. Used for idempotency — a redelivered id is claimed once and dropped on repeat.

The dispatcher:

  1. Looks up the provider named in the path. Unregistered → 404 { "error": "Webhook provider '…' is not configured" }.
  2. Verifies the signature. Invalid or missing → 401.
  3. Parses the JSON body. Malformed → 400.
  4. Runs the handler and always responds 200 for a verified delivery — even if the handler throws, the error is returned in the body ({ "success": false, "error": "…" }) rather than as a 4xx/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 ref must start with refs/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-Delivery id 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

OperationSDKREST API
List the project’s incoming webhooks.webhooks.list(id)GET /api/projects/:id/incoming-webhooks
project:read
Create an incoming hook with an authorized action and authentication mode.webhooks.create(id, input)POST /api/projects/:id/incoming-webhooks
project:write
List project webhook deliveries.webhooks.deliveries(id, input?)GET /api/projects/:id/webhook-deliveries
project:read
Update a hook and reauthorize its action when requested.webhooks.update(parentId, id, input)PATCH /api/projects/:id/incoming-webhooks/:hookId
project:write
Rotate hook credentials and reauthorize execution.webhooks.rotate(parentId, id)POST /api/projects/:id/incoming-webhooks/:hookId/rotate
project:write
Remove an incoming hook.webhooks.remove(parentId, id)DELETE /api/projects/:id/incoming-webhooks/:hookId
project:write
Read one hook’s delivery history.webhooks.hookDeliveries(parentId, id, input?)GET /api/projects/:id/incoming-webhooks/:hookId/deliveries
project:read
Invoke the hook’s saved action through the SDK.webhooks.invoke(parentId, id)POST /api/projects/:id/incoming-webhooks/:hookId/invoke
project:write
Read webhook deliveries visible in the organization.webhooks.listDeliveries(input?)GET /api/settings/webhook-deliveries
settings:read

HTTP endpoints

OperationSDKREST API
Receive a provider webhook (github or stripe), verify its signature, and dispatch it.HTTP onlyPOST /api/webhooks/:provider
Handler authentication
Trigger a backup from an inbound webhook (bearer token is the credential).HTTP onlyPOST /api/webhooks/backup
Handler authentication
Authenticate an incoming hook and dispatch its saved action.HTTP onlyPOST /api/webhooks/incoming/:id
Handler authentication

On this page