Skip to content

Webhooks

A webhook is an HTTP callback. When a Git ref moves, Amendable sends an HTTP POST with JSON to an HTTPS URL you run. You do not poll the API. Amendable tells you. This is the same idea as GitHub webhooks: a signed POST on ref changes, then your server clones and does the work.

Hooks here are account-level. One URL receives events from every repository you own. You do not register a hook per repo. Amendable stores Git and does not run your jobs. There is no hosted GitHub Actions or any other CI. The webhook is how a runner, a build job, or a product you ship finds out that a push happened.

You need a webhook when some other process must react to a Git change and that process is not the one that ran git push.

Skip it if you only clone and push from a laptop. Skip it if an agent pushes and then keeps working in the same session. In those cases the pusher already knows the ref moved.

Register one when a separate system should start work:

  • CI on your runners. On push, clone repository.clone_url at after and run tests. Point GitHub Actions or any other runner at a receiver you host, or have that receiver enqueue the job.
  • Build artifacts from Git. On create of a tag, or push to main, clone the tree and produce whatever you ship: packages, images, previews, exports. MakerRepo is one example (CAD manufacturing).
  • A Git-backed product. On push, refresh derived data and UI. BeanHub is one example. More on that: Build a Git product.
  • Handoff between agents. Agent A pushes to a scratch repo. The webhook starts agent B or a queue job so B does not poll.

Create the webhook once at account setup, not per repository. It already fans out.

  1. A Git ref moves (push, create, or delete), or you send a ping.
  2. Amendable POSTs JSON to each active HTTPS URL on your account.
  3. Your endpoint verifies X-Amendable-Signature (HMAC-SHA256 of the raw body) and replies 2xx quickly.
  4. You clone and do the job asynchronously. Timeouts and failed deliveries are under Delivery behavior.
Event When
push A branch or tag tip moved, including the first commit on a new ref
create A branch or tag was created (before is 40 zeros)
delete A branch or tag was deleted (after is 40 zeros)
ping Synthetic. Use Ping or POST /v1/webhooks/{id}/ping
repository Reserved. Do not rely on it yet

Creating a branch emits create and push.

The signing secret is generated by Amendable. You cannot set it. It is returned once on create.

Settings → Webhooks. Create, ping, and inspect deliveries there. Open a webhook to see Recent deliveries (status, HTTP code, attempt count, error).

Every delivery includes:

Header Value
Content-Type application/json
X-Amendable-Event push, create, delete, or ping
X-Amendable-Event-Id Event UUID
X-Amendable-Delivery Delivery UUID
X-Amendable-Signature Hex HMAC-SHA256 of the raw body using the webhook secret
import hashlib
import hmac
def valid(secret: str, body: bytes, signature: str) -> bool:
expected = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)

Reply 2xx after a valid signature. A non-2xx status, including 401 on a bad HMAC, is retried.

A local HTTP listener can use this check while you develop. The delivery URL you register must be https. Production rejects http, localhost, and private IPs.

Enough to clone and check out after:

{
"id": "event-uuid",
"type": "push",
"ref": "refs/heads/main",
"ref_name": "main",
"ref_type": "branch",
"before": "0000000000000000000000000000000000000000",
"after": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"created": true,
"deleted": false,
"commits": [],
"head_commit": { "id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" },
"tag": null,
"repository": {
"id": "repo-uuid",
"name": "my-repo",
"full_name": "demo/my-repo",
"default_branch": "main",
"html_url": "https://amendable.io/r/demo/my-repo",
"clone_url": "https://amendable.io/r/demo/my-repo.git",
"owner": { "id": "user-uuid", "username": "demo" }
}
}

Full field list: Webhook payloads.

Each delivery is tried up to 8 times. Each attempt waits 30 seconds for connect plus response. HTTP 2xx is success. Amendable then stops.

These failures retry. Waits between attempts are 8s, 16s, 32s, 64s, 128s, 256s, then 512s (about 17 minutes of backoff if every attempt fails):

  • Timeout or other transport error
  • DNS lookup failure
  • Any HTTP status that is not 2xx (including 3xx, 4xx, and 5xx)

After the 8th failed attempt the delivery is FAILED. It does not retry again. A later git event creates a new delivery.

These fail on the first attempt, with no retry:

  • The webhook is inactive or deleted
  • The URL is blocked as unsafe (private, loopback, or Amendable’s own hosts)
  • An unexpected worker error

Return 2xx quickly and do the clone or job asynchronously. Redirects are not followed.

Settings → Webhooks lists each endpoint and its last delivery status.

Amendable Settings, Webhooks list. One active endpoint https://ci.example.com/hooks/amendable with last delivery FAILED.

Open a webhook. Recent deliveries shows when, event, status (SUCCESS, FAILED, or PENDING), HTTP status, attempt count, and error. Details has the error text, the response body, and the JSON payload. Use Ping on that page to test without a git push.

Webhook details. Recent deliveries table with a FAILED push after 8 attempts (HTTP 500), a successful ping (HTTP 204), and a PENDING push with ConnectTimeout.

The same rows are on GET /v1/webhooks/{id}/deliveries (needs API_WEBHOOKS or ALL). The CLI prints them with amendable webhook deliveries.

Ping even if ping is not in events. The ping endpoint force-delivers to that webhook only.