Webhooks
Outgoing webhooks let you receive real-time HTTP notifications when events happen in your oec.sh environments — deployments complete, environments are created, statuses change. Instead of polling, your server receives a push.
Base URL: https://api.oec.sh/api/public/v1
Available Events
Deployment Events
| Event | When it fires |
|---|---|
deploy.started | A deployment task begins executing. |
deploy.completed | A deployment finishes successfully. |
deploy.failed | A deployment finishes with an error. |
deploy.cancelled | A deployment is cancelled before completion. |
Environment Events
| Event | When it fires |
|---|---|
environment.created | A new environment is provisioned. |
environment.deleted | An environment is deleted. |
environment.status_changed | An environment's status transitions (e.g. stopped → running). |
Automation Rule Events
| Event | When it fires |
|---|---|
automation_rule.triggered | An automation rule matched its trigger condition and began executing. |
automation_rule.completed | An automation rule ran all its actions successfully. |
automation_rule.failed | An automation rule failed during execution (e.g. deploy error, action timeout). |
You can subscribe to up to 10 events per webhook.
Setup
List Webhooks
GET /webhooksReturns all webhooks for your organisation (or project, if using a project-scoped key).
Query Parameters
| Parameter | Type | Description |
|---|---|---|
limit | integer | Results per page. Min 1, max 100. Default 50. |
offset | integer | Skip this many results. Default 0. |
Response — 200 OK
{
"items": [
{
"id": "wh-uuid",
"url": "https://hooks.example.com/oec-events",
"events": ["deploy.completed", "deploy.failed"],
"description": "CI/CD notifications",
"project_id": null,
"is_active": true,
"last_triggered_at": "2026-03-01T14:22:00Z",
"failure_count": 0,
"created_at": "2026-01-15T09:00:00Z"
}
],
"total": 1,
"limit": 50,
"offset": 0,
"has_more": false
}Create Webhook
POST /webhooksRegisters a new outgoing webhook. Requires a full_access API key.
Secret shown once. The response includes a secret field (format: whsec_...) for HMAC signature verification. This value is shown only at creation time. Store it securely. If lost, rotate it with POST /webhooks/{id}/rotate-secret.
This endpoint supports idempotency via the X-Idempotency-Key header. Duplicate requests within 24 hours return the original response.
Limits
- Max 100 webhooks per organisation
- Webhook mutation rate limit: 10 create/update/delete per minute per API key
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS endpoint to deliver events to. Max 2048 characters. Must use HTTPS. Private IP literals are rejected (SSRF protection). |
events | array | Yes | List of event names to subscribe to. See Available Events. Max 10 events. |
description | string | No | Optional note for your reference. Max 500 characters. |
project_id | UUID | No | Scope the webhook to a single project. Omit for org-wide events. |
is_active | boolean | No | Whether to deliver events immediately. Defaults to true. |
format | string | No | Payload format. One of raw (default), slack, teams, or discord. See Payload Formats. |
Response — 201 Created
{
"id": "wh-uuid",
"url": "https://hooks.example.com/oec-events",
"events": ["deploy.completed", "deploy.failed"],
"description": "CI/CD notifications",
"project_id": null,
"is_active": true,
"last_triggered_at": null,
"failure_count": 0,
"created_at": "2026-03-13T10:00:00Z",
"secret": "whsec_abc123..."
}Example
curl -X POST "https://api.oec.sh/api/public/v1/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: $(uuidgen)" \
-d '{
"url": "https://hooks.example.com/oec-events",
"events": ["deploy.completed", "deploy.failed", "environment.created"],
"description": "CI/CD pipeline notifications"
}'Get Webhook
GET /webhooks/{webhook_id}Returns a single webhook by ID.
Update Webhook
PATCH /webhooks/{webhook_id}Updates a webhook's URL, events, description, or active state. Requires full_access key. All fields are optional.
Request Body
| Field | Type | Description |
|---|---|---|
url | string | New HTTPS endpoint URL. |
events | array | New event list (replaces existing). Max 10. |
description | string | New description. |
is_active | boolean | Set to true to re-enable a paused webhook. |
format | string | Change the payload format: raw, slack, teams, or discord. |
Example — Re-enable a paused webhook
curl -X PATCH "https://api.oec.sh/api/public/v1/webhooks/YOUR_WEBHOOK_ID" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"is_active": true}'Delete Webhook
DELETE /webhooks/{webhook_id}Permanently deletes the webhook and all its delivery history. This cannot be undone.
Requires full_access key. Returns 204 No Content.
curl -X DELETE "https://api.oec.sh/api/public/v1/webhooks/YOUR_WEBHOOK_ID" \
-H "Authorization: Bearer YOUR_API_KEY"Test a Webhook
POST /webhooks/{webhook_id}/testSends a synchronous test ping event to the webhook URL and returns the HTTP response. Does not create a delivery log entry. Requires full_access key.
curl -X POST "https://api.oec.sh/api/public/v1/webhooks/YOUR_WEBHOOK_ID/test" \
-H "Authorization: Bearer YOUR_API_KEY"{
"success": true,
"status_code": 200,
"duration_ms": 142,
"message": null
}Rotate Signing Secret
POST /webhooks/{webhook_id}/rotate-secretGenerates a new HMAC signing secret. The old secret is immediately invalidated. The new plaintext secret is shown once in the response — store it immediately.
curl -X POST "https://api.oec.sh/api/public/v1/webhooks/YOUR_WEBHOOK_ID/rotate-secret" \
-H "Authorization: Bearer YOUR_API_KEY"{
"secret": "whsec_new_secret_here..."
}Payload Formats
oec.sh can deliver event payloads in four formats. Choose the format that matches your destination when creating or updating a webhook.
| Format | Use when... |
|---|---|
raw | You control the receiver — a custom server, CI pipeline, or automation script. Full JSON envelope with HMAC signature. |
slack | Sending directly to a Slack Incoming Webhook URL (Block Kit format, no HMAC header). |
teams | Sending to a Microsoft Teams Incoming Webhook (Adaptive Card format). |
discord | Sending to a Discord webhook URL (Embed format). |
raw (default)
The full event envelope is delivered as JSON with the X-OEC-Signature HMAC header. See Signature Verification. This is the most flexible format — you can parse and forward however you like.
{
"event_id": "evt_01abc123def456",
"event": "automation_rule.completed",
"timestamp": "2026-03-15T09:00:00Z",
"organization_id": "YOUR_ORG_ID",
"data": {
"rule_name": "Deploy on push",
"action_type": "deploy_latest",
"trigger_type": "push",
"branch": "main",
"actor": "jane@example.com"
}
}slack
Delivers a Slack Block Kit (opens in a new tab) message directly to a Slack Incoming Webhook URL. The X-OEC-Signature header is not sent for platform formats — Slack handles its own auth.
To use:
- Create an Incoming Webhook in Slack (your workspace → Apps → Incoming Webhooks)
- Copy the Webhook URL (
https://hooks.slack.com/services/...) - Create a webhook on oec.sh with that URL and
"format": "slack"
The message includes the event name, a color-coded sidebar (green for success, red for failure), and key details from the event data.
teams
Delivers an Adaptive Card (opens in a new tab) to a Microsoft Teams channel via an Incoming Webhook connector. Set up a Teams Incoming Webhook, paste the URL, and set "format": "teams".
discord
Delivers a rich embed to a Discord channel webhook. In Discord, go to channel Settings → Integrations → Webhooks, create one, and copy the URL. Set "format": "discord".
Platform formats (slack, teams, discord) do not include the X-OEC-Signature header — these services use their own authentication. If you need signature verification, use raw format with your own endpoint.
Receiving Webhooks
Payload Format
oec.sh delivers a POST request to your endpoint with a JSON body:
{
"event_id": "evt_01abc123def456",
"event": "deploy.completed",
"timestamp": "2026-03-13T12:05:00Z",
"organization_id": "YOUR_ORG_ID",
"data": {
"environment_id": "YOUR_ENV_ID",
"project_id": "YOUR_PROJECT_ID",
"task_id": "task-uuid",
"status": "completed",
"duration_seconds": 300
}
}Each payload includes a unique event_id. Use this field to deduplicate deliveries in your handler — retries send the same event_id.
Signature Verification
Every delivery includes an X-OEC-Signature header:
X-OEC-Signature: sha256=<hex_digest>The signature is an HMAC-SHA256 of the raw request body, signed with your webhook's secret.
Always verify signatures before processing a webhook payload. This prevents bad actors from sending fake events to your endpoint.
import hashlib
import hmac
from fastapi import Request, HTTPException
WEBHOOK_SECRET = "whsec_your_secret_here"
async def verify_webhook(request: Request) -> bytes:
body = await request.body()
signature_header = request.headers.get("X-OEC-Signature", "")
if not signature_header.startswith("sha256="):
raise HTTPException(status_code=400, detail="Missing signature")
expected_sig = signature_header[len("sha256="):]
computed_sig = hmac.new(
WEBHOOK_SECRET.encode(),
body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(computed_sig, expected_sig):
raise HTTPException(status_code=401, detail="Invalid signature")
return bodyAlways use a timing-safe comparison (e.g. hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js) to avoid timing oracle attacks. Never use == to compare signatures.
Responding to Webhooks
Your endpoint must respond with an HTTP 2xx status code within 10 seconds. Any non-2xx response or a timeout is recorded as a delivery failure.
Reliability
Retry Logic
If your endpoint returns a non-2xx response or times out, oec.sh retries delivery with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 (initial) | Immediate |
| 2 | ~30 seconds |
| 3 | ~5 minutes |
| 4 | ~30 minutes |
| 5 | ~2 hours |
Auto-Pause on Repeated Failures
After 10 consecutive failed deliveries, the webhook is automatically paused (is_active set to false). You will need to:
- Fix the issue with your endpoint
- Re-enable the webhook:
curl -X PATCH "https://api.oec.sh/api/public/v1/webhooks/YOUR_WEBHOOK_ID" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"is_active": true}'
Delivery Log
GET /webhooks/{webhook_id}/deliveriesReturns the 50 most recent delivery attempts for a webhook, newest first.
[
{
"id": "delivery-uuid",
"event": "deploy.completed",
"status": "delivered",
"attempt_count": 1,
"response_status": 200,
"response_body": null,
"error_reason": null,
"duration_ms": 142,
"delivered_at": "2026-03-13T12:05:01Z",
"created_at": "2026-03-13T12:05:00Z"
}
]Delivery Object Fields
| Field | Type | Description |
|---|---|---|
id | UUID | Delivery record ID. |
event | string | Event name. |
status | string | "delivered", "failed", or "retrying". |
attempt_count | integer | How many times delivery was attempted. |
response_status | integer|null | HTTP status code from your endpoint. |
response_body | string|null | Response body snippet (when available). |
error_reason | string|null | Human-readable reason for failure, if applicable. |
duration_ms | integer|null | Round-trip time to your endpoint. |
delivered_at | ISO 8601|null | When the most recent attempt completed. |
created_at | ISO 8601 | When the delivery was first attempted. |
Mutation Rate Limit
Webhook create, update, and delete operations are rate-limited to 10 mutations per 60-second window per API key. Exceeding this returns 429 Too Many Requests.