Send signed outgoing webhooks
Fed by the records themselves, delivered only to public HTTPS addresses checked at every attempt, retried, and switched off after repeated failures. A submitter's details travel only to webhooks that opted into them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -144,6 +144,8 @@ tags:
|
||||
description: What is wrong with the site, and what is left to translate
|
||||
- name: Batch
|
||||
description: Several operations in one, and importing records from JSON or CSV
|
||||
- name: Webhooks
|
||||
description: Telling other systems what happened here
|
||||
- name: Users
|
||||
description: Local user administration
|
||||
- name: Media
|
||||
@@ -3812,6 +3814,167 @@ paths:
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
"422": { $ref: "#/components/responses/ValidationError" }
|
||||
|
||||
/v1/webhooks:
|
||||
get:
|
||||
tags: [Webhooks]
|
||||
summary: List webhooks
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
responses:
|
||||
"200":
|
||||
description: Paginated webhooks
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/WebhookCollection" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
post:
|
||||
tags: [Webhooks]
|
||||
summary: Create a webhook
|
||||
description: |
|
||||
Somewhere to POST to when something happens here. HTTPS to a public
|
||||
address only — checked now and again at every delivery — and redirects are
|
||||
not followed. The `secret` signing the deliveries is generated unless sent,
|
||||
and is in this answer's `secret` once, never again.
|
||||
|
||||
Each delivery is a JSON body `{id, event, created_at, data}` with headers
|
||||
`X-Webhook-Event`, `X-Webhook-Delivery`, `X-Webhook-Timestamp` and
|
||||
`X-Webhook-Signature`: `sha256=` and the HMAC-SHA256 of
|
||||
`{timestamp}.{body}` with the secret. Failed deliveries are retried five
|
||||
times over about two and a half hours; twenty failures in a row switch the
|
||||
webhook off.
|
||||
|
||||
Events: `application.received` (its sender's details only with
|
||||
`include_personal_data`), `content.published`, `content.unpublished`,
|
||||
`review.requested`, `review.returned` — or `*`.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/WebhookWrite" }
|
||||
responses:
|
||||
"201":
|
||||
description: Webhook created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data: { $ref: "#/components/schemas/Webhook" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
"422": { $ref: "#/components/responses/ValidationError" }
|
||||
|
||||
/v1/webhooks/{webhook}:
|
||||
parameters:
|
||||
- name: webhook
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
get:
|
||||
tags: [Webhooks]
|
||||
summary: Show a webhook
|
||||
responses:
|
||||
"200":
|
||||
description: The webhook
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data: { $ref: "#/components/schemas/Webhook" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
"404": { description: Not found }
|
||||
patch:
|
||||
tags: [Webhooks]
|
||||
summary: Update a webhook
|
||||
description: "Partial update. `rotate_secret` makes a new secret, in this answer's `secret` once. Switching it back on resets its failure count."
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/WebhookWrite" }
|
||||
responses:
|
||||
"200":
|
||||
description: Webhook updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data: { $ref: "#/components/schemas/Webhook" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
"422": { $ref: "#/components/responses/ValidationError" }
|
||||
delete:
|
||||
tags: [Webhooks]
|
||||
summary: Delete a webhook
|
||||
responses:
|
||||
"204": { description: Deleted }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
|
||||
/v1/webhooks/{webhook}/deliveries:
|
||||
get:
|
||||
tags: [Webhooks]
|
||||
summary: Deliveries of a webhook
|
||||
description: Newest first.
|
||||
parameters:
|
||||
- name: webhook
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
- name: status
|
||||
in: query
|
||||
description: "`pending`, `succeeded` or `failed`."
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"200":
|
||||
description: Paginated deliveries
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/WebhookDeliveryCollection" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
|
||||
/v1/webhooks/{webhook}/ping:
|
||||
post:
|
||||
tags: [Webhooks]
|
||||
summary: Send a test delivery
|
||||
description: A `ping` event, to check the receiving end.
|
||||
parameters:
|
||||
- name: webhook
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
"202":
|
||||
description: Ping queued
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data: { $ref: "#/components/schemas/WebhookDelivery" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
|
||||
/v1/webhook-deliveries/{delivery}/redeliver:
|
||||
post:
|
||||
tags: [Webhooks]
|
||||
summary: Send a delivery again
|
||||
description: As a new delivery of the same event and payload.
|
||||
parameters:
|
||||
- name: delivery
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
"202":
|
||||
description: Delivery queued
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data: { $ref: "#/components/schemas/WebhookDelivery" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
|
||||
/v1/media:
|
||||
get:
|
||||
tags: [Media]
|
||||
@@ -5640,6 +5803,66 @@ components:
|
||||
links: { $ref: "#/components/schemas/PaginationLinks" }
|
||||
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
||||
|
||||
Webhook:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
name: { type: string }
|
||||
url: { type: string, format: uri }
|
||||
events: { type: array, items: { type: string } }
|
||||
include_personal_data: { type: boolean }
|
||||
is_active: { type: boolean }
|
||||
consecutive_failures: { type: integer }
|
||||
last_delivered_at: { type: string, format: date-time, nullable: true }
|
||||
created_at: { type: string, format: date-time }
|
||||
updated_at: { type: string, format: date-time }
|
||||
|
||||
WebhookWrite:
|
||||
type: object
|
||||
required: [name, url, events]
|
||||
properties:
|
||||
name: { type: string, maxLength: 120 }
|
||||
url: { type: string, format: uri, description: "HTTPS, to a public address." }
|
||||
events:
|
||||
type: array
|
||||
items: { type: string, enum: ["*", application.received, content.published, content.unpublished, review.requested, review.returned] }
|
||||
secret: { type: string, minLength: 16, description: On creation only; generated when left out. }
|
||||
rotate_secret: { type: boolean, description: On update only. }
|
||||
include_personal_data: { type: boolean, default: false }
|
||||
is_active: { type: boolean }
|
||||
|
||||
WebhookCollection:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/Webhook" }
|
||||
links: { $ref: "#/components/schemas/PaginationLinks" }
|
||||
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
||||
|
||||
WebhookDelivery:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
uuid: { type: string, description: "Sent as `X-Webhook-Delivery`." }
|
||||
event: { type: string }
|
||||
status: { type: string, enum: [pending, succeeded, failed] }
|
||||
attempts: { type: integer }
|
||||
response_status: { type: integer, nullable: true }
|
||||
error: { type: string, nullable: true }
|
||||
payload: { type: object, additionalProperties: true }
|
||||
delivered_at: { type: string, format: date-time, nullable: true }
|
||||
created_at: { type: string, format: date-time }
|
||||
|
||||
WebhookDeliveryCollection:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/WebhookDelivery" }
|
||||
links: { $ref: "#/components/schemas/PaginationLinks" }
|
||||
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
||||
|
||||
MediaFor:
|
||||
type: string
|
||||
enum: [pages, technologies, projects, members, clients, testimonials]
|
||||
|
||||
Reference in New Issue
Block a user