HHeros Developers

Version 1.0.0

Heros DELIVERY

Webhook endpoints and the deliveries made to them.

Base URLhttps://sandbox.droomwork.io
AuthenticationDroomwork-Api-Key: dw_test_… or Authorization: Bearer dw_test_…
Signed in: requests you run on this page use a key made for your account. Your keys →

Register an endpoint and we deliver the events you asked for to it, each one signed so you can prove it came from us.

What you should know before you start

Verify before you parse. Every delivery carries a Droomwork-Signature header over the exact bytes of the body. Check it against the raw bytes you received, not against a re-serialisation of the parsed object: key order changes and the signature won't match. Don't let a body that fails verification reach your JSON parser.

The signature carries a timestamp. Check it. The header is t=<unix seconds>,v1=<hex digest>, and the digest is an HMAC-SHA256 over t + "." + body. One v1= comes for each active secret, so after you rotate there are two: read them all and accept when any one matches. Reject a timestamp more than five minutes from your own clock, in either direction, however good the digest is. An old timestamp with a valid signature is a replay of something we really did send.

You see the secret once. We return it when you create the endpoint and when you rotate, and never again. Afterwards you get a digest that tells you which secret is which.

Two secrets can be active at once. Rotating issues a new secret and keeps the previous one, so you have time to move your verification without a delivery being dropped. Every delivery is signed with each active secret, and matching any one of them is enough.

Retries follow a published schedule. Eight attempts in all. The first goes out at once, and each one after it waits longer than the last: 1m, 5m, 30m, 2h, 5h, 10h, then 20h. Those are gaps between attempts, so the eighth lands about a day and a half after the event. A 2xx from you stops them. Anything else continues, including no answer at all. Each attempt is signed with its own timestamp, so a retry twenty hours later still verifies.

A failed delivery is not a lost event. After the last attempt we mark it failed and keep it available for replay. If you were down for a day, you lost the automatic delivery and nothing else.

Two scopes, and a webhook is not money. delivery:read lists endpoints and deliveries. delivery:write registers an endpoint, rotates its secret, disables it, sends a test event and replays a delivery. Sandbox keys from the developer portal carry both; a key you issue for part of your system carries what you name, and a key that only reads payroll cannot move where your events go.

Getting started

Register an endpoint and keep the secret it returns. Send a test event and verify what arrives. A test event goes through everything a real event goes through: same envelope, same signature, same schedule. It's marked is_test, its envelope says mocked is true, and its data is a stand-in we make up. So use it to prove your receiver answers and your signature check passes, then read the event type's own shape before you act on a real one.

Every operation below has two samples: Direct HTTP, with nothing but your language's own client, and Client library, with ours. Same request, same answer; the switch above each sample picks one and remembers it. Which should I use?

GET/v1/webhook_endpoints#

See every endpoint you registered

webhook.endpoints.list

This lists every webhook endpoint you registered, with the url we deliver to and the event_types it takes. It gives you every id, so your product can show where your events go.

You send nothing here, and there is nothing to filter on. Each endpoint is one record in data, newest first, and data is empty until you register one.

Check disabled_at on every record. A disabled endpoint stays in the list, and disabled_at is the only field that marks it.

Nothing has to exist before this call. After this, GET /v1/webhook_endpoints/{id} reads one endpoint back, and GET /v1/webhook_deliveries takes an id from here as endpoint_id to list every delivery we made to that endpoint. The same id is the {id} on every other /v1/webhook_endpoints/{id} call.

Returns

The endpoints you registered, each with a digest for every secret your verifier should accept.

object always "list" required

Always list. Tells you which kind of record you are looking at, so one handler can read any response.

data array of WebhookEndpoint required

The records on this page, in the order the list promises. Empty when nothing matched.

9 fields of WebhookEndpoint
id string required

Assigned when you register the endpoint at POST /v1/webhook_endpoints; it starts with whe_ and never changes. Pass it as {id} on every /v1/webhook_endpoints/{id} call and as endpoint_id on GET /v1/webhook_deliveries.

object always "webhook_endpoint" required

Always webhook_endpoint. Tells you which kind of record you are looking at, so one handler can read any response.

livemode boolean required

False for a record in the sandbox realm, true for one in the live realm. This answers which data a record belongs to. mocked answers where its numbers came from, and the two are separate questions. A sandbox payslip computed by the real engine is livemode false and mocked false. A deferred capability in the live realm is livemode true and mocked true.

mocked boolean required

True when this response came from a mock, false when the engine computed the numbers. It stays true for a capability that is still on its documented mock, even in the live realm. It answers a different question from livemode: livemode says which realm you are in, this says where the numbers came from.

url string · uri required

The URL you registered, where we deliver. It must accept a POST and answer 2xx on success; to deliver somewhere else, register a new endpoint and disable this one.

event_types array of string required

Which events we deliver here, as you registered them, such as run.approved. A single * means every event.

secret_digests array of string required

One per active secret, newest first, at most two. A digest identifies which secret is which without being one.

disabled_at string · date-time · nullable optional

When you disabled this endpoint, as an RFC 3339 timestamp in UTC. null while it still receives deliveries.

created_at string · date-time required

When the record was created, as an RFC 3339 timestamp in UTC.

has_more boolean required

true when there are more records after this page. Pass the last record's id as starting_after to get the next page.

Other responses

401No valid credential was presented.
403The credential does not carry the required scope.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/webhook_endpoints" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, DELIVERYApi } from '@heros/sdk';

const api = new DELIVERYApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));

const result = await api.webhookEndpointsList({});
const response = await fetch('https://sandbox.droomwork.io/v1/webhook_endpoints', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import heros_sdk

config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.DELIVERYApi(client)

result = api.webhook_endpoints_list()
import os

import requests

response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/webhook_endpoints',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
    },
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

$config = HerosSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\DELIVERYApi(new GuzzleHttp\Client(), $config);

$result = $api->webhookEndpointsList();
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/webhook_endpoints');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.DeliveryApi;

ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
DeliveryApi api = new DeliveryApi(client);

var result = api.webhookEndpointsList();
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/webhook_endpoints"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Heros.Sdk.Api;
using Heros.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new DELIVERYApi(config);

var result = api.WebhookEndpointsList();
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/webhook_endpoints");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
	"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)

result, _, err := client.DELIVERYAPI.WebhookEndpointsList(ctx).Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/webhook_endpoints", nil)
	req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "list",
  "data": [
    {
      "id": "whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "webhook_endpoint",
      "livemode": false,
      "mocked": false,
      "url": "https://files.sandbox.droomwork.com/example",
      "event_types": [
        "example"
      ],
      "secret_digests": [
        "sha256:9f2c1a4e7b0d3856"
      ],
      "created_at": "2026-09-01T09:00:00Z",
      "disabled_at": "2026-09-01T09:00:00Z"
    }
  ],
  "has_more": true
}
POST/v1/webhook_endpoints#

Start receiving events at your own URL

webhook.endpoints.create

This registers the URL you want events delivered to, and hands back its first signing secret. Your own code can act on an approved payroll run: we POST that event to your URL.

Send the url events will be posted to. Add event_types to pick which events, or leave it out for all. Copy secret from the 201. Keep id for the other /v1/webhook_endpoints/{id} calls.

Nothing has to exist before this call. After this, POST /v1/webhook_endpoints/{id}/test sends a signed event to your URL, and GET /v1/webhook_deliveries/{id} gives you the bytes we signed, to check your verification against.

Headers

Idempotency-Key string required

A key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.

Body

url string · uri required

Where to deliver. Must accept a POST and answer 2xx on success.

event_types array of string optional

Which events to deliver. A single * means everything, which is the default and is what most integrations want to start with.

Returns

The endpoint record, created_at included, so you can store when you registered it without listing your endpoints.

id string required

Assigned when you register the endpoint at POST /v1/webhook_endpoints; it starts with whe_ and never changes. Pass it as {id} on every /v1/webhook_endpoints/{id} call and as endpoint_id on GET /v1/webhook_deliveries.

object always "webhook_endpoint" required

Always webhook_endpoint. Tells you which kind of record you are looking at, so one handler can read any response.

livemode boolean required

False for a record in the sandbox realm, true for one in the live realm. This answers which data a record belongs to. mocked answers where its numbers came from, and the two are separate questions. A sandbox payslip computed by the real engine is livemode false and mocked false. A deferred capability in the live realm is livemode true and mocked true.

mocked boolean required

True when this response came from a mock, false when the engine computed the numbers. It stays true for a capability that is still on its documented mock, even in the live realm. It answers a different question from livemode: livemode says which realm you are in, this says where the numbers came from.

url string · uri required

The URL you registered, where we deliver. It must accept a POST and answer 2xx on success; to deliver somewhere else, register a new endpoint and disable this one.

event_types array of string required

Which events we deliver here, as you registered them, such as run.approved. A single * means every event.

secret_digests array of string required

One per active secret, newest first, at most two. A digest identifies which secret is which without being one.

disabled_at string · date-time · nullable optional

When you disabled this endpoint, as an RFC 3339 timestamp in UTC. null while it still receives deliveries.

created_at string · date-time required

When the record was created, as an RFC 3339 timestamp in UTC.

secret string required

Shown here and never again. Keep it: it is what you verify a delivery's signature with.

Other responses

400The request could not be read, or a value was refused.
401No valid credential was presented.
403The credential does not carry the required scope.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/webhook_endpoints" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/droomwork/hooks","event_types":["run.approved","run.executed"]}'
import { Configuration, DELIVERYApi } from '@heros/sdk';

const api = new DELIVERYApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));

const result = await api.webhookEndpointsCreate({
  idempotencyKey: crypto.randomUUID(),
  deliveryWebhookEndpointCreateRequest: {"url":"https://example.com/droomwork/hooks","eventTypes":["run.approved","run.executed"]},
});
const response = await fetch('https://sandbox.droomwork.io/v1/webhook_endpoints', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    "url": "https://example.com/droomwork/hooks",
    "event_types": [
      "run.approved",
      "run.executed"
    ]
  }),
});
const result = await response.json();
import os

import heros_sdk

config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.DELIVERYApi(client)

result = api.webhook_endpoints_create(body={"url": "https://example.com/droomwork/hooks", "event_types": ["run.approved", "run.executed"]})
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/webhook_endpoints',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={"url": "https://example.com/droomwork/hooks", "event_types": ["run.approved", "run.executed"]},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

$config = HerosSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\DELIVERYApi(new GuzzleHttp\Client(), $config);

$result = $api->webhookEndpointsCreate($idempotencyKey, json_decode('{"url":"https://example.com/droomwork/hooks","event_types":["run.approved","run.executed"]}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/webhook_endpoints');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Content-Type: application/json',
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
  CURLOPT_POSTFIELDS => '{"url":"https://example.com/droomwork/hooks","event_types":["run.approved","run.executed"]}',
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.DeliveryApi;

ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
DeliveryApi api = new DeliveryApi(client);

var result = api.webhookEndpointsCreate(idempotencyKey, body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/webhook_endpoints"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", UUID.randomUUID().toString())
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "url": "https://example.com/droomwork/hooks",
          "event_types": [
            "run.approved",
            "run.executed"
          ]
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Heros.Sdk.Api;
using Heros.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new DELIVERYApi(config);

var result = api.WebhookEndpointsCreate(idempotencyKey, body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/webhook_endpoints");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
    {
      "url": "https://example.com/droomwork/hooks",
      "event_types": [
        "run.approved",
        "run.executed"
      ]
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
	"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)

result, _, err := client.DELIVERYAPI.WebhookEndpointsCreate(ctx).IdempotencyKey(key).DeliveryWebhookEndpointCreateRequest(body).Execute()
body := strings.NewReader(`{
  "url": "https://example.com/droomwork/hooks",
  "event_types": [
    "run.approved",
    "run.executed"
  ]
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/webhook_endpoints", body)
	req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "webhook_endpoint",
  "livemode": false,
  "mocked": false,
  "url": "https://files.sandbox.droomwork.com/example",
  "event_types": [
    "example"
  ],
  "secret_digests": [
    "sha256:9f2c1a4e7b0d3856"
  ],
  "created_at": "2026-09-01T09:00:00Z",
  "disabled_at": "2026-09-01T09:00:00Z",
  "secret": "whsec_3f9a1c7e2b8d4056a1e9c3f7b2d80456e1a9c3f7b2d80456"
}
GET/v1/webhook_endpoints/{id}#

Check one endpoint as it stands now

webhook.endpoints.retrieve

This reads one registered endpoint back as we hold it. So a check you run at startup can confirm the endpoint your code expects is the one we hold, and stop if it is not.

Send the endpoint's id in the path, and you get the url we deliver to, its event_types, and one digest for each active secret. disabled_at tells you whether we still deliver there. Another organisation's id answers not_found, not forbidden.

Match secret_digests against the digests you stored, and check every one. Right after a rotation, the digest for the secret your code verifies with is the second in the list, not the first.

An endpoint has to exist, from POST /v1/webhook_endpoints. You come here after a POST /v1/webhook_endpoints/{id}/rotate_secret or POST /v1/webhook_endpoints/{id}/disable made elsewhere in your system.

Path parameters

id string required

The endpoint's id, as returned when you registered it at POST /v1/webhook_endpoints or as it reads on each record from GET /v1/webhook_endpoints. It starts with whe_.

Returns

One endpoint in full, enough to decide whether to rotate its secret or register a replacement.

id string required

Assigned when you register the endpoint at POST /v1/webhook_endpoints; it starts with whe_ and never changes. Pass it as {id} on every /v1/webhook_endpoints/{id} call and as endpoint_id on GET /v1/webhook_deliveries.

object always "webhook_endpoint" required

Always webhook_endpoint. Tells you which kind of record you are looking at, so one handler can read any response.

livemode boolean required

False for a record in the sandbox realm, true for one in the live realm. This answers which data a record belongs to. mocked answers where its numbers came from, and the two are separate questions. A sandbox payslip computed by the real engine is livemode false and mocked false. A deferred capability in the live realm is livemode true and mocked true.

mocked boolean required

True when this response came from a mock, false when the engine computed the numbers. It stays true for a capability that is still on its documented mock, even in the live realm. It answers a different question from livemode: livemode says which realm you are in, this says where the numbers came from.

url string · uri required

The URL you registered, where we deliver. It must accept a POST and answer 2xx on success; to deliver somewhere else, register a new endpoint and disable this one.

event_types array of string required

Which events we deliver here, as you registered them, such as run.approved. A single * means every event.

secret_digests array of string required

One per active secret, newest first, at most two. A digest identifies which secret is which without being one.

disabled_at string · date-time · nullable optional

When you disabled this endpoint, as an RFC 3339 timestamp in UTC. null while it still receives deliveries.

created_at string · date-time required

When the record was created, as an RFC 3339 timestamp in UTC.

Other responses

401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, DELIVERYApi } from '@heros/sdk';

const api = new DELIVERYApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));

const result = await api.webhookEndpointsRetrieve({ id: 'whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import heros_sdk

config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.DELIVERYApi(client)

result = api.webhook_endpoints_retrieve(id='whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
    },
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

$config = HerosSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\DELIVERYApi(new GuzzleHttp\Client(), $config);

$result = $api->webhookEndpointsRetrieve(id: 'whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.DeliveryApi;

ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
DeliveryApi api = new DeliveryApi(client);

var result = api.webhookEndpointsRetrieve("whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Heros.Sdk.Api;
using Heros.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new DELIVERYApi(config);

var result = api.WebhookEndpointsRetrieve(id: "whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
	"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)

result, _, err := client.DELIVERYAPI.WebhookEndpointsRetrieve(ctx, "whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", nil)
	req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "webhook_endpoint",
  "livemode": false,
  "mocked": false,
  "url": "https://files.sandbox.droomwork.com/example",
  "event_types": [
    "example"
  ],
  "secret_digests": [
    "sha256:9f2c1a4e7b0d3856"
  ],
  "created_at": "2026-09-01T09:00:00Z",
  "disabled_at": "2026-09-01T09:00:00Z"
}
POST/v1/webhook_endpoints/{id}/rotate_secret#

Move to a new signing secret

webhook.endpoints.rotate_secret

This issues a new signing secret for one endpoint. So you can start moving off a leaked secret on the day you find it, and you do not have to register the endpoint again or point your events somewhere new.

Send the endpoint's id in the path. Send no request body. You get the whole endpoint back, with the new secret on it.

Only two secrets can be active, and a second rotation drops the older one. So deploy the new secret. Send a test event with POST /v1/webhook_endpoints/{id}/test and check your code verifies it. Then rotate again, and the old secret stops signing.

An endpoint has to exist first, from POST /v1/webhook_endpoints. GET /v1/webhook_endpoints/{id} says which secrets are still active.

Path parameters

id string required

The endpoint's id, as returned when you registered it at POST /v1/webhook_endpoints or as it reads on each record from GET /v1/webhook_endpoints. It starts with whe_.

Headers

Idempotency-Key string required

A key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.

Returns

The endpoint, with the new secret shown once.

id string required

Assigned when you register the endpoint at POST /v1/webhook_endpoints; it starts with whe_ and never changes. Pass it as {id} on every /v1/webhook_endpoints/{id} call and as endpoint_id on GET /v1/webhook_deliveries.

object always "webhook_endpoint" required

Always webhook_endpoint. Tells you which kind of record you are looking at, so one handler can read any response.

livemode boolean required

False for a record in the sandbox realm, true for one in the live realm. This answers which data a record belongs to. mocked answers where its numbers came from, and the two are separate questions. A sandbox payslip computed by the real engine is livemode false and mocked false. A deferred capability in the live realm is livemode true and mocked true.

mocked boolean required

True when this response came from a mock, false when the engine computed the numbers. It stays true for a capability that is still on its documented mock, even in the live realm. It answers a different question from livemode: livemode says which realm you are in, this says where the numbers came from.

url string · uri required

The URL you registered, where we deliver. It must accept a POST and answer 2xx on success; to deliver somewhere else, register a new endpoint and disable this one.

event_types array of string required

Which events we deliver here, as you registered them, such as run.approved. A single * means every event.

secret_digests array of string required

One per active secret, newest first, at most two. A digest identifies which secret is which without being one.

disabled_at string · date-time · nullable optional

When you disabled this endpoint, as an RFC 3339 timestamp in UTC. null while it still receives deliveries.

created_at string · date-time required

When the record was created, as an RFC 3339 timestamp in UTC.

secret string required

Shown here and never again. Keep it: it is what you verify a delivery's signature with.

Other responses

401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/rotate_secret" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, DELIVERYApi } from '@heros/sdk';

const api = new DELIVERYApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));

const result = await api.webhookEndpointsRotateSecret({ id: 'whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/rotate_secret', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const result = await response.json();
import os

import heros_sdk

config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.DELIVERYApi(client)

result = api.webhook_endpoints_rotate_secret(id='whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/rotate_secret',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

$config = HerosSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\DELIVERYApi(new GuzzleHttp\Client(), $config);

$result = $api->webhookEndpointsRotateSecret(id: 'whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/rotate_secret');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.DeliveryApi;

ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
DeliveryApi api = new DeliveryApi(client);

var result = api.webhookEndpointsRotateSecret("whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/rotate_secret"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .header("Idempotency-Key", UUID.randomUUID().toString())
    .method("POST", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Heros.Sdk.Api;
using Heros.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new DELIVERYApi(config);

var result = api.WebhookEndpointsRotateSecret(id: "whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/rotate_secret");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
	"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)

result, _, err := client.DELIVERYAPI.WebhookEndpointsRotateSecret(ctx, "whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/rotate_secret", nil)
	req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "webhook_endpoint",
  "livemode": false,
  "mocked": false,
  "url": "https://files.sandbox.droomwork.com/example",
  "event_types": [
    "example"
  ],
  "secret_digests": [
    "sha256:9f2c1a4e7b0d3856"
  ],
  "created_at": "2026-09-01T09:00:00Z",
  "disabled_at": "2026-09-01T09:00:00Z",
  "secret": "whsec_3f9a1c7e2b8d4056a1e9c3f7b2d80456e1a9c3f7b2d80456"
}
POST/v1/webhook_endpoints/{id}/disable#

Stop sending events to a URL

webhook.endpoints.disable

This stops deliveries to one endpoint you registered. We do not attempt the deliveries already waiting, and we queue no new ones. It's how your product retires a receiver.

Send the endpoint's id in the path. Send no request body. The 200 gives back the endpoint with disabled_at set.

Nothing here turns an endpoint back on. A disabled endpoint stays in GET /v1/webhook_endpoints and looks like a working one, so read disabled_at on each record it returns.

POST /v1/webhook_endpoints is where the id came from, and where you register a new endpoint if you still want these events. After this, GET /v1/webhook_deliveries still lists every delivery we made to this one.

Path parameters

id string required

The endpoint's id, as returned when you registered it at POST /v1/webhook_endpoints or as it reads on each record from GET /v1/webhook_endpoints. It starts with whe_.

Headers

Idempotency-Key string required

A key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.

Returns

The endpoint record, so you can confirm the url you stopped is the one you meant.

id string required

Assigned when you register the endpoint at POST /v1/webhook_endpoints; it starts with whe_ and never changes. Pass it as {id} on every /v1/webhook_endpoints/{id} call and as endpoint_id on GET /v1/webhook_deliveries.

object always "webhook_endpoint" required

Always webhook_endpoint. Tells you which kind of record you are looking at, so one handler can read any response.

livemode boolean required

False for a record in the sandbox realm, true for one in the live realm. This answers which data a record belongs to. mocked answers where its numbers came from, and the two are separate questions. A sandbox payslip computed by the real engine is livemode false and mocked false. A deferred capability in the live realm is livemode true and mocked true.

mocked boolean required

True when this response came from a mock, false when the engine computed the numbers. It stays true for a capability that is still on its documented mock, even in the live realm. It answers a different question from livemode: livemode says which realm you are in, this says where the numbers came from.

url string · uri required

The URL you registered, where we deliver. It must accept a POST and answer 2xx on success; to deliver somewhere else, register a new endpoint and disable this one.

event_types array of string required

Which events we deliver here, as you registered them, such as run.approved. A single * means every event.

secret_digests array of string required

One per active secret, newest first, at most two. A digest identifies which secret is which without being one.

disabled_at string · date-time · nullable optional

When you disabled this endpoint, as an RFC 3339 timestamp in UTC. null while it still receives deliveries.

created_at string · date-time required

When the record was created, as an RFC 3339 timestamp in UTC.

Other responses

401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disable" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, DELIVERYApi } from '@heros/sdk';

const api = new DELIVERYApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));

const result = await api.webhookEndpointsDisable({ id: 'whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disable', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const result = await response.json();
import os

import heros_sdk

config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.DELIVERYApi(client)

result = api.webhook_endpoints_disable(id='whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disable',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

$config = HerosSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\DELIVERYApi(new GuzzleHttp\Client(), $config);

$result = $api->webhookEndpointsDisable(id: 'whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disable');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.DeliveryApi;

ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
DeliveryApi api = new DeliveryApi(client);

var result = api.webhookEndpointsDisable("whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disable"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .header("Idempotency-Key", UUID.randomUUID().toString())
    .method("POST", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Heros.Sdk.Api;
using Heros.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new DELIVERYApi(config);

var result = api.WebhookEndpointsDisable(id: "whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disable");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
	"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)

result, _, err := client.DELIVERYAPI.WebhookEndpointsDisable(ctx, "whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disable", nil)
	req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "webhook_endpoint",
  "livemode": false,
  "mocked": false,
  "url": "https://files.sandbox.droomwork.com/example",
  "event_types": [
    "example"
  ],
  "secret_digests": [
    "sha256:9f2c1a4e7b0d3856"
  ],
  "created_at": "2026-09-01T09:00:00Z",
  "disabled_at": "2026-09-01T09:00:00Z"
}
POST/v1/webhook_endpoints/{id}/test#

Send a test event

webhook.endpoints.test

This sends one signed event to an endpoint you registered. Every other delivery waits for a module to raise an event, so this call is how you get one at your own URL whenever you choose, instead of waiting for a payroll run to finish.

Send the endpoint's id in the path. The body is optional: send a type, or none for payroll.run.completed. You get back the queued delivery, with its own id and is_test true.

202 means queued, not arrived: an attempt still has to reach your URL and answer 2xx.

A disabled endpoint answers 409.

POST /v1/webhook_endpoints comes first, for the {id} and the signing secret. Then GET /v1/webhook_deliveries/{id} hands you the exact bytes we signed.

Path parameters

id string required

The endpoint's id, as returned when you registered it at POST /v1/webhook_endpoints or as it reads on each record from GET /v1/webhook_endpoints. It starts with whe_.

Headers

Idempotency-Key string required

A key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.

Body optional

type string optional

The event type to send. Defaults to payroll.run.completed.

Returns

The queued test delivery, with the retry schedule we follow, so you know how long to watch.

id string required

Starts with whd_ and never changes. Read it off GET /v1/webhook_deliveries or POST /v1/webhook_endpoints/{id}/test, then pass it as {id} to GET /v1/webhook_deliveries/{id} and POST /v1/webhook_deliveries/{id}/replay.

object always "webhook_delivery" required

Always webhook_delivery. Tells you which kind of record you are looking at, so one handler can read any response.

livemode boolean required

False for a record in the sandbox realm, true for one in the live realm. This answers which data a record belongs to. mocked answers where its numbers came from, and the two are separate questions. A sandbox payslip computed by the real engine is livemode false and mocked false. A deferred capability in the live realm is livemode true and mocked true.

mocked boolean required

True when this response came from a mock, false when the engine computed the numbers. It stays true for a capability that is still on its documented mock, even in the live realm. It answers a different question from livemode: livemode says which realm you are in, this says where the numbers came from.

endpoint_id string required

The id of the endpoint this delivery goes to, from POST /v1/webhook_endpoints or GET /v1/webhook_endpoints; it starts with whe_. Pass it as endpoint_id to GET /v1/webhook_deliveries to list every delivery to that endpoint.

event_id string required

The id of the event this delivery carries. It starts with evt_ and matches the id in the delivered body's envelope and on the events list of the module that raised it, such as GET /v1/payroll/events; a replay carries the same id.

event_type string required

The event's type, as in the envelope, such as run.approved. On a test delivery it is the type you sent, or payroll.run.completed when you sent none.

sequence integer required

Monotonic per endpoint, from one, with no gaps. A gap means you missed a delivery, and replay is how you get it.

attempts integer required

How many have been made, including the one in flight.

last_status integer · nullable optional

The HTTP status of the most recent attempt, where there was one.

last_fault string · nullable optional

Why the most recent attempt produced no status: a refused connection, a DNS failure, a timeout.

next_attempt_at string · date-time · nullable optional

Null once the delivery has succeeded or run out of attempts.

delivered_at string · date-time · nullable optional

When an attempt got a 2xx from you, as an RFC 3339 timestamp in UTC. null until one has; a 2xx stops the retries, and only a replay starts them again.

failed_at string · date-time · nullable optional

Set after the last attempt. The delivery stays available for replay.

is_test boolean required

true when this delivery came from POST /v1/webhook_endpoints/{id}/test and not from a real event. Same envelope, signature and schedule; only this flag differs.

retry_schedule_seconds array of integer required

The published schedule, so you can read it off instead of measuring it.

created_at string · date-time required

When the record was created, as an RFC 3339 timestamp in UTC.

Other responses

401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.
409The record is not in a state that allows this.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/test" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"type":"payroll.run.completed"}'
import { Configuration, DELIVERYApi } from '@heros/sdk';

const api = new DELIVERYApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));

const result = await api.webhookEndpointsTest({
  id: 'whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
  idempotencyKey: crypto.randomUUID(),
  deliveryWebhookTestRequest: {"type":"payroll.run.completed"},
});
const response = await fetch('https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/test', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    "type": "payroll.run.completed"
  }),
});
const result = await response.json();
import os

import heros_sdk

config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.DELIVERYApi(client)

result = api.webhook_endpoints_test(id='whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"type": "payroll.run.completed"})
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/test',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={"type": "payroll.run.completed"},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

$config = HerosSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\DELIVERYApi(new GuzzleHttp\Client(), $config);

$result = $api->webhookEndpointsTest($idempotencyKey, json_decode('{"type":"payroll.run.completed"}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/test');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Content-Type: application/json',
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
  CURLOPT_POSTFIELDS => '{"type":"payroll.run.completed"}',
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.DeliveryApi;

ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
DeliveryApi api = new DeliveryApi(client);

var result = api.webhookEndpointsTest("whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/test"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", UUID.randomUUID().toString())
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "type": "payroll.run.completed"
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Heros.Sdk.Api;
using Heros.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new DELIVERYApi(config);

var result = api.WebhookEndpointsTest(id: "whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/test");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
    {
      "type": "payroll.run.completed"
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
	"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)

result, _, err := client.DELIVERYAPI.WebhookEndpointsTest(ctx, "whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).DeliveryWebhookTestRequest(body).Execute()
body := strings.NewReader(`{
  "type": "payroll.run.completed"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/webhook_endpoints/whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/test", body)
	req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "webhook_delivery",
  "livemode": false,
  "mocked": false,
  "endpoint_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "event_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "event_type": "run.approved",
  "sequence": 1,
  "attempts": 1,
  "is_test": true,
  "retry_schedule_seconds": [
    1
  ],
  "created_at": "2026-09-01T09:00:00Z",
  "last_status": 1,
  "last_fault": "The register did not answer within the timeout.",
  "next_attempt_at": "2026-09-01T09:00:00Z",
  "delivered_at": "2026-09-01T09:00:00Z",
  "failed_at": "2026-09-01T09:00:00Z"
}
GET/v1/webhook_deliveries#

See every delivery we made to your endpoints

webhook.deliveries.list

This lists every delivery we made to your endpoints, newest first, and how each one went. event_id is the same id your receiver read in the delivered body, so a nightly job can name every event your log is missing.

Pass endpoint_id for one endpoint, or leave it out for all of them. Each row carries attempts, last_status and next_attempt_at, so one read tells you what is still being retried.

delivered_at stays null until an attempt gets a 2xx from you, and null is not failure. failed_at says the attempts ran out.

Nothing has to exist before this call, and it answers with nothing until a delivery has been made. After this, GET /v1/webhook_deliveries/{id} adds the signed bytes and the signature header this list leaves out, and POST /v1/webhook_deliveries/{id}/replay queues a delivery again.

Query parameters

endpoint_id string optional

Return only deliveries to one endpoint, by its id as returned from POST /v1/webhook_endpoints or listed at GET /v1/webhook_endpoints; it starts with whe_. Leave it out to get deliveries to every endpoint you have registered.

Returns

The deliveries made to your endpoints, with the test ones marked so your reconciliation can skip them.

object always "list" required

Always list. Tells you which kind of record you are looking at, so one handler can read any response.

data array of WebhookDelivery required

The records on this page, in the order the list promises. Empty when nothing matched.

17 fields of WebhookDelivery
id string required

Starts with whd_ and never changes. Read it off GET /v1/webhook_deliveries or POST /v1/webhook_endpoints/{id}/test, then pass it as {id} to GET /v1/webhook_deliveries/{id} and POST /v1/webhook_deliveries/{id}/replay.

object always "webhook_delivery" required

Always webhook_delivery. Tells you which kind of record you are looking at, so one handler can read any response.

livemode boolean required

False for a record in the sandbox realm, true for one in the live realm. This answers which data a record belongs to. mocked answers where its numbers came from, and the two are separate questions. A sandbox payslip computed by the real engine is livemode false and mocked false. A deferred capability in the live realm is livemode true and mocked true.

mocked boolean required

True when this response came from a mock, false when the engine computed the numbers. It stays true for a capability that is still on its documented mock, even in the live realm. It answers a different question from livemode: livemode says which realm you are in, this says where the numbers came from.

endpoint_id string required

The id of the endpoint this delivery goes to, from POST /v1/webhook_endpoints or GET /v1/webhook_endpoints; it starts with whe_. Pass it as endpoint_id to GET /v1/webhook_deliveries to list every delivery to that endpoint.

event_id string required

The id of the event this delivery carries. It starts with evt_ and matches the id in the delivered body's envelope and on the events list of the module that raised it, such as GET /v1/payroll/events; a replay carries the same id.

event_type string required

The event's type, as in the envelope, such as run.approved. On a test delivery it is the type you sent, or payroll.run.completed when you sent none.

sequence integer required

Monotonic per endpoint, from one, with no gaps. A gap means you missed a delivery, and replay is how you get it.

attempts integer required

How many have been made, including the one in flight.

last_status integer · nullable optional

The HTTP status of the most recent attempt, where there was one.

last_fault string · nullable optional

Why the most recent attempt produced no status: a refused connection, a DNS failure, a timeout.

next_attempt_at string · date-time · nullable optional

Null once the delivery has succeeded or run out of attempts.

delivered_at string · date-time · nullable optional

When an attempt got a 2xx from you, as an RFC 3339 timestamp in UTC. null until one has; a 2xx stops the retries, and only a replay starts them again.

failed_at string · date-time · nullable optional

Set after the last attempt. The delivery stays available for replay.

is_test boolean required

true when this delivery came from POST /v1/webhook_endpoints/{id}/test and not from a real event. Same envelope, signature and schedule; only this flag differs.

retry_schedule_seconds array of integer required

The published schedule, so you can read it off instead of measuring it.

created_at string · date-time required

When the record was created, as an RFC 3339 timestamp in UTC.

has_more boolean required

true when there are more records after this page. Pass the last record's id as starting_after to get the next page.

Other responses

401No valid credential was presented.
403The credential does not carry the required scope.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: endpoint_id (optional)
curl -X GET "https://sandbox.droomwork.io/v1/webhook_deliveries?endpoint_id=whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, DELIVERYApi } from '@heros/sdk';

const api = new DELIVERYApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));

// query parameters: endpoint_id (optional)
const result = await api.webhookDeliveriesList({ endpointId: 'whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
// query parameters: endpoint_id (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/webhook_deliveries?endpoint_id=whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import heros_sdk

config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.DELIVERYApi(client)

# query parameters: endpoint_id (optional)
result = api.webhook_deliveries_list(endpoint_id='whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

# query parameters: endpoint_id (optional)
response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/webhook_deliveries?endpoint_id=whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
    },
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

$config = HerosSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\DELIVERYApi(new GuzzleHttp\Client(), $config);

# query parameters: endpoint_id (optional)
$result = $api->webhookDeliveriesList(endpoint_id: 'whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
// query parameters: endpoint_id (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/webhook_deliveries?endpoint_id=whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.DeliveryApi;

ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
DeliveryApi api = new DeliveryApi(client);

// query parameters: endpoint_id (optional)
var result = api.webhookDeliveriesList("whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
// query parameters: endpoint_id (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/webhook_deliveries?endpoint_id=whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Heros.Sdk.Api;
using Heros.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new DELIVERYApi(config);

// query parameters: endpoint_id (optional)
var result = api.WebhookDeliveriesList(endpointId: "whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
// query parameters: endpoint_id (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/webhook_deliveries?endpoint_id=whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
	"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)

// query parameters: endpoint_id (optional)
result, _, err := client.DELIVERYAPI.WebhookDeliveriesList(ctx).EndpointId("whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
// query parameters: endpoint_id (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/webhook_deliveries?endpoint_id=whe_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", nil)
	req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "list",
  "data": [
    {
      "id": "whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "webhook_delivery",
      "livemode": false,
      "mocked": false,
      "endpoint_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "event_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "event_type": "run.approved",
      "sequence": 1,
      "attempts": 1,
      "is_test": true,
      "retry_schedule_seconds": [
        1
      ],
      "created_at": "2026-09-01T09:00:00Z",
      "last_status": 1,
      "last_fault": "The register did not answer within the timeout.",
      "next_attempt_at": "2026-09-01T09:00:00Z",
      "delivered_at": "2026-09-01T09:00:00Z",
      "failed_at": "2026-09-01T09:00:00Z"
    }
  ],
  "has_more": true
}
GET/v1/webhook_deliveries/{id}#

Check the bytes we signed and sent

webhook.deliveries.retrieve

This reads one delivery back. It's the only call that returns signed_body, the exact bytes we signed. So you can test your verification code on a delivery we really made: your digest matches ours, or the bug is yours.

Send the delivery's id in the path. You get 200, the whole record, and both signed_body and signature_header to check it with.

A delivery must exist first. POST /v1/webhook_endpoints then POST /v1/webhook_endpoints/{id}/test makes one on demand; GET /v1/webhook_deliveries lists every delivery already made. After this, POST /v1/webhook_deliveries/{id}/replay sends the same bytes again.

Path parameters

id string required

The delivery's id, as it reads on each record from GET /v1/webhook_deliveries or on the response to POST /v1/webhook_endpoints/{id}/test. It starts with whd_.

Returns

The delivery, with created_at, so you can compare when we recorded it against when your handler saw it.

id string required

Starts with whd_ and never changes. Read it off GET /v1/webhook_deliveries or POST /v1/webhook_endpoints/{id}/test, then pass it as {id} to GET /v1/webhook_deliveries/{id} and POST /v1/webhook_deliveries/{id}/replay.

object always "webhook_delivery" required

Always webhook_delivery. Tells you which kind of record you are looking at, so one handler can read any response.

livemode boolean required

False for a record in the sandbox realm, true for one in the live realm. This answers which data a record belongs to. mocked answers where its numbers came from, and the two are separate questions. A sandbox payslip computed by the real engine is livemode false and mocked false. A deferred capability in the live realm is livemode true and mocked true.

mocked boolean required

True when this response came from a mock, false when the engine computed the numbers. It stays true for a capability that is still on its documented mock, even in the live realm. It answers a different question from livemode: livemode says which realm you are in, this says where the numbers came from.

endpoint_id string required

The id of the endpoint this delivery goes to, from POST /v1/webhook_endpoints or GET /v1/webhook_endpoints; it starts with whe_. Pass it as endpoint_id to GET /v1/webhook_deliveries to list every delivery to that endpoint.

event_id string required

The id of the event this delivery carries. It starts with evt_ and matches the id in the delivered body's envelope and on the events list of the module that raised it, such as GET /v1/payroll/events; a replay carries the same id.

event_type string required

The event's type, as in the envelope, such as run.approved. On a test delivery it is the type you sent, or payroll.run.completed when you sent none.

sequence integer required

Monotonic per endpoint, from one, with no gaps. A gap means you missed a delivery, and replay is how you get it.

attempts integer required

How many have been made, including the one in flight.

last_status integer · nullable optional

The HTTP status of the most recent attempt, where there was one.

last_fault string · nullable optional

Why the most recent attempt produced no status: a refused connection, a DNS failure, a timeout.

next_attempt_at string · date-time · nullable optional

Null once the delivery has succeeded or run out of attempts.

delivered_at string · date-time · nullable optional

When an attempt got a 2xx from you, as an RFC 3339 timestamp in UTC. null until one has; a 2xx stops the retries, and only a replay starts them again.

failed_at string · date-time · nullable optional

Set after the last attempt. The delivery stays available for replay.

is_test boolean required

true when this delivery came from POST /v1/webhook_endpoints/{id}/test and not from a real event. Same envelope, signature and schedule; only this flag differs.

retry_schedule_seconds array of integer required

The published schedule, so you can read it off instead of measuring it.

created_at string · date-time required

When the record was created, as an RFC 3339 timestamp in UTC.

signed_body string optional

The exact bytes that were signed and sent.

signature_header string · nullable optional

The Droomwork-Signature sent on the most recent attempt. Null before the first one. Each attempt signs its own timestamp, so there is no single signature for a delivery.

Other responses

401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/webhook_deliveries/whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, DELIVERYApi } from '@heros/sdk';

const api = new DELIVERYApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));

const result = await api.webhookDeliveriesRetrieve({ id: 'whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/webhook_deliveries/whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import heros_sdk

config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.DELIVERYApi(client)

result = api.webhook_deliveries_retrieve(id='whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/webhook_deliveries/whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
    },
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

$config = HerosSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\DELIVERYApi(new GuzzleHttp\Client(), $config);

$result = $api->webhookDeliveriesRetrieve(id: 'whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/webhook_deliveries/whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.DeliveryApi;

ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
DeliveryApi api = new DeliveryApi(client);

var result = api.webhookDeliveriesRetrieve("whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/webhook_deliveries/whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Heros.Sdk.Api;
using Heros.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new DELIVERYApi(config);

var result = api.WebhookDeliveriesRetrieve(id: "whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/webhook_deliveries/whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
	"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)

result, _, err := client.DELIVERYAPI.WebhookDeliveriesRetrieve(ctx, "whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/webhook_deliveries/whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", nil)
	req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "webhook_delivery",
  "livemode": false,
  "mocked": false,
  "endpoint_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "event_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "event_type": "run.approved",
  "sequence": 1,
  "attempts": 1,
  "is_test": true,
  "retry_schedule_seconds": [
    1
  ],
  "created_at": "2026-09-01T09:00:00Z",
  "last_status": 1,
  "last_fault": "The register did not answer within the timeout.",
  "next_attempt_at": "2026-09-01T09:00:00Z",
  "delivered_at": "2026-09-01T09:00:00Z",
  "failed_at": "2026-09-01T09:00:00Z",
  "signed_body": "example",
  "signature_header": "example"
}
POST/v1/webhook_deliveries/{id}/replay#

Send a missed delivery again

webhook.deliveries.replay

This puts one delivery back on the queue, with the whole schedule ahead of it again.

Send the delivery's id in the path. Send no request body. The 202 hands back that delivery, with next_attempt_at set.

The 202 means queued, not arrived: an attempt still has to reach your URL and answer 2xx. The event_id repeats, so your duplicate check may drop it. You get the same signed bytes, with a new signature over them.

Call POST /v1/webhook_endpoints first; this needs a delivery to it, from a real event or from POST /v1/webhook_endpoints/{id}/test. GET /v1/webhook_deliveries is where you see what to replay. Then GET /v1/webhook_deliveries/{id} shows how the latest attempt went.

Path parameters

id string required

The delivery's id, as it reads on each record from GET /v1/webhook_deliveries or on the response to POST /v1/webhook_endpoints/{id}/test. It starts with whd_.

Headers

Idempotency-Key string required

A key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.

Returns

That delivery, with created_at, so you can tell how old the delivery you are replaying is.

id string required

Starts with whd_ and never changes. Read it off GET /v1/webhook_deliveries or POST /v1/webhook_endpoints/{id}/test, then pass it as {id} to GET /v1/webhook_deliveries/{id} and POST /v1/webhook_deliveries/{id}/replay.

object always "webhook_delivery" required

Always webhook_delivery. Tells you which kind of record you are looking at, so one handler can read any response.

livemode boolean required

False for a record in the sandbox realm, true for one in the live realm. This answers which data a record belongs to. mocked answers where its numbers came from, and the two are separate questions. A sandbox payslip computed by the real engine is livemode false and mocked false. A deferred capability in the live realm is livemode true and mocked true.

mocked boolean required

True when this response came from a mock, false when the engine computed the numbers. It stays true for a capability that is still on its documented mock, even in the live realm. It answers a different question from livemode: livemode says which realm you are in, this says where the numbers came from.

endpoint_id string required

The id of the endpoint this delivery goes to, from POST /v1/webhook_endpoints or GET /v1/webhook_endpoints; it starts with whe_. Pass it as endpoint_id to GET /v1/webhook_deliveries to list every delivery to that endpoint.

event_id string required

The id of the event this delivery carries. It starts with evt_ and matches the id in the delivered body's envelope and on the events list of the module that raised it, such as GET /v1/payroll/events; a replay carries the same id.

event_type string required

The event's type, as in the envelope, such as run.approved. On a test delivery it is the type you sent, or payroll.run.completed when you sent none.

sequence integer required

Monotonic per endpoint, from one, with no gaps. A gap means you missed a delivery, and replay is how you get it.

attempts integer required

How many have been made, including the one in flight.

last_status integer · nullable optional

The HTTP status of the most recent attempt, where there was one.

last_fault string · nullable optional

Why the most recent attempt produced no status: a refused connection, a DNS failure, a timeout.

next_attempt_at string · date-time · nullable optional

Null once the delivery has succeeded or run out of attempts.

delivered_at string · date-time · nullable optional

When an attempt got a 2xx from you, as an RFC 3339 timestamp in UTC. null until one has; a 2xx stops the retries, and only a replay starts them again.

failed_at string · date-time · nullable optional

Set after the last attempt. The delivery stays available for replay.

is_test boolean required

true when this delivery came from POST /v1/webhook_endpoints/{id}/test and not from a real event. Same envelope, signature and schedule; only this flag differs.

retry_schedule_seconds array of integer required

The published schedule, so you can read it off instead of measuring it.

created_at string · date-time required

When the record was created, as an RFC 3339 timestamp in UTC.

Other responses

401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/webhook_deliveries/whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/replay" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, DELIVERYApi } from '@heros/sdk';

const api = new DELIVERYApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));

const result = await api.webhookDeliveriesReplay({ id: 'whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/webhook_deliveries/whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/replay', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const result = await response.json();
import os

import heros_sdk

config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.DELIVERYApi(client)

result = api.webhook_deliveries_replay(id='whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/webhook_deliveries/whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/replay',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

$config = HerosSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\DELIVERYApi(new GuzzleHttp\Client(), $config);

$result = $api->webhookDeliveriesReplay(id: 'whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/webhook_deliveries/whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/replay');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.DeliveryApi;

ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
DeliveryApi api = new DeliveryApi(client);

var result = api.webhookDeliveriesReplay("whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/webhook_deliveries/whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/replay"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .header("Idempotency-Key", UUID.randomUUID().toString())
    .method("POST", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Heros.Sdk.Api;
using Heros.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new DELIVERYApi(config);

var result = api.WebhookDeliveriesReplay(id: "whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/webhook_deliveries/whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/replay");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
	"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)

result, _, err := client.DELIVERYAPI.WebhookDeliveriesReplay(ctx, "whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/webhook_deliveries/whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/replay", nil)
	req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "whd_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "webhook_delivery",
  "livemode": false,
  "mocked": false,
  "endpoint_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "event_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "event_type": "run.approved",
  "sequence": 1,
  "attempts": 1,
  "is_test": true,
  "retry_schedule_seconds": [
    1
  ],
  "created_at": "2026-09-01T09:00:00Z",
  "last_status": 1,
  "last_fault": "The register did not answer within the timeout.",
  "next_attempt_at": "2026-09-01T09:00:00Z",
  "delivered_at": "2026-09-01T09:00:00Z",
  "failed_at": "2026-09-01T09:00:00Z"
}