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

NameInTypeNotes
limitqueryintegerDefault 25, in [1, 100]. Outside the range is 400 invalid_request, never clamped.
cursorquerystringOpaque, from the previous page's next_cursor. Never construct one.

Responses

200A page.

FieldTypeRequiredNotes
dataDestination[]yes
has_morebooleanyes
next_cursorstring | nullyes
{
  "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

FieldTypeRequiredNotes
namestringyesFor a person reading a list.
urluriyeshttps:// only, and it must resolve to a public address; otherwise 403 forbidden (07 §4.2).
eventsWebhookEvent[]Default ["run.completed"]. A non-empty subset of 02 §4.6.
secretstringSupply your own, or omit and one is generated.
{
  "name": "bullshit.doctor",
  "url": "https://bullshit.doctor/hooks/script-rip",
  "events": [
    "run.completed",
    "run.failed"
  ]
}

Responses

201Registered. secret appears here once.

FieldTypeRequiredNotes
iduuidyes
namestringyes
urluriyeshttps:// only.
eventsWebhookEvent[]yes
enabledbooleanyes
disabled_reasonstring | nullDerived. A sentence when the destination was disabled automatically after five consecutive exhausted deliveries (07 §12.5); null otherwise.
created_atdate-timeyes
updated_atdate-time
last_deliveryLastDelivery | null
secretstringyesShown 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

NameInTypeNotes
idpathuuidThe destination id.

Request body

FieldTypeRequiredNotes
namestring
urlurihttps:// only; re-validated like a registration.
eventsWebhookEvent[]
enabledboolean
rotate_secretbooleantrue to issue a new secret, returned once in this response.
secretstringWith rotate_secret: supply your own.
{
  "enabled": false
}

Responses

200The destination. Carries secret only when it was rotated.

FieldTypeRequiredNotes
iduuidyes
namestringyes
urluriyeshttps:// only.
eventsWebhookEvent[]yes
enabledbooleanyes
disabled_reasonstring | nullyesDerived. A sentence when the destination was disabled automatically after five consecutive exhausted deliveries (07 §12.5); null otherwise.
created_atdate-timeyes
updated_atdate-timeyes
last_deliveryLastDelivery | nullyes
secretstringPresent 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

NameInTypeNotes
idpathuuidThe destination id.

Responses

200A receipt.

FieldTypeRequiredNotes
iduuidyes
deletedbooleanyes
deliveries_removedintegeryes
messagestringyes
{
  "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

NameInTypeNotes
idpathuuidThe destination id.

Request body

FieldTypeRequiredNotes
eventWebhookEventDefault run.completed. Test another event's shape.
{}

Responses

200What happened.

FieldTypeRequiredNotes
destination_iduuidyes
eventWebhookEventyes
deliveredbooleanyes
status_codeinteger | nullyes
latency_msintegeryes
signature_headerstringyesThe exact X-ScriptRip-Signature that was sent, so you can verify your verifier.
body_sha256stringyessha256 of the exact bytes sent.
errorstring | nullyesWhich of DNS, TLS, timeout, redirect or refused-address failed, when delivered is false and there was no status code.
messagestringyes
{
  "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

NameInTypeNotes
idpathuuidThe destination id.
limitqueryintegerDefault 25, in [1, 100]. Outside the range is 400 invalid_request, never clamped.
cursorquerystringOpaque, from the previous page's next_cursor. Never construct one.
status (repeatable)queryDeliveryStatusRepeatable; values union. ?status=exhausted is the question an operator actually has.
event (repeatable)queryWebhookEventRepeatable; values union.

Responses

200A page.

FieldTypeRequiredNotes
dataDelivery[]yes
has_morebooleanyes
next_cursorstring | nullyes
{
  "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"