DDroomwork Developers

Version 1.0.0

Droomwork 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. 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?

GET/v1/webhook_endpoints#

List webhook endpoints

webhook.endpoints.list

The endpoints you have registered.

Returns

A page of endpoints.

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 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" 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. See DEC-068 and ADR-0011.

mocked boolean required

True 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 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 '@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)
Response
{
  "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
}
POST/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 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, with its secret shown once.

id string required

Assigned 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" 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. See DEC-068 and ADR-0011.

mocked boolean required

True 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 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 '@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)
Response
{
  "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"
}
GET/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 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 webhook_endpoint_.

Returns

The endpoint.

id string required

Assigned 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" 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. See DEC-068 and ADR-0011.

mocked boolean required

True 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 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/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)
Response
{
  "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"
}
POST/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 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 webhook_endpoint_.

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 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" 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. See DEC-068 and ADR-0011.

mocked boolean required

True 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 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/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)
Response
{
  "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"
}
POST/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 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 webhook_endpoint_.

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 disabled endpoint.

id string required

Assigned 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" 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. See DEC-068 and ADR-0011.

mocked boolean required

True 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 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/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)
Response
{
  "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"
}
POST/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 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 webhook_endpoint_.

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 delivery.

id string required

Starts 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" 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. See DEC-068 and ADR-0011.

mocked boolean required

True 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 required

The 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 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 rather than 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 reason about it rather than measure 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/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)
Response
{
  "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"
}
GET/v1/webhook_deliveries#

List deliveries

webhook.deliveries.list

Your deliveries, newest first.

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 webhook_endpoint_. Leave it out to get deliveries to every endpoint you have registered.

Returns

A page of deliveries.

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 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" 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. See DEC-068 and ADR-0011.

mocked boolean required

True 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 required

The 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 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 rather than 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 reason about it rather than measure 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=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)
Response
{
  "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
}
GET/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 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 webhook_delivery_.

Returns

The delivery, with the signed bytes.

id string required

Starts 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" 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. See DEC-068 and ADR-0011.

mocked boolean required

True 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 required

The 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 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 rather than 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 reason about it rather than measure 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/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)
Response
{
  "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"
}
POST/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 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 webhook_delivery_.

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 delivery, queued again.

id string required

Starts 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" 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. See DEC-068 and ADR-0011.

mocked boolean required

True 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 required

The 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 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 rather than 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 reason about it rather than measure 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/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)
Response
{
  "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"
}