A customer is who you send to. An endpoint is where their webhooks go. Both are managed through the REST API with a manage-scoped token, in the dashboard under Customers, or by the customers themselves in the portal. All three surfaces operate on the same objects, so an endpoint a customer adds in the portal is immediately visible to your API and your team.
Customers#
Customers are identified by an externalId you choose: your database's account id, an org slug, whatever you already use.
curl -X POST https://api.repost.sh/v1/customers/acme \
-H "Authorization: Bearer $REPOST_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Acme Corp", "metadata": {"plan": "enterprise"}}'Creation is an idempotent upsert. It returns 201 when the customer is new and 200 with the existing customer otherwise, so you can call it from a signup flow without checking first. A customer carries an optional display name and free-form metadata.
| Operation | Route |
|---|---|
| Upsert | POST /v1/customers/{externalId} |
| Get | GET /v1/customers/{externalId} |
| List | GET /v1/customers?limit=20&cursor=... |
| Update | PATCH /v1/customers/{externalId} |
| Delete | DELETE /v1/customers/{externalId} (also removes its endpoints, see below) |
You don't have to pre-create customers. Creating an endpoint for an unknown externalId creates the customer at the same time.
Endpoints#
curl -X POST https://api.repost.sh/v1/customers/acme/endpoints \
-H "Authorization: Bearer $REPOST_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://acme.example.com/webhooks",
"description": "Production receiver",
"subscriptions": ["order.created", "order.refunded"],
"headers": {"x-acme-source": "repost"}
}'The 201 response contains the endpoint and its whsec_... signing secret. The secret is returned only on creation; store it or show it to your customer right away. It remains available later through the secret routes and the portal.
| Property | Meaning |
|---|---|
url | HTTPS destination. Validated for safety, see below. |
subscriptions | Event-type names to deliver, or ["*"] (the default) for all of them. |
description | Optional label, up to 512 characters. |
headers | Custom headers added to every delivery. The webhook-*, content-type, and user-agent headers are reserved and cannot be overridden. |
status | ENABLED, PAUSED, or DISABLED. |
retryPolicy | Optional. {"version": 1, "delaysMs": [...]} replaces the default retry schedule for this endpoint; null restores it. |
The other routes follow the same pattern: GET /v1/customers/{externalId}/endpoints lists a customer's endpoints, and GET, PATCH, and DELETE /v1/endpoints/{id} operate on one. Updates apply to pending retries as well, so correcting a mistyped URL also redirects the backlog.
Pause and resume#
curl -X POST https://api.repost.sh/v1/endpoints/{id}/pause -H "Authorization: Bearer $REPOST_TOKEN"
curl -X POST https://api.repost.sh/v1/endpoints/{id}/resume -H "Authorization: Bearer $REPOST_TOKEN"Use pause for maintenance windows. While an endpoint is paused nothing is delivered to it and nothing burns retries against a receiver you know is down. Retries that come due are held. A message published meanwhile still gets a delivery for the endpoint, listed as pending (the dashboard labels it held · endpoint paused), with no attempt made. Together those messages are the endpoint's missed window. Resume decides what happens to that window with an optional catchUp body:
curl -X POST https://api.repost.sh/v1/endpoints/{id}/resume \
-H "Authorization: Bearer $REPOST_TOKEN" -H "Content-Type: application/json" \
-d '{"catchUp":"replay"}'catchUp | What happens to the messages missed while paused |
|---|---|
replay | A hidden catch-up job sends them (only the ones the endpoint has not received yet) in publish order, while live messages keep flowing. Deliveries run in parallel, so messages published within the same second can reach the endpoint in either order; use Webhook-Id and the message timestamp when order matters. The endpoint reports catchUpFrom until the job completes. |
skip | They are forgotten; only messages published after the resume are delivered. Their held deliveries read failed with reason skipped. |
later (default) | The endpoint goes live now and the window is kept (catchUpFrom), so a later resume offers it again. Nothing moves the deliveries in that window until a catch-up runs, and they say so: they stay pending with holdReason: "catch_up_deferred". |
A body-less resume therefore keeps the window and delivers nothing from it. An invalid catchUp value is rejected with 400. The resume response is the endpoint record plus a missed block, { "count", "countIsLowerBound", "since", "choice", "jobId" }, saying how many messages the choice applied to and which catch-up job replays them. The count comes from the search index, which commits a few seconds after a publish, so a resume taken right after one reports countIsLowerBound: true and the dashboard says "at least N"; the choice still applies to everything published while paused, counted or not. While the catch-up job runs, the endpoint record's catchUp block reports its progress as { "jobId", "total", "done" } and is null otherwise. Pausing an endpoint while its catch-up is running cancels the catch-up; the remaining messages are offered again on the next resume. The same three choices exist in the dashboard, the customer portal, repost send endpoints resume --catch-up, and the send_endpoint_resume agent operation.
Deleting an endpoint behaves differently: its pending, held and retrying deliveries fail terminally as endpoint_gone, and a replay of one of them answers 404. The endpoint's delivery history stays searchable by its id.
URL safety#
Endpoint URLs are validated when set and checked again when a connection is opened. localhost, .local names, and private or reserved IP ranges are rejected with 422, and DNS answers are re-checked at dial time, so an endpoint cannot be pointed at your internal network. To receive webhooks on a development machine, use a public target such as a Repost bucket forwarded to localhost with the CLI.
Deleting a customer#
DELETE /v1/customers/{externalId} removes the customer, its endpoints and their signing secrets at once. Nothing is delivered to those endpoints from then on, the customer's portal links and sessions stop working, and the customer's messages and deliveries disappear from every list and search immediately.
The hiding is immediate because every read excludes the deleted customer as soon as the deletion commits — messages, payloads, deliveries, attempts and the dead-letter queue alike, on the API, the CLI, the dashboard and the portal. The stored documents themselves are erased by a queued erasure job, because the search index rewrites its files in batches: one delete pass runs right away and re-runs 24 hours, 6 days and 24 days after the previous pass, so the erasure closes about a month after the delete. The environment's settings page lists each erasure with its passes done, next sweep and closing time, and repost send environments get shows the same in an erasure block; the entry stays visible for 30 days after it closes. Deleting a whole environment works the same way at the environment's scope: its API tokens are revoked and publishes rejected immediately, its public event docs are taken down, and one erasure covers everything it held.