Webhooks
Register HTTPS endpoints that receive signed, retried event deliveries (such as workflow_run.completed). Verify the HMAC signature, inspect the delivery log, and replay failed deliveries.
Webhooks push events to a URL you own instead of making you poll. Register an HTTPS endpoint and Pictograph POSTs a signed JSON body to it whenever a subscribed event fires. Deliveries are durable, retried with backoff, and every attempt is recorded in a log you can inspect and replay.
The endpoint must be public HTTPS: private, loopback, and metadata addresses are rejected at registration time. Every management call (create, update, delete, test, replay, rotate) requires an admin or owner API key.
Python and the CLI address an endpoint by its registered URL, matched exactly (an id also works). REST addresses it by the endpoint’s org-unique name, which defaults to the URL’s host, or by its id.
event_types
The event types you can subscribe to. An endpoint with an empty subscription receives all of them, including ones added later.
Source: Webhooks.event_types
print(client.webhooks.event_types())
# ['workflow_run.completed', 'workflow_run.failed']
pictograph webhooks event-types
curl -s "https://api.pictograph.io/api/v1/developer/webhooks/event-types" \
-H "X-API-Key: $PICTOGRAPH_API_KEY"
Returns list[str]
| Event | Fires when |
|---|---|
workflow_run.completed |
A workflow run finishes successfully. The payload carries the run id and signed artifact URLs. |
workflow_run.failed |
A workflow run ends in error. |
create
Register an endpoint. Returns CreatedWebhookEndpoint - an endpoint plus the
signing secret. It is DERIVED, not stored, so it can always be shown again from
Settings (use
rotate_secret to mint a new one).
Source: Webhooks.create
| Arg | Type | Default | Notes |
|---|---|---|---|
url |
str |
required | HTTPS URL that receives the signed POST. |
description |
str | None |
None |
Free-text label shown in the endpoints list. |
event_types |
list[str] | None |
None |
Events to deliver. None subscribes to every event type. |
created = client.webhooks.create(
url="https://example.com/hooks/pictograph",
description="Prod ingestion",
event_types=["workflow_run.completed", "workflow_run.failed"],
)
print(created.endpoint.id)
print(created.secret) # whsec_… also revealable from Settings
pictograph webhooks create https://example.com/hooks/pictograph \
--description "Prod ingestion" \
--event workflow_run.completed --event workflow_run.failed
curl -s -X POST "https://api.pictograph.io/api/v1/developer/webhooks/endpoints" \
-H "X-API-Key: $PICTOGRAPH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/pictograph", "description": "Prod ingestion", "event_types": ["workflow_run.completed", "workflow_run.failed"]}'
Returns CreatedWebhookEndpoint
CreatedWebhookEndpoint · 2 fields
class CreatedWebhookEndpoint(BaseModel):
"""Create / rotate response - carries the one-time signing secret."""
endpoint: WebhookEndpoint
secret: str
Omit event_types to receive every event.
list
Every endpoint in your organization. No secret material is returned, only a
display secret_prefix.
Source: Webhooks.list
for ep in client.webhooks.list():
print(ep.id, ep.url, ep.enabled, ep.event_types)
pictograph webhooks list
curl -s "https://api.pictograph.io/api/v1/developer/webhooks/endpoints" \
-H "X-API-Key: $PICTOGRAPH_API_KEY"
Returns Sequence[WebhookEndpoint]
WebhookEndpoint · 13 fields
class WebhookEndpoint(BaseModel):
"""A registered outbound webhook destination."""
id: str
organization_id: str
url: str
description: str | None = None
event_types: list[str] = []
enabled: bool = True
secret_version: int = 1
secret_prefix: str | None = None
consecutive_failures: int = 0
disabled_reason: str | None = None
auth_header_names: list[str] | None = None
last_delivery_at: datetime | None = None
created_at: datetime | None = None
| Field | Meaning |
|---|---|
id |
Endpoint UUID. |
url |
The HTTPS destination. |
event_types |
Subscribed event types (empty means all). |
enabled |
Whether deliveries are sent. Auto-disabled after repeated failures. |
secret_version |
Current signing-secret version (bumped on rotate). |
secret_prefix |
First characters of the signing secret, for display only. |
consecutive_failures |
Failed deliveries in a row. Resets to 0 on any success. |
disabled_reason |
Set when the endpoint was auto-disabled. |
last_delivery_at |
Timestamp of the most recent delivery attempt. |
get
Fetch one endpoint.
Source: Webhooks.get
| Arg | Type | Default | Notes |
|---|---|---|---|
endpoint |
str |
required | Webhook endpoint name or id. |
ep = client.webhooks.get(
endpoint="https://example.com/hooks/pictograph",
)
print(ep.url, ep.enabled)
pictograph webhooks get https://example.com/hooks/pictograph
curl -s "https://api.pictograph.io/api/v1/developer/webhooks/endpoints/$ENDPOINT" \
-H "X-API-Key: $PICTOGRAPH_API_KEY"
Returns WebhookEndpoint
WebhookEndpoint · 13 fields
class WebhookEndpoint(BaseModel):
"""A registered outbound webhook destination."""
id: str
organization_id: str
url: str
description: str | None = None
event_types: list[str] = []
enabled: bool = True
secret_version: int = 1
secret_prefix: str | None = None
consecutive_failures: int = 0
disabled_reason: str | None = None
auth_header_names: list[str] | None = None
last_delivery_at: datetime | None = None
created_at: datetime | None = None
update
Change the url, description, subscribed events, or enabled flag. Only the fields you pass change; re-enabling clears the failure health state.
Source: Webhooks.update
| Arg | Type | Default | Notes |
|---|---|---|---|
endpoint |
str |
required | Webhook endpoint name or id. |
url |
str | None |
None |
New destination URL. Left unchanged when None. |
description |
str | None |
None |
New label. Left unchanged when None. |
event_types |
Sequence[str] | None |
None |
Replaces the subscribed event list. Left unchanged when None. |
enabled |
bool | None |
None |
Pause or resume delivery. Left unchanged when None. |
ep = client.webhooks.update(
endpoint="https://example.com/hooks/pictograph",
url="https://example.com/hooks/v2",
event_types=["workflow_run.completed"],
enabled=True,
)
pictograph webhooks update https://example.com/hooks/pictograph \
--url https://example.com/hooks/v2 \
--event workflow_run.completed \
--enabled
curl -s -X PATCH "https://api.pictograph.io/api/v1/developer/webhooks/endpoints/$ENDPOINT" \
-H "X-API-Key: $PICTOGRAPH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/v2", "event_types": ["workflow_run.completed"], "enabled": true}'
Returns WebhookEndpoint
WebhookEndpoint · 13 fields
class WebhookEndpoint(BaseModel):
"""A registered outbound webhook destination."""
id: str
organization_id: str
url: str
description: str | None = None
event_types: list[str] = []
enabled: bool = True
secret_version: int = 1
secret_prefix: str | None = None
consecutive_failures: int = 0
disabled_reason: str | None = None
auth_header_names: list[str] | None = None
last_delivery_at: datetime | None = None
created_at: datetime | None = None
rotate_secret
Mint a new signing secret. The plaintext is returned once; the previous secret stays valid through a short grace window, and deliveries carry both signatures, so you can roll receivers without dropping anything.
Source: Webhooks.rotate_secret
| Arg | Type | Default | Notes |
|---|---|---|---|
endpoint |
str |
required | Webhook endpoint name or id. |
rotated = client.webhooks.rotate_secret(
endpoint="https://example.com/hooks/pictograph",
)
print(rotated.secret) # also revealable from Settings
pictograph webhooks rotate-secret https://example.com/hooks/pictograph
curl -s -X POST "https://api.pictograph.io/api/v1/developer/webhooks/endpoints/$ENDPOINT/rotate-secret" \
-H "X-API-Key: $PICTOGRAPH_API_KEY"
Returns CreatedWebhookEndpoint
CreatedWebhookEndpoint · 2 fields
class CreatedWebhookEndpoint(BaseModel):
"""Create / rotate response - carries the one-time signing secret."""
endpoint: WebhookEndpoint
secret: str
test
Send a synthetic signed test event and get the immediate result. The attempt is recorded in the delivery log.
Source: Webhooks.test
| Arg | Type | Default | Notes |
|---|---|---|---|
endpoint |
str |
required | Webhook endpoint name or id. |
result = client.webhooks.test(
endpoint="https://example.com/hooks/pictograph",
)
print(result["delivered"], result["status_code"])
pictograph webhooks test https://example.com/hooks/pictograph
curl -s -X POST "https://api.pictograph.io/api/v1/developer/webhooks/endpoints/$ENDPOINT/test" \
-H "X-API-Key: $PICTOGRAPH_API_KEY"
Returns dict[str, Any]
deliveries
Inspect the delivery log, optionally filtered by endpoint and status
(pending, delivered, failed, dead_letter).
Source: Webhooks.deliveries
| Arg | Type | Default | Notes |
|---|---|---|---|
endpoint |
str | None |
None |
Webhook endpoint name or id. |
status |
WebhookDeliveryStatus | None |
None |
Only deliveries in this state: pending, delivered or failed. |
limit |
int |
50 |
Page size. |
offset |
int |
0 |
Page offset. |
for d in client.webhooks.deliveries(status="failed", limit=20):
print(d.id, d.event_type, d.attempts, d.last_status_code, d.last_error)
pictograph webhooks deliveries --status failed --limit 20
curl -s "https://api.pictograph.io/api/v1/developer/webhooks/deliveries?status=failed&limit=20" \
-H "X-API-Key: $PICTOGRAPH_API_KEY"
Returns Sequence[WebhookDelivery]
WebhookDelivery · 12 fields
class WebhookDelivery(BaseModel):
"""One delivery attempt-set for an event to an endpoint (durable, retried)."""
id: str
endpoint_id: str
organization_id: str
event_type: str
delivery_id: str
status: Literal['pending', 'delivered', 'failed', 'dead_letter']
attempts: int = 0
last_status_code: int | None = None
last_error: str | None = None
next_retry_at: datetime | None = None
created_at: datetime | None = None
delivered_at: datetime | None = None
| Field | Meaning |
|---|---|
id |
Delivery row UUID (use with replay). |
delivery_id |
Idempotency token sent to your endpoint as X-Pictograph-Delivery-Id. |
event_type |
The event delivered. |
status |
pending, delivered, failed, or dead_letter. |
attempts |
Attempts made so far. |
last_status_code |
HTTP status of the last attempt. |
last_error |
Error from the last failed attempt. |
next_retry_at |
When the next attempt is due (while pending). |
delivered_at |
When delivery first succeeded. |
replay
Re-queue a failed or dead-letter delivery with a fresh retry budget.
Source: Webhooks.replay
| Arg | Type | Default | Notes |
|---|---|---|---|
delivery_id |
str |
required | Delivery id from the deliveries list. |
client.webhooks.replay(
delivery_id="f3a4b5c6-7d8e-4f92-a013-b4c5d6e7f809",
)
pictograph webhooks replay f3a4b5c6-7d8e-4f92-a013-b4c5d6e7f809
curl -s -X POST "https://api.pictograph.io/api/v1/developer/webhooks/deliveries/$DELIVERY_ID/replay" \
-H "X-API-Key: $PICTOGRAPH_API_KEY"
Returns None
delete
Delete an endpoint and its delivery history.
Source: Webhooks.delete
| Arg | Type | Default | Notes |
|---|---|---|---|
endpoint |
str |
required | Webhook endpoint name or id. |
client.webhooks.delete(
endpoint="https://example.com/hooks/pictograph",
)
pictograph webhooks delete https://example.com/hooks/pictograph --yes
curl -s -X DELETE "https://api.pictograph.io/api/v1/developer/webhooks/endpoints/$ENDPOINT" \
-H "X-API-Key: $PICTOGRAPH_API_KEY"
Returns None
Verifying a delivery
Every delivery carries X-Pictograph-Signature shaped t=<unix_ts>,v1=<hex>.
Compute HMAC-SHA256 over "{timestamp}.{raw_body}" with your signing secret and
compare in constant time. Reject deliveries older than five minutes to close the
replay window. During a rotation the header carries several v1= values, so
accept if any one matches.
import hashlib
import hmac
import time
def verify(secret: str, signature_header: str, raw_body: bytes, *, tolerance: int = 300) -> bool:
timestamp: str | None = None
signatures: list[str] = []
for part in signature_header.split(","):
key, _, value = part.partition("=")
if key == "t":
timestamp = value
elif key == "v1":
signatures.append(value)
if timestamp is None or not signatures:
return False
if abs(time.time() - int(timestamp)) > tolerance:
return False # stale: possible replay
expected = hmac.new(
secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return any(hmac.compare_digest(sig, expected) for sig in signatures)
| Header | Meaning |
|---|---|
X-Pictograph-Signature |
t=<ts>,v1=<hex> (one or more v1 during rotation). |
X-Pictograph-Delivery-Id |
Stable id for the delivery. Dedupe on it - delivery is at-least-once. |
X-Pictograph-Event-Type |
The event type, for routing. |
X-Pictograph-Timestamp |
The signed timestamp, mirrored for convenience. |
Retries and auto-disable
A non-2xx response or a connection failure is retried on a backoff schedule
(roughly 30s, 2m, 10m, 1h, 6h). Once the budget is exhausted the delivery is
marked dead_letter rather than dropped, so you can replay it after the
endpoint recovers. An endpoint that fails many deliveries in a row is
auto-disabled; update(..., enabled=True) resumes it. Return a 2xx quickly and
do heavy processing asynchronously: a slow handler looks like a failure and
triggers retries.