Version 1.0.0
Droomwork DELIVERY
Webhook endpoints and the deliveries made to them.
https://sandbox.droomwork.ioDroomwork-Api-Key: dw_test_… or Authorization: Bearer dw_test_…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. Reject anything older than five minutes 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 over roughly a day, at 10s, 1m, 5m, 30m, 2h, 5h, 10h and 20h. A 2xx from you stops them; anything else continues. 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 mint 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 and nothing else about it differs.
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?
/v1/webhook_endpoints#List webhook endpoints
webhook.endpoints.list
The endpoints you have registered.
Returns
A page of endpoints.
object
always "list"
requiredAlways list. Tells you which kind of record you are looking at, so one handler can read any response.
data
array of WebhookEndpoint
requiredThe records on this page, in the order the list promises. Empty when nothing matched.
9 fields of WebhookEndpoint
id
string
requiredAssigned when you register the endpoint at POST /v1/webhook_endpoints; it starts with webhook_endpoint_ 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"
requiredAlways webhook_endpoint. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredFalse 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. See DEC-068 and ADR-0011.
mocked
boolean
requiredTrue when this response came from a mock rather than from a calculation, and false when the engine computed it. Required by M1 row 10.
It stays true for a capability that is deferred by section 3 of the contract, even in the live realm, because that capability is still on its documented mock. It is deliberately not called sandbox: sandbox is the name of a realm, and livemode is the field that says which realm you are in. See ADR-0011.
url
string · uri
requiredThe 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
requiredWhich events we deliver here, as you registered them, such as run.approved. A single * means every event.
secret_digests
array of string
requiredOne per active secret, newest first, at most two. A digest identifies which secret is which without being one.
disabled_at
string · date-time · nullable
optionalWhen you disabled this endpoint, as an RFC 3339 timestamp in UTC. null while it still receives deliveries.
created_at
string · date-time
requiredWhen the record was created, as an RFC 3339 timestamp in UTC.
has_more
boolean
requiredtrue when there are more records after this page. Pass the last record's id as starting_after to get the next page.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/webhook_endpoints" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, DELIVERYApi } from '@droomwork/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 droomwork
config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.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 droomwork "github.com/fenibofubara/droomwork-sdk-go"
ctx := context.WithValue(context.Background(), droomwork.ContextAPIKeys, map[string]droomwork.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := droomwork.NewConfiguration()
cfg.Servers = droomwork.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := droomwork.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)
{
"object": "list",
"data": [
{
"id": "webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "webhook_endpoint",
"livemode": true,
"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
}
/v1/webhook_endpoints#Register a webhook endpoint
webhook.endpoints.create
Register the URL you want deliveries sent to. You get back the endpoint and its first signing secret.
The secret is in this response and in no other. Keep it: what you can read back afterwards is a digest that tells you which secret is which, not the secret itself.
Headers
Idempotency-Key
string
requiredA 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
requiredWhere to deliver. Must accept a POST and answer 2xx on success.
event_types
array of string
optionalWhich events to deliver. A single * means everything, which is the default and is what most integrations want to start with.
Returns
The endpoint, with its secret shown once.
id
string
requiredAssigned when you register the endpoint at POST /v1/webhook_endpoints; it starts with webhook_endpoint_ 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"
requiredAlways webhook_endpoint. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredFalse 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. See DEC-068 and ADR-0011.
mocked
boolean
requiredTrue when this response came from a mock rather than from a calculation, and false when the engine computed it. Required by M1 row 10.
It stays true for a capability that is deferred by section 3 of the contract, even in the live realm, because that capability is still on its documented mock. It is deliberately not called sandbox: sandbox is the name of a realm, and livemode is the field that says which realm you are in. See ADR-0011.
url
string · uri
requiredThe 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
requiredWhich events we deliver here, as you registered them, such as run.approved. A single * means every event.
secret_digests
array of string
requiredOne per active secret, newest first, at most two. A digest identifies which secret is which without being one.
disabled_at
string · date-time · nullable
optionalWhen you disabled this endpoint, as an RFC 3339 timestamp in UTC. null while it still receives deliveries.
created_at
string · date-time
requiredWhen the record was created, as an RFC 3339 timestamp in UTC.
secret
string
requiredShown here and never again. Keep it: it is what you verify a delivery's signature with.
Other responses
Errors it can return
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 '@droomwork/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 droomwork
config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.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 droomwork "github.com/fenibofubara/droomwork-sdk-go"
ctx := context.WithValue(context.Background(), droomwork.ContextAPIKeys, map[string]droomwork.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := droomwork.NewConfiguration()
cfg.Servers = droomwork.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := droomwork.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)
{
"id": "webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "webhook_endpoint",
"livemode": true,
"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"
}
/v1/webhook_endpoints/{id}#Retrieve a webhook endpoint
webhook.endpoints.retrieve
One endpoint. Ask for one that isn't yours and you get not_found, not forbidden.
Path parameters
id
string
requiredThe 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 webhook_endpoint_.
Returns
The endpoint.
id
string
requiredAssigned when you register the endpoint at POST /v1/webhook_endpoints; it starts with webhook_endpoint_ 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"
requiredAlways webhook_endpoint. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredFalse 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. See DEC-068 and ADR-0011.
mocked
boolean
requiredTrue when this response came from a mock rather than from a calculation, and false when the engine computed it. Required by M1 row 10.
It stays true for a capability that is deferred by section 3 of the contract, even in the live realm, because that capability is still on its documented mock. It is deliberately not called sandbox: sandbox is the name of a realm, and livemode is the field that says which realm you are in. See ADR-0011.
url
string · uri
requiredThe 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
requiredWhich events we deliver here, as you registered them, such as run.approved. A single * means every event.
secret_digests
array of string
requiredOne per active secret, newest first, at most two. A digest identifies which secret is which without being one.
disabled_at
string · date-time · nullable
optionalWhen you disabled this endpoint, as an RFC 3339 timestamp in UTC. null while it still receives deliveries.
created_at
string · date-time
requiredWhen the record was created, as an RFC 3339 timestamp in UTC.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, DELIVERYApi } from '@droomwork/sdk';
const api = new DELIVERYApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.webhookEndpointsRetrieve({ id: 'webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import droomwork
config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.DELIVERYApi(client)
result = api.webhook_endpoints_retrieve(id='webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = DroomworkSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\Api\DELIVERYApi(new GuzzleHttp\Client(), $config);
$result = $api->webhookEndpointsRetrieve(id: 'webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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("webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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 Droomwork.Sdk.Api;
using Droomwork.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: "webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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 droomwork "github.com/fenibofubara/droomwork-sdk-go"
ctx := context.WithValue(context.Background(), droomwork.ContextAPIKeys, map[string]droomwork.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := droomwork.NewConfiguration()
cfg.Servers = droomwork.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := droomwork.NewAPIClient(cfg)
result, _, err := client.DELIVERYAPI.WebhookEndpointsRetrieve(ctx, "webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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)
{
"id": "webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "webhook_endpoint",
"livemode": true,
"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"
}
/v1/webhook_endpoints/{id}/rotate_secret#Rotate the signing secret
webhook.endpoints.rotate_secret
Issue a new signing secret. The previous one stays active.
We sign deliveries with both until you rotate again, so nothing is dropped while you move. Move your verification to the new secret, then rotate once more to retire the old one.
The new secret is in this response and in no other.
Path parameters
id
string
requiredThe 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 webhook_endpoint_.
Headers
Idempotency-Key
string
requiredA 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
requiredAssigned when you register the endpoint at POST /v1/webhook_endpoints; it starts with webhook_endpoint_ 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"
requiredAlways webhook_endpoint. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredFalse 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. See DEC-068 and ADR-0011.
mocked
boolean
requiredTrue when this response came from a mock rather than from a calculation, and false when the engine computed it. Required by M1 row 10.
It stays true for a capability that is deferred by section 3 of the contract, even in the live realm, because that capability is still on its documented mock. It is deliberately not called sandbox: sandbox is the name of a realm, and livemode is the field that says which realm you are in. See ADR-0011.
url
string · uri
requiredThe 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
requiredWhich events we deliver here, as you registered them, such as run.approved. A single * means every event.
secret_digests
array of string
requiredOne per active secret, newest first, at most two. A digest identifies which secret is which without being one.
disabled_at
string · date-time · nullable
optionalWhen you disabled this endpoint, as an RFC 3339 timestamp in UTC. null while it still receives deliveries.
created_at
string · date-time
requiredWhen the record was created, as an RFC 3339 timestamp in UTC.
secret
string
requiredShown here and never again. Keep it: it is what you verify a delivery's signature with.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/rotate_secret" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"import { Configuration, DELIVERYApi } from '@droomwork/sdk';
const api = new DELIVERYApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.webhookEndpointsRotateSecret({ id: 'webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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 droomwork
config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.DELIVERYApi(client)
result = api.webhook_endpoints_rotate_secret(id='webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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 = DroomworkSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\Api\DELIVERYApi(new GuzzleHttp\Client(), $config);
$result = $api->webhookEndpointsRotateSecret(id: 'webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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("webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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 Droomwork.Sdk.Api;
using Droomwork.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: "webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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 droomwork "github.com/fenibofubara/droomwork-sdk-go"
ctx := context.WithValue(context.Background(), droomwork.ContextAPIKeys, map[string]droomwork.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := droomwork.NewConfiguration()
cfg.Servers = droomwork.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := droomwork.NewAPIClient(cfg)
result, _, err := client.DELIVERYAPI.WebhookEndpointsRotateSecret(ctx, "webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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)
{
"id": "webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "webhook_endpoint",
"livemode": true,
"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"
}
/v1/webhook_endpoints/{id}/disable#Disable a webhook endpoint
webhook.endpoints.disable
Stop delivery to this endpoint. Queued deliveries won't be attempted and no new ones are accepted. Deliveries already made stay readable.
Path parameters
id
string
requiredThe 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 webhook_endpoint_.
Headers
Idempotency-Key
string
requiredA 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 disabled endpoint.
id
string
requiredAssigned when you register the endpoint at POST /v1/webhook_endpoints; it starts with webhook_endpoint_ 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"
requiredAlways webhook_endpoint. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredFalse 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. See DEC-068 and ADR-0011.
mocked
boolean
requiredTrue when this response came from a mock rather than from a calculation, and false when the engine computed it. Required by M1 row 10.
It stays true for a capability that is deferred by section 3 of the contract, even in the live realm, because that capability is still on its documented mock. It is deliberately not called sandbox: sandbox is the name of a realm, and livemode is the field that says which realm you are in. See ADR-0011.
url
string · uri
requiredThe 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
requiredWhich events we deliver here, as you registered them, such as run.approved. A single * means every event.
secret_digests
array of string
requiredOne per active secret, newest first, at most two. A digest identifies which secret is which without being one.
disabled_at
string · date-time · nullable
optionalWhen you disabled this endpoint, as an RFC 3339 timestamp in UTC. null while it still receives deliveries.
created_at
string · date-time
requiredWhen the record was created, as an RFC 3339 timestamp in UTC.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disable" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"import { Configuration, DELIVERYApi } from '@droomwork/sdk';
const api = new DELIVERYApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.webhookEndpointsDisable({ id: 'webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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 droomwork
config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.DELIVERYApi(client)
result = api.webhook_endpoints_disable(id='webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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 = DroomworkSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\Api\DELIVERYApi(new GuzzleHttp\Client(), $config);
$result = $api->webhookEndpointsDisable(id: 'webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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("webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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 Droomwork.Sdk.Api;
using Droomwork.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: "webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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 droomwork "github.com/fenibofubara/droomwork-sdk-go"
ctx := context.WithValue(context.Background(), droomwork.ContextAPIKeys, map[string]droomwork.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := droomwork.NewConfiguration()
cfg.Servers = droomwork.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := droomwork.NewAPIClient(cfg)
result, _, err := client.DELIVERYAPI.WebhookEndpointsDisable(ctx, "webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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)
{
"id": "webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "webhook_endpoint",
"livemode": true,
"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"
}
/v1/webhook_endpoints/{id}/test#Send a test event
webhook.endpoints.test
Send a test event to this endpoint. It goes through the same envelope, signature and retry schedule a real event uses. It's marked is_test so you can tell it apart, and nothing else about it differs.
You get 202 once the delivery is queued. Read the delivery back to see whether it arrived.
Path parameters
id
string
requiredThe 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 webhook_endpoint_.
Headers
Idempotency-Key
string
requiredA 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
optionalThe event type to send. Defaults to payroll.run.completed.
Returns
The queued delivery.
id
string
requiredStarts with webhook_delivery_ 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"
requiredAlways webhook_delivery. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredFalse 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. See DEC-068 and ADR-0011.
mocked
boolean
requiredTrue when this response came from a mock rather than from a calculation, and false when the engine computed it. Required by M1 row 10.
It stays true for a capability that is deferred by section 3 of the contract, even in the live realm, because that capability is still on its documented mock. It is deliberately not called sandbox: sandbox is the name of a realm, and livemode is the field that says which realm you are in. See ADR-0011.
endpoint_id
string
requiredThe id of the endpoint this delivery goes to, from POST /v1/webhook_endpoints or GET /v1/webhook_endpoints; it starts with webhook_endpoint_. Pass it as endpoint_id to GET /v1/webhook_deliveries to list every delivery to that endpoint.
event_id
string
requiredThe 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
requiredThe 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
requiredMonotonic per endpoint, from one, with no gaps. A gap means you missed a delivery, and replay is how you get it.
attempts
integer
requiredHow many have been made, including the one in flight.
last_status
integer · nullable
optionalThe HTTP status of the most recent attempt, where there was one.
last_fault
string · nullable
optionalWhy the most recent attempt produced no status: a refused connection, a DNS failure, a timeout.
next_attempt_at
string · date-time · nullable
optionalNull once the delivery has succeeded or run out of attempts.
delivered_at
string · date-time · nullable
optionalWhen 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
optionalSet after the last attempt. The delivery stays available for replay.
is_test
boolean
requiredtrue when this delivery came from POST /v1/webhook_endpoints/{id}/test rather than a real event. Same envelope, signature and schedule; only this flag differs.
retry_schedule_seconds
array of integer
requiredThe published schedule, so you can reason about it rather than measure it.
created_at
string · date-time
requiredWhen the record was created, as an RFC 3339 timestamp in UTC.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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 '@droomwork/sdk';
const api = new DELIVERYApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.webhookEndpointsTest({
id: 'webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
idempotencyKey: crypto.randomUUID(),
deliveryWebhookTestRequest: {"type":"payroll.run.completed"},
});const response = await fetch('https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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 droomwork
config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.DELIVERYApi(client)
result = api.webhook_endpoints_test(id='webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"type": "payroll.run.completed"})import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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 = DroomworkSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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/webhook_endpoint_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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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("webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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 Droomwork.Sdk.Api;
using Droomwork.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: "webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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 droomwork "github.com/fenibofubara/droomwork-sdk-go"
ctx := context.WithValue(context.Background(), droomwork.ContextAPIKeys, map[string]droomwork.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := droomwork.NewConfiguration()
cfg.Servers = droomwork.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := droomwork.NewAPIClient(cfg)
result, _, err := client.DELIVERYAPI.WebhookEndpointsTest(ctx, "webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).DeliveryWebhookTestRequest(body).Execute()body := strings.NewReader(`{
"type": "payroll.run.completed"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/webhook_endpoints/webhook_endpoint_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)
{
"id": "webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "webhook_delivery",
"livemode": true,
"mocked": false,
"endpoint_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"event_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"event_type": "example",
"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"
}
/v1/webhook_deliveries#List deliveries
webhook.deliveries.list
Your deliveries, newest first.
Query parameters
endpoint_id
string
optionalReturn 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 webhook_endpoint_. Leave it out to get deliveries to every endpoint you have registered.
Returns
A page of deliveries.
object
always "list"
requiredAlways list. Tells you which kind of record you are looking at, so one handler can read any response.
data
array of WebhookDelivery
requiredThe records on this page, in the order the list promises. Empty when nothing matched.
17 fields of WebhookDelivery
id
string
requiredStarts with webhook_delivery_ 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"
requiredAlways webhook_delivery. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredFalse 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. See DEC-068 and ADR-0011.
mocked
boolean
requiredTrue when this response came from a mock rather than from a calculation, and false when the engine computed it. Required by M1 row 10.
It stays true for a capability that is deferred by section 3 of the contract, even in the live realm, because that capability is still on its documented mock. It is deliberately not called sandbox: sandbox is the name of a realm, and livemode is the field that says which realm you are in. See ADR-0011.
endpoint_id
string
requiredThe id of the endpoint this delivery goes to, from POST /v1/webhook_endpoints or GET /v1/webhook_endpoints; it starts with webhook_endpoint_. Pass it as endpoint_id to GET /v1/webhook_deliveries to list every delivery to that endpoint.
event_id
string
requiredThe 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
requiredThe 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
requiredMonotonic per endpoint, from one, with no gaps. A gap means you missed a delivery, and replay is how you get it.
attempts
integer
requiredHow many have been made, including the one in flight.
last_status
integer · nullable
optionalThe HTTP status of the most recent attempt, where there was one.
last_fault
string · nullable
optionalWhy the most recent attempt produced no status: a refused connection, a DNS failure, a timeout.
next_attempt_at
string · date-time · nullable
optionalNull once the delivery has succeeded or run out of attempts.
delivered_at
string · date-time · nullable
optionalWhen 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
optionalSet after the last attempt. The delivery stays available for replay.
is_test
boolean
requiredtrue when this delivery came from POST /v1/webhook_endpoints/{id}/test rather than a real event. Same envelope, signature and schedule; only this flag differs.
retry_schedule_seconds
array of integer
requiredThe published schedule, so you can reason about it rather than measure it.
created_at
string · date-time
requiredWhen the record was created, as an RFC 3339 timestamp in UTC.
has_more
boolean
requiredtrue when there are more records after this page. Pass the last record's id as starting_after to get the next page.
Other responses
Errors it can return
# query parameters: endpoint_id (optional)
curl -X GET "https://sandbox.droomwork.io/v1/webhook_deliveries?endpoint_id=webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, DELIVERYApi } from '@droomwork/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: 'webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });// query parameters: endpoint_id (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/webhook_deliveries?endpoint_id=webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import droomwork
config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.DELIVERYApi(client)
# query parameters: endpoint_id (optional)
result = api.webhook_deliveries_list(endpoint_id='webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import requests
# query parameters: endpoint_id (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/webhook_deliveries?endpoint_id=webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = DroomworkSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\Api\DELIVERYApi(new GuzzleHttp\Client(), $config);
# query parameters: endpoint_id (optional)
$result = $api->webhookDeliveriesList(endpoint_id: 'webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
// query parameters: endpoint_id (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/webhook_deliveries?endpoint_id=webhook_endpoint_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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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("webhook_endpoint_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=webhook_endpoint_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 Droomwork.Sdk.Api;
using Droomwork.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: "webhook_endpoint_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=webhook_endpoint_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 droomwork "github.com/fenibofubara/droomwork-sdk-go"
ctx := context.WithValue(context.Background(), droomwork.ContextAPIKeys, map[string]droomwork.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := droomwork.NewConfiguration()
cfg.Servers = droomwork.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := droomwork.NewAPIClient(cfg)
// query parameters: endpoint_id (optional)
result, _, err := client.DELIVERYAPI.WebhookDeliveriesList(ctx).EndpointId("webhook_endpoint_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()// query parameters: endpoint_id (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/webhook_deliveries?endpoint_id=webhook_endpoint_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)
{
"object": "list",
"data": [
{
"id": "webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "webhook_delivery",
"livemode": true,
"mocked": false,
"endpoint_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"event_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"event_type": "example",
"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
}
/v1/webhook_deliveries/{id}#Retrieve a delivery
webhook.deliveries.retrieve
One delivery, with the exact bytes we signed and the signature header we sent on the most recent attempt.
Start here when verification fails on your side. The usual cause is re-serialising the body before checking it. The list omits both fields; read the delivery to get them.
Path parameters
id
string
requiredThe 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 webhook_delivery_.
Returns
The delivery, with the signed bytes.
id
string
requiredStarts with webhook_delivery_ 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"
requiredAlways webhook_delivery. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredFalse 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. See DEC-068 and ADR-0011.
mocked
boolean
requiredTrue when this response came from a mock rather than from a calculation, and false when the engine computed it. Required by M1 row 10.
It stays true for a capability that is deferred by section 3 of the contract, even in the live realm, because that capability is still on its documented mock. It is deliberately not called sandbox: sandbox is the name of a realm, and livemode is the field that says which realm you are in. See ADR-0011.
endpoint_id
string
requiredThe id of the endpoint this delivery goes to, from POST /v1/webhook_endpoints or GET /v1/webhook_endpoints; it starts with webhook_endpoint_. Pass it as endpoint_id to GET /v1/webhook_deliveries to list every delivery to that endpoint.
event_id
string
requiredThe 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
requiredThe 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
requiredMonotonic per endpoint, from one, with no gaps. A gap means you missed a delivery, and replay is how you get it.
attempts
integer
requiredHow many have been made, including the one in flight.
last_status
integer · nullable
optionalThe HTTP status of the most recent attempt, where there was one.
last_fault
string · nullable
optionalWhy the most recent attempt produced no status: a refused connection, a DNS failure, a timeout.
next_attempt_at
string · date-time · nullable
optionalNull once the delivery has succeeded or run out of attempts.
delivered_at
string · date-time · nullable
optionalWhen 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
optionalSet after the last attempt. The delivery stays available for replay.
is_test
boolean
requiredtrue when this delivery came from POST /v1/webhook_endpoints/{id}/test rather than a real event. Same envelope, signature and schedule; only this flag differs.
retry_schedule_seconds
array of integer
requiredThe published schedule, so you can reason about it rather than measure it.
created_at
string · date-time
requiredWhen the record was created, as an RFC 3339 timestamp in UTC.
signed_body
string
optionalThe exact bytes that were signed and sent.
signature_header
string · nullable
optionalThe 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
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/webhook_deliveries/webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, DELIVERYApi } from '@droomwork/sdk';
const api = new DELIVERYApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.webhookDeliveriesRetrieve({ id: 'webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/webhook_deliveries/webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import droomwork
config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.DELIVERYApi(client)
result = api.webhook_deliveries_retrieve(id='webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/webhook_deliveries/webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = DroomworkSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\Api\DELIVERYApi(new GuzzleHttp\Client(), $config);
$result = $api->webhookDeliveriesRetrieve(id: 'webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/webhook_deliveries/webhook_delivery_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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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("webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/webhook_deliveries/webhook_delivery_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 Droomwork.Sdk.Api;
using Droomwork.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: "webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/webhook_deliveries/webhook_delivery_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 droomwork "github.com/fenibofubara/droomwork-sdk-go"
ctx := context.WithValue(context.Background(), droomwork.ContextAPIKeys, map[string]droomwork.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := droomwork.NewConfiguration()
cfg.Servers = droomwork.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := droomwork.NewAPIClient(cfg)
result, _, err := client.DELIVERYAPI.WebhookDeliveriesRetrieve(ctx, "webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/webhook_deliveries/webhook_delivery_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)
{
"id": "webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "webhook_delivery",
"livemode": true,
"mocked": false,
"endpoint_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"event_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"event_type": "example",
"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"
}
/v1/webhook_deliveries/{id}/replay#Replay a delivery
webhook.deliveries.replay
Queue a delivery again with the whole schedule ahead of it. Call this after you see a gap in the sequence, or after you were down through all eight attempts.
You get the same bytes that were signed the first time, not a rebuilt body, so what you verify is what was signed. The signature over them is new; the old one carries a timestamp you'd reject.
Path parameters
id
string
requiredThe 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 webhook_delivery_.
Headers
Idempotency-Key
string
requiredA 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 delivery, queued again.
id
string
requiredStarts with webhook_delivery_ 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"
requiredAlways webhook_delivery. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredFalse 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. See DEC-068 and ADR-0011.
mocked
boolean
requiredTrue when this response came from a mock rather than from a calculation, and false when the engine computed it. Required by M1 row 10.
It stays true for a capability that is deferred by section 3 of the contract, even in the live realm, because that capability is still on its documented mock. It is deliberately not called sandbox: sandbox is the name of a realm, and livemode is the field that says which realm you are in. See ADR-0011.
endpoint_id
string
requiredThe id of the endpoint this delivery goes to, from POST /v1/webhook_endpoints or GET /v1/webhook_endpoints; it starts with webhook_endpoint_. Pass it as endpoint_id to GET /v1/webhook_deliveries to list every delivery to that endpoint.
event_id
string
requiredThe 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
requiredThe 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
requiredMonotonic per endpoint, from one, with no gaps. A gap means you missed a delivery, and replay is how you get it.
attempts
integer
requiredHow many have been made, including the one in flight.
last_status
integer · nullable
optionalThe HTTP status of the most recent attempt, where there was one.
last_fault
string · nullable
optionalWhy the most recent attempt produced no status: a refused connection, a DNS failure, a timeout.
next_attempt_at
string · date-time · nullable
optionalNull once the delivery has succeeded or run out of attempts.
delivered_at
string · date-time · nullable
optionalWhen 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
optionalSet after the last attempt. The delivery stays available for replay.
is_test
boolean
requiredtrue when this delivery came from POST /v1/webhook_endpoints/{id}/test rather than a real event. Same envelope, signature and schedule; only this flag differs.
retry_schedule_seconds
array of integer
requiredThe published schedule, so you can reason about it rather than measure it.
created_at
string · date-time
requiredWhen the record was created, as an RFC 3339 timestamp in UTC.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/webhook_deliveries/webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/replay" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"import { Configuration, DELIVERYApi } from '@droomwork/sdk';
const api = new DELIVERYApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.webhookDeliveriesReplay({ id: 'webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/webhook_deliveries/webhook_delivery_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 droomwork
config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.DELIVERYApi(client)
result = api.webhook_deliveries_replay(id='webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/webhook_deliveries/webhook_delivery_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 = DroomworkSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\Api\DELIVERYApi(new GuzzleHttp\Client(), $config);
$result = $api->webhookDeliveriesReplay(id: 'webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/webhook_deliveries/webhook_delivery_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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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("webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/webhook_deliveries/webhook_delivery_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 Droomwork.Sdk.Api;
using Droomwork.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: "webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/webhook_deliveries/webhook_delivery_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 droomwork "github.com/fenibofubara/droomwork-sdk-go"
ctx := context.WithValue(context.Background(), droomwork.ContextAPIKeys, map[string]droomwork.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := droomwork.NewConfiguration()
cfg.Servers = droomwork.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := droomwork.NewAPIClient(cfg)
result, _, err := client.DELIVERYAPI.WebhookDeliveriesReplay(ctx, "webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/webhook_deliveries/webhook_delivery_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)
{
"id": "webhook_delivery_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "webhook_delivery",
"livemode": true,
"mocked": false,
"endpoint_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"event_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"event_type": "example",
"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"
}