Destinations
Where finished transcripts are pushed, signed.
GET /v1/destinations
List destinations. Every destination for the account, newest first, in the one list envelope. The ceiling is 20 per account so it is never paginated in practice.
Scope: destinations:read
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
limit | query | integer | Default 25, in [1, 100]. Outside the range is 400 invalid_request, never clamped. |
cursor | query | string | Opaque, from the previous page's next_cursor. Never construct one. |
Responses
200 — A page.
| Field | Type | Required | Notes |
|---|---|---|---|
data | Destination[] | yes | |
has_more | boolean | yes | |
next_cursor | string | null | yes |
{
"data": [
{
"id": "6d2b8a10-1f47-4c8e-a2f0-cb9e4d3a7711",
"name": "bullshit.doctor",
"url": "https://bullshit.doctor/hooks/script-rip",
"events": [
"run.completed",
"run.failed"
],
"enabled": true,
"disabled_reason": null,
"created_at": "2026-03-01T09:14:02Z",
"updated_at": "2026-03-11T18:10:00Z",
"last_delivery": {
"event": "run.completed",
"status": "delivered",
"status_code": 200,
"attempt": 1,
"delivered_at": "2026-03-11T18:09:59Z"
}
}
],
"has_more": false,
"next_cursor": null
}Errors: invalid_request (400), unauthenticated (401), forbidden (403), rate_limited (429), internal_error (500)
cURL
curl -sS https://<host>/v1/destinations -H "Authorization: Bearer $SCRIPTRIP_KEY"POST /v1/destinations
Register a destination. The secret is in this response and never again. The URL must be https:// and must resolve to a public address; loopback, private, link-local, carrier-grade NAT, unique-local and cloud-metadata addresses are 403 forbidden at registration and re-checked at every send. Above 20 destinations is 429 quota_exceeded.
Scope: destinations:write
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | For a person reading a list. |
url | uri | yes | https:// only, and it must resolve to a public address; otherwise 403 forbidden (07 §4.2). |
events | WebhookEvent[] | Default ["run.completed"]. A non-empty subset of 02 §4.6. | |
secret | string | Supply your own, or omit and one is generated. |
{
"name": "bullshit.doctor",
"url": "https://bullshit.doctor/hooks/script-rip",
"events": [
"run.completed",
"run.failed"
]
}Responses
201 — Registered. secret appears here once.
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | |
name | string | yes | |
url | uri | yes | https:// only. |
events | WebhookEvent[] | yes | |
enabled | boolean | yes | |
disabled_reason | string | null | Derived. A sentence when the destination was disabled automatically after five consecutive exhausted deliveries (07 §12.5); null otherwise. | |
created_at | date-time | yes | |
updated_at | date-time | ||
last_delivery | LastDelivery | null | ||
secret | string | yes | Shown ONCE — in this response and never again. Store it as SCRIPTRIP_WEBHOOK_SECRET on the receiver. |
{
"id": "6d2b8a10-1f47-4c8e-a2f0-cb9e4d3a7711",
"name": "bullshit.doctor",
"url": "https://bullshit.doctor/hooks/script-rip",
"events": [
"run.completed",
"run.failed"
],
"enabled": true,
"disabled_reason": null,
"created_at": "2026-03-01T09:14:02Z",
"updated_at": "2026-03-01T09:14:02Z",
"last_delivery": null,
"secret": "whsec_7Kq2mVx9RnB4tLpC0eZaW1sYdH6gJ3fU"
}Errors: invalid_request (400), unauthenticated (401), forbidden (403), rate_limited (429), internal_error (500), quota_exceeded (429)
cURL
curl -sS -X POST https://<host>/v1/destinations \
-H "Authorization: Bearer $SCRIPTRIP_KEY" -H "Content-Type: application/json" \
-d '{"name":"bullshit.doctor","url":"https://bullshit.doctor/hooks/script-rip","events":["run.completed","run.failed"]}'PATCH /v1/destinations/{id}
Update a destination. Any of name, url, events, enabled; absent fields are unchanged. {"rotate_secret": true} — optionally with secret — returns the new secret once; there is no dual-secret window. Re-enabling replays nothing. Disabling keeps the delivery history.
Scope: destinations:write
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
id | path | uuid | The destination id. |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | ||
url | uri | https:// only; re-validated like a registration. | |
events | WebhookEvent[] | ||
enabled | boolean | ||
rotate_secret | boolean | true to issue a new secret, returned once in this response. | |
secret | string | With rotate_secret: supply your own. |
{
"enabled": false
}Responses
200 — The destination. Carries secret only when it was rotated.
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | |
name | string | yes | |
url | uri | yes | https:// only. |
events | WebhookEvent[] | yes | |
enabled | boolean | yes | |
disabled_reason | string | null | yes | Derived. A sentence when the destination was disabled automatically after five consecutive exhausted deliveries (07 §12.5); null otherwise. |
created_at | date-time | yes | |
updated_at | date-time | yes | |
last_delivery | LastDelivery | null | yes | |
secret | string | Present only when rotate_secret was true. Shown once. |
{
"id": "6d2b8a10-1f47-4c8e-a2f0-cb9e4d3a7711",
"name": "bullshit.doctor",
"url": "https://bullshit.doctor/hooks/script-rip",
"events": [
"run.completed",
"run.failed"
],
"enabled": false,
"disabled_reason": null,
"created_at": "2026-03-01T09:14:02Z",
"updated_at": "2026-03-11T18:10:00Z",
"last_delivery": {
"event": "run.completed",
"status": "delivered",
"status_code": 200,
"attempt": 1,
"delivered_at": "2026-03-11T18:09:59Z"
}
}Errors: invalid_request (400), unauthenticated (401), forbidden (403), rate_limited (429), internal_error (500), not_found (404)
cURL
curl -sS -X PATCH "https://<host>/v1/destinations/$DEST_ID" \
-H "Authorization: Bearer $SCRIPTRIP_KEY" -H "Content-Type: application/json" \
-d '{"enabled":false}'DELETE /v1/destinations/{id}
Remove a destination. Deliveries cascade, so the history goes with it — which is why enabled: false exists for the case where the history is worth keeping. 200 with a receipt; a second delete is 404.
Scope: destinations:write
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
id | path | uuid | The destination id. |
Responses
200 — A receipt.
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | |
deleted | boolean | yes | |
deliveries_removed | integer | yes | |
message | string | yes |
{
"id": "6d2b8a10-1f47-4c8e-a2f0-cb9e4d3a7711",
"deleted": true,
"deliveries_removed": 412,
"message": "The destination and its 412 delivery records were removed."
}Errors: unauthenticated (401), forbidden (403), rate_limited (429), internal_error (500), not_found (404)
cURL
curl -sS -X DELETE "https://<host>/v1/destinations/$DEST_ID" -H "Authorization: Bearer $SCRIPTRIP_KEY"POST /v1/destinations/{id}/test
Send a signed test delivery. Sent inline; the result is the response. The body is a complete, correctly signed payload carrying a synthetic run whose id is the all-zero UUID and whose test field is true. It writes no delivery record. A non-2xx from the receiver, a DNS failure, a TLS failure or a timeout is still 200 here with delivered: false — the test succeeded in telling you what happened.
Scope: destinations:write · Per key, 6 per minute — human-triggered.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
id | path | uuid | The destination id. |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
event | WebhookEvent | Default run.completed. Test another event's shape. |
{}Responses
200 — What happened.
| Field | Type | Required | Notes |
|---|---|---|---|
destination_id | uuid | yes | |
event | WebhookEvent | yes | |
delivered | boolean | yes | |
status_code | integer | null | yes | |
latency_ms | integer | yes | |
signature_header | string | yes | The exact X-ScriptRip-Signature that was sent, so you can verify your verifier. |
body_sha256 | string | yes | sha256 of the exact bytes sent. |
error | string | null | yes | Which of DNS, TLS, timeout, redirect or refused-address failed, when delivered is false and there was no status code. |
message | string | yes |
{
"destination_id": "6d2b8a10-1f47-4c8e-a2f0-cb9e4d3a7711",
"event": "run.completed",
"delivered": true,
"status_code": 200,
"latency_ms": 214,
"signature_header": "t=1773252601,v1=1f8ac10f23c5b5bc1167bda84b833e5c057a77d2ef2f3f2b6b0c1d4e5a6b7c8d",
"body_sha256": "9c1185a5c5e9fc54612808977ee8f548b2258d31f5a2a0e9b7c3d4e5f6a7b8c9",
"error": null,
"message": "Delivered. The receiver answered 200 in 214 ms."
}Errors: invalid_request (400), unauthenticated (401), forbidden (403), rate_limited (429), internal_error (500), not_found (404)
cURL
curl -sS -X POST "https://<host>/v1/destinations/$DEST_ID/test" -H "Authorization: Bearer $SCRIPTRIP_KEY"GET /v1/destinations/{id}/deliveries
What we sent and what came back. Cursor paginated, newest first. The audit answer, where last_delivery on the destination is the health answer. A receiver that was down reads this to find what it missed, then pulls those runs by id or asks for them again with POST /v1/runs/{id}/deliver.
Scope: destinations:read
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
id | path | uuid | The destination id. |
limit | query | integer | Default 25, in [1, 100]. Outside the range is 400 invalid_request, never clamped. |
cursor | query | string | Opaque, from the previous page's next_cursor. Never construct one. |
status (repeatable) | query | DeliveryStatus | Repeatable; values union. ?status=exhausted is the question an operator actually has. |
event (repeatable) | query | WebhookEvent | Repeatable; values union. |
Responses
200 — A page.
| Field | Type | Required | Notes |
|---|---|---|---|
data | Delivery[] | yes | |
has_more | boolean | yes | |
next_cursor | string | null | yes |
{
"data": [
{
"id": "8c41d0aa-63e2-4a52-9f77-2b1c0e8d4a90",
"run_id": "0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b",
"event": "run.completed",
"attempt": 3,
"status": "delivered",
"status_code": 200,
"error": null,
"next_attempt_at": null,
"delivered_at": "2026-03-11T18:14:22Z",
"created_at": "2026-03-11T18:09:58Z"
}
],
"has_more": false,
"next_cursor": null
}Errors: invalid_request (400), unauthenticated (401), forbidden (403), rate_limited (429), internal_error (500), not_found (404)
cURL
curl -sS "https://<host>/v1/destinations/$DEST_ID/deliveries?status=exhausted" \
-H "Authorization: Bearer $SCRIPTRIP_KEY"