Inbound webhooks
An inbound webhook lets an external system push an event into a team — the canonical case being a CI failure that opens a blocker for whoever caused it. Endpoints are authenticated by an HMAC signature, not a bearer token.
Create an endpoint
A team admin creates an endpoint through the API. It returns the delivery url and a
secret (the secret is shown once):
curl -X POST "https://stndp.io/api/v1/webhooks?team=TEAM" \
-H "Authorization: Bearer <your-admin-token>" \
-H "Content-Type: application/json" \
-d '{
"provider": "ci",
"rules": [
{"on": "ci_failure", "action": "block", "text": "CI failed on {branch}", "severity": "high"}
]
}'
# → { "id": 1, "token": "…", "secret": "…", "url": "https://stndp.io/api/hooks/<token>" }
Sign & send a delivery
POST the JSON event to the endpoint url. Sign the raw request body
with HMAC-SHA256 using the secret, and send it in the
X-Stndp-Signature header as sha256=<hex>. Include a unique
X-Stndp-Delivery id so retries are de-duplicated:
BODY='{"event":"ci_failure","actor_email":"dev@acme.com","branch":"main"}'
SIG="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')"
curl -X POST "$URL" \
-H "Content-Type: application/json" \
-H "X-Stndp-Signature: $SIG" \
-H "X-Stndp-Delivery: $(uuidgen)" \
-d "$BODY"
A bad signature is rejected with 401. A delivery whose id (or whose byte-identical
body) was already seen returns {"status": "duplicate"} and does nothing — deliveries
are idempotent, so retries are safe.
Rules
rules is a list of rule objects (a bare object is treated as a one-rule list). Each
rule:
{
"on": "ci_failure", # optional: fire only when the event type matches.
# The event type is payload["event"] or payload["event_type"].
# Omit to match any event.
"action": "block", # the canonical action: open a blocked entry.
"text": "CI failed on {branch}", # optional template; {field} is filled from the
# payload's scalar fields. Default: "CI/build issue".
"severity": "high" # optional: low | normal | high | critical (default high)
}
Who gets blocked. The event's actor is resolved to a stndp user — first by
actor/actor_id through the provider's identity mapping (e.g. a connected
GitHub login), then by actor_email/
email. If the actor can't be resolved yet, the event is held and retried once the
mapping exists, so nothing is lost. The opened block is deduped per delivery, so re-processing
never double-opens it.
Slack uses its own signed receivers rather than this endpoint — see Slack.