Version 1.0.0
Droomwork RAIL
Classification, instruments and the legal standing of a working relationship.
https://sandbox.droomwork.ioDroomwork-Api-Key: dw_test_… or Authorization: Bearer dw_test_…RAIL gives you a working relationship you can stand behind: tell it the facts, get a classification, an executed instrument, and a statement of what you owe and when.
What you should know before you start
Classify before you draft. Send the facts you're prepared to attest to and you get a classification back, with the full reasoning attached: which indicators fired, their weights, the version they were judged against, and the facts you attested to. Keep it; it's what you'll show if the classification is ever questioned.
Your instruments come from an approved clause library. Nothing is written for you on the fly. You can read every clause before you use it.
You can't issue a contractor agreement over facts that classify as employment. The classification decides, not the template you'd prefer.
Both parties need a live Passport before anything executes, and that includes you. Check your own side first.
You get the basis of what's owed, never a figure. The obligation envelope tells you what's owed, to which authority, on what basis, from what date and how often. Hand it to your payroll.
The worker gets their own copy. Each party receives the executed instrument on the channel they used, and the worker can retrieve it without going through the engaging party.
Getting started
Open an engagement and attest to the facts. Classify it. Draft the instrument. Collect assent from both parties. Register it. Read the obligation envelope.
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/rail/engagements#List engagements
rail.engagements.list
Your engagements, newest first.
Query parameters
limit
integer
optionalHow many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.
starting_after
string
optionalThe id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.
status
string
optionalOnly engagements in one status: classified, papered, executed, registered, current, lapsed, disputed or terminated. Leave it out to get every status.
classifiedpaperedexecutedregisteredcurrentlapseddisputedterminatedclassification
string
optionalOnly engagements classified as employment, fixed_term_employment, independent_contracting, apprenticeship, casual or task_based. Leave it out to get every classification.
employmentfixed_term_employmentindependent_contractingapprenticeshipcasualtask_basedReturns
A page of engagements.
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 Engagement
requiredThe records on this page, in the order the list promises. Empty when nothing matched.
15 fields of Engagement
id
string
requiredThe engagement's identifier, starting with rail_engagement_. You get it from POST /v1/rail/engagements and pass it as engagement_id on every call about this engagement; it never changes.
object
always "engagement"
requiredAlways engagement. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
status
string
requiredTransitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.
classifiedpaperedexecutedregisteredcurrentlapseddisputedterminatedengaging_party_ref
string
requiredThe party engaging the worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the worker.
worker_ref
string
requiredThe worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the engaging party.
classification
one of
optionalThe classification once classify has run, or null before then: employment, fixed_term_employment, independent_contracting, apprenticeship, casual or task_based. It decides which instrument you can paper.
Classificationorclassification_review_required
boolean
optionalTrue when confidence was low or the result was contested. A person decides.
instrument_in_force_id
string · nullable
optionalThe id of the instrument in force, starting with rail_instrument_, from rail.instruments.create or, for a variation, rail.instruments.supersede. null until one is executed; a variation that supersedes it takes its place here once executed.
envelope_id
string · nullable
optionalThe id of the obligation envelope published for this engagement, starting with rail_envelope_; null until one is published. Retrieve it at GET /v1/rail/engagements/{engagement_id}/envelope or list it at GET /v1/rail/envelopes.
starts_on
string · date
optionalThe date the engagement starts, as a calendar date in YYYY-MM-DD. As you sent it when you opened the engagement.
ends_on
string · date · nullable
optionalThe date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.
as_of
string · date · nullable
optionalPresent when this was answered as at a past date rather than now.
created_at
string · date-time
optionalWhen 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: limit (optional), starting_after (optional), status (optional), classification (optional)
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements?limit=25&status=classified&classification=employment" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
// query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
const result = await api.railEngagementsList({ limit: 25, status: 'classified', classification: 'employment' });// query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements?limit=25&status=classified&classification=employment', {
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.RAILApi(client)
# query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
result = api.rail_engagements_list(limit=25, status='classified', classification='employment')import os
import requests
# query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/engagements?limit=25&status=classified&classification=employment',
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\RAILApi(new GuzzleHttp\Client(), $config);
# query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
$result = $api->railEngagementsList(limit: 25, status: 'classified', classification: 'employment');<?php
// query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements?limit=25&status=classified&classification=employment');
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.RailApi;
import com.droomwork.sdk.model.*;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
// query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
var result = api.railEngagementsList(25, null, RailEngagementStatus.fromValue("classified"), RailClassification.fromValue("employment"));// query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements?limit=25&status=classified&classification=employment"))
.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 RAILApi(config);
// query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
var result = api.RailEngagementsList(limit: 25, status: RailEngagementStatus.Classified, classification: RailClassification.Employment);// query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/engagements?limit=25&status=classified&classification=employment");
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: limit (optional), starting_after (optional), status (optional), classification (optional)
result, _, err := client.RAILAPI.RailEngagementsList(ctx).Limit(25).Status(droomwork.RailEngagementStatus("classified")).Classification(droomwork.RailClassification("employment")).Execute()// query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements?limit=25&status=classified&classification=employment", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "engagement",
"livemode": true,
"mocked": true,
"status": "classified",
"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"classification": "employment",
"classification_review_required": true,
"instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"starts_on": "2026-09-01",
"ends_on": "2026-09-01",
"as_of": "2026-09-01",
"created_at": "2026-09-01T09:00:00Z"
}
],
"has_more": true
}
/v1/rail/engagements#Open an engagement
rail.engagements.create
Records the two parties and the facts you attest to. The attesting individual and the timestamp are kept permanently, so you can show what was relied on and who said it.
Creating an engagement classifies nothing. Call classify next.
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
engaging_party_ref
string
requiredThe party engaging the worker: the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent again at POST /v1/identity/verifications. Both parties must be anchored, or the call is refused with subject_not_anchored.
worker_ref
string
requiredThe worker: the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent again at POST /v1/identity/verifications. It must resolve to an anchored identity, or the call is refused with subject_not_anchored.
starts_on
string · date
requiredThe date the engagement starts, as a calendar date in YYYY-MM-DD.
ends_on
string · date
optionalThe date the engagement ends, as a calendar date in YYYY-MM-DD. Leave it out when there is no fixed end date.
attested_facts
AttestedFacts
requiredThe facts the classification reads. You state them, and they're kept permanently with who stated them.
9 fields of AttestedFacts
controls_how_work_is_done
boolean
optionaltrue when the engaging party directs how the work is carried out, not only what is delivered. Control is one of the indicators the classification reads.
controls_when_work_is_done
boolean
optionaltrue when the engaging party sets the hours or days the work is done, rather than the worker choosing them.
provides_equipment
boolean
optionaltrue when the engaging party supplies the tools and equipment the work is done with, rather than the worker bringing their own.
exclusive_to_this_party
boolean
optionaltrue when the worker works for this engaging party alone and takes no engagements from anyone else.
can_send_a_substitute
boolean
optionaltrue when the worker may send someone else to do the work in their place, rather than having to do it personally.
bears_financial_risk
boolean
optionaltrue when the worker carries the financial risk of the work: their own costs, and the loss if it goes wrong or is not paid for.
integrated_into_organisation
boolean
optionaltrue when the worker is part of the engaging party's organisation, its teams and its management, rather than an outside supplier to it.
duration_months
integer · minimum 0
optionalHow long the engagement is expected to run, in whole months, 0 or more.
occupation_code
string
optionalThe occupation the work falls under, as a code such as ng-7412. Give the code for the work actually done: the classification reads it with the other facts.
attesting_individual
string
optionalA named person, recorded permanently. Not a system account.
Returns
The engagement, awaiting classification.
id
string
requiredThe engagement's identifier, starting with rail_engagement_. You get it from POST /v1/rail/engagements and pass it as engagement_id on every call about this engagement; it never changes.
object
always "engagement"
requiredAlways engagement. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
status
string
requiredTransitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.
classifiedpaperedexecutedregisteredcurrentlapseddisputedterminatedengaging_party_ref
string
requiredThe party engaging the worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the worker.
worker_ref
string
requiredThe worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the engaging party.
classification
one of
optionalThe classification once classify has run, or null before then: employment, fixed_term_employment, independent_contracting, apprenticeship, casual or task_based. It decides which instrument you can paper.
Classificationorclassification_review_required
boolean
optionalTrue when confidence was low or the result was contested. A person decides.
instrument_in_force_id
string · nullable
optionalThe id of the instrument in force, starting with rail_instrument_, from rail.instruments.create or, for a variation, rail.instruments.supersede. null until one is executed; a variation that supersedes it takes its place here once executed.
envelope_id
string · nullable
optionalThe id of the obligation envelope published for this engagement, starting with rail_envelope_; null until one is published. Retrieve it at GET /v1/rail/engagements/{engagement_id}/envelope or list it at GET /v1/rail/envelopes.
starts_on
string · date
optionalThe date the engagement starts, as a calendar date in YYYY-MM-DD. As you sent it when you opened the engagement.
ends_on
string · date · nullable
optionalThe date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.
as_of
string · date · nullable
optionalPresent when this was answered as at a past date rather than now.
created_at
string · date-time
optionalWhen 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/rail/engagements" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"engaging_party_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","worker_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","starts_on":"2026-09-01","attested_facts":{"controls_how_work_is_done":true,"controls_when_work_is_done":true,"provides_equipment":true,"exclusive_to_this_party":true,"can_send_a_substitute":true,"bears_financial_risk":true,"integrated_into_organisation":true,"duration_months":0,"occupation_code":"ng-7412"},"ends_on":"2026-09-01","attesting_individual":"example"}'import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railEngagementsCreate({
idempotencyKey: crypto.randomUUID(),
railEngagementCreateRequest: {"engagingPartyRef":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","workerRef":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","startsOn":"2026-09-01","attestedFacts":{"controlsHowWorkIsDone":true,"controlsWhenWorkIsDone":true,"providesEquipment":true,"exclusiveToThisParty":true,"canSendASubstitute":true,"bearsFinancialRisk":true,"integratedIntoOrganisation":true,"durationMonths":0,"occupationCode":"ng-7412"},"endsOn":"2026-09-01","attestingIndividual":"example"},
});const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"starts_on": "2026-09-01",
"attested_facts": {
"controls_how_work_is_done": true,
"controls_when_work_is_done": true,
"provides_equipment": true,
"exclusive_to_this_party": true,
"can_send_a_substitute": true,
"bears_financial_risk": true,
"integrated_into_organisation": true,
"duration_months": 0,
"occupation_code": "ng-7412"
},
"ends_on": "2026-09-01",
"attesting_individual": "example"
}),
});
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.RAILApi(client)
result = api.rail_engagements_create(body={"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "starts_on": "2026-09-01", "attested_facts": {"controls_how_work_is_done": True, "controls_when_work_is_done": True, "provides_equipment": True, "exclusive_to_this_party": True, "can_send_a_substitute": True, "bears_financial_risk": True, "integrated_into_organisation": True, "duration_months": 0, "occupation_code": "ng-7412"}, "ends_on": "2026-09-01", "attesting_individual": "example"})import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/engagements',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "starts_on": "2026-09-01", "attested_facts": {"controls_how_work_is_done": True, "controls_when_work_is_done": True, "provides_equipment": True, "exclusive_to_this_party": True, "can_send_a_substitute": True, "bears_financial_risk": True, "integrated_into_organisation": True, "duration_months": 0, "occupation_code": "ng-7412"}, "ends_on": "2026-09-01", "attesting_individual": "example"},
)
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railEngagementsCreate($idempotencyKey, json_decode('{"engaging_party_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","worker_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","starts_on":"2026-09-01","attested_facts":{"controls_how_work_is_done":true,"controls_when_work_is_done":true,"provides_equipment":true,"exclusive_to_this_party":true,"can_send_a_substitute":true,"bears_financial_risk":true,"integrated_into_organisation":true,"duration_months":0,"occupation_code":"ng-7412"},"ends_on":"2026-09-01","attesting_individual":"example"}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements');
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 => '{"engaging_party_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","worker_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","starts_on":"2026-09-01","attested_facts":{"controls_how_work_is_done":true,"controls_when_work_is_done":true,"provides_equipment":true,"exclusive_to_this_party":true,"can_send_a_substitute":true,"bears_financial_risk":true,"integrated_into_organisation":true,"duration_months":0,"occupation_code":"ng-7412"},"ends_on":"2026-09-01","attesting_individual":"example"}',
]);
$result = json_decode(curl_exec($ch), true);import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railEngagementsCreate(idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements"))
.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("""
{
"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"starts_on": "2026-09-01",
"attested_facts": {
"controls_how_work_is_done": true,
"controls_when_work_is_done": true,
"provides_equipment": true,
"exclusive_to_this_party": true,
"can_send_a_substitute": true,
"bears_financial_risk": true,
"integrated_into_organisation": true,
"duration_months": 0,
"occupation_code": "ng-7412"
},
"ends_on": "2026-09-01",
"attesting_individual": "example"
}
"""))
.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 RAILApi(config);
var result = api.RailEngagementsCreate(idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"starts_on": "2026-09-01",
"attested_facts": {
"controls_how_work_is_done": true,
"controls_when_work_is_done": true,
"provides_equipment": true,
"exclusive_to_this_party": true,
"can_send_a_substitute": true,
"bears_financial_risk": true,
"integrated_into_organisation": true,
"duration_months": 0,
"occupation_code": "ng-7412"
},
"ends_on": "2026-09-01",
"attesting_individual": "example"
}
""", 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.RAILAPI.RailEngagementsCreate(ctx).IdempotencyKey(key).RailEngagementCreateRequest(body).Execute()body := strings.NewReader(`{
"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"starts_on": "2026-09-01",
"attested_facts": {
"controls_how_work_is_done": true,
"controls_when_work_is_done": true,
"provides_equipment": true,
"exclusive_to_this_party": true,
"can_send_a_substitute": true,
"bears_financial_risk": true,
"integrated_into_organisation": true,
"duration_months": 0,
"occupation_code": "ng-7412"
},
"ends_on": "2026-09-01",
"attesting_individual": "example"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "engagement",
"livemode": true,
"mocked": true,
"status": "classified",
"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"classification": "employment",
"classification_review_required": true,
"instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"starts_on": "2026-09-01",
"ends_on": "2026-09-01",
"as_of": "2026-09-01",
"created_at": "2026-09-01T09:00:00Z"
}
/v1/rail/reconcile#Recover from missed lapse and termination events
rail.engagements.reconcile
The current standing of every engagement you hold an interest in, with a sequence number.
Call this if you dropped a webhook or your consumer was down. Otherwise a missed lapse leaves a worker taking offers on an engagement that is no longer lawful, and nothing tells you.
Query parameters
since_sequence
integer
optionalOnly changes after this sequence number: pass the as_of_sequence from your last call to GET /v1/rail/reconcile. Leave it out to get the current standing of every engagement.
Returns
The current standing of every engagement you hold an interest in.
object
always "engagement_reconciliation"
requiredAlways engagement_reconciliation. Tells you which kind of record you are looking at, so one handler can read any response.
as_of_sequence
integer · minimum 0
requiredThe sequence number this report is current to. Keep it and pass it as since_sequence next time to get only what changed after it.
generated_at
string · date-time
optionalWhen this report was produced, as an RFC 3339 timestamp in UTC.
engagements
array of object
requiredOne row per engagement you hold an interest in, giving its current standing and the sequence number of its latest change. Only the changes after since_sequence when you passed one.
4 fields
engagement_id
string
requiredThe engagement this row is about: its id, starting with rail_engagement_, as returned by POST /v1/rail/engagements. Pass it as engagement_id to GET /v1/rail/engagements/{engagement_id} to read it in full.
status
string
requiredTransitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.
classifiedpaperedexecutedregisteredcurrentlapseddisputedterminatedsequence
integer
requiredThe sequence number of this engagement's latest change. Higher means later; compare it with the last event you handled to see what you missed.
changed_at
string · date-time
optionalWhen the engagement last changed standing, as an RFC 3339 timestamp in UTC.
Other responses
Errors it can return
# query parameters: since_sequence (optional)
curl -X GET "https://sandbox.droomwork.io/v1/rail/reconcile?since_sequence=0" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
// query parameters: since_sequence (optional)
const result = await api.railEngagementsReconcile({ sinceSequence: 0 });// query parameters: since_sequence (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/reconcile?since_sequence=0', {
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.RAILApi(client)
# query parameters: since_sequence (optional)
result = api.rail_engagements_reconcile(since_sequence=0)import os
import requests
# query parameters: since_sequence (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/reconcile?since_sequence=0',
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\RAILApi(new GuzzleHttp\Client(), $config);
# query parameters: since_sequence (optional)
$result = $api->railEngagementsReconcile(since_sequence: 0);<?php
// query parameters: since_sequence (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/reconcile?since_sequence=0');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
// query parameters: since_sequence (optional)
var result = api.railEngagementsReconcile(0);// query parameters: since_sequence (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/reconcile?since_sequence=0"))
.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 RAILApi(config);
// query parameters: since_sequence (optional)
var result = api.RailEngagementsReconcile(sinceSequence: 0);// query parameters: since_sequence (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/reconcile?since_sequence=0");
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: since_sequence (optional)
result, _, err := client.RAILAPI.RailEngagementsReconcile(ctx).SinceSequence(0).Execute()// query parameters: since_sequence (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/reconcile?since_sequence=0", 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": "engagement_reconciliation",
"as_of_sequence": 0,
"engagements": [
{
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"status": "classified",
"sequence": 1,
"changed_at": "2026-09-01T09:00:00Z"
}
],
"generated_at": "2026-09-01T09:00:00Z"
}
/v1/rail/envelopes#List obligation envelopes
rail.envelopes.list
What each of your engagements makes owed, for your payroll and remittance to read.
No envelope carries an amount. It carries the basis your payroll computes the amount from.
Query parameters
limit
integer
optionalHow many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.
starting_after
string
optionalThe id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.
engagement_id
string
optionalOnly the envelopes of one engagement, by its id, which starts with rail_engagement_ and comes from POST /v1/rail/engagements or GET /v1/rail/engagements. Leave it out to list envelopes across all your engagements.
Returns
A page of envelopes.
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 ObligationEnvelope
requiredThe records on this page, in the order the list promises. Empty when nothing matched.
10 fields of ObligationEnvelope
id
string
requiredThe envelope's identifier, starting with rail_envelope_. You get it at GET /v1/rail/engagements/{engagement_id}/envelope, as envelope_id on the engagement and in data on the envelope.published event; it never changes.
object
always "obligation_envelope"
requiredAlways obligation_envelope. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
engagement_id
string
requiredThe engagement this envelope states the obligations for: its id, starting with rail_engagement_, as returned by POST /v1/rail/engagements. Pass it as engagement_id to retrieve either record.
classification
string
optionalDecided by the versioned pack. No model decides it and you can't assert one yourself.
employmentfixed_term_employmentindependent_contractingapprenticeshipcasualtask_basedobligations
array of object
requiredOne entry per obligation that attaches: to which authority, on what basis, from what date and how often, never an amount. One a threshold or exemption removes is absent, not present and zero.
7 fields
authority
string
requiredThe authority the obligation is owed to, by name, such as Rivers State Internal Revenue Service. For people to read; match records on authority_id.
authority_id
string
optionalThe id of the authority the obligation is owed to, as listed at GET /v1/remittance/authorities; it starts with remit_authority_rail_obligation_. Match on it rather than on authority, which is the name.
basis
string
requiredHow an amount is worked out, not what it is. Your payroll computes the figure from it.
effective_from
string · date
requiredThe date from which the obligation applies, as a calendar date in YYYY-MM-DD. Nothing is owed under it for any earlier date.
effective_to
string · date · nullable
optionalThe last date the obligation applies, as a calendar date in YYYY-MM-DD. null while it has no end date.
frequency
string
requiredHow often the obligation falls due: monthly, quarterly, annual, or per_engagement for one that is owed once for the engagement rather than by period.
monthlyquarterlyannualper_engagementthreshold_note
string
optionalWhy a threshold or exemption did or did not apply here.
rule_pack_version
string
optionalThe version of the rule pack these obligations were stated under, such as 2026.08.1. Keep it with the envelope: it names the rules your payroll's figures trace back to.
published_at
string · date-time
requiredWhen this envelope was published for your payroll and remittance to read, as an RFC 3339 timestamp in UTC.
supersedes
string · nullable
optionalThe id of the envelope this one replaces, starting with rail_envelope_, or null for the first envelope on this engagement. Follow it back to see what was owed before.
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: limit (optional), starting_after (optional), engagement_id (optional)
curl -X GET "https://sandbox.droomwork.io/v1/rail/envelopes?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
// query parameters: limit (optional), starting_after (optional), engagement_id (optional)
const result = await api.railEnvelopesList({ limit: 25, engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });// query parameters: limit (optional), starting_after (optional), engagement_id (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/envelopes?limit=25&engagement_id=rail_engagement_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.RAILApi(client)
# query parameters: limit (optional), starting_after (optional), engagement_id (optional)
result = api.rail_envelopes_list(limit=25, engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import requests
# query parameters: limit (optional), starting_after (optional), engagement_id (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/envelopes?limit=25&engagement_id=rail_engagement_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\RAILApi(new GuzzleHttp\Client(), $config);
# query parameters: limit (optional), starting_after (optional), engagement_id (optional)
$result = $api->railEnvelopesList(limit: 25, engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
// query parameters: limit (optional), starting_after (optional), engagement_id (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/envelopes?limit=25&engagement_id=rail_engagement_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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
// query parameters: limit (optional), starting_after (optional), engagement_id (optional)
var result = api.railEnvelopesList(25, null, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");// query parameters: limit (optional), starting_after (optional), engagement_id (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/envelopes?limit=25&engagement_id=rail_engagement_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 RAILApi(config);
// query parameters: limit (optional), starting_after (optional), engagement_id (optional)
var result = api.RailEnvelopesList(limit: 25, engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");// query parameters: limit (optional), starting_after (optional), engagement_id (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/envelopes?limit=25&engagement_id=rail_engagement_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: limit (optional), starting_after (optional), engagement_id (optional)
result, _, err := client.RAILAPI.RailEnvelopesList(ctx).Limit(25).EngagementId("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()// query parameters: limit (optional), starting_after (optional), engagement_id (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/envelopes?limit=25&engagement_id=rail_engagement_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": "rail_envelope_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "obligation_envelope",
"livemode": true,
"mocked": true,
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"obligations": [
{
"authority": "Rivers State Internal Revenue Service",
"basis": "paye_graduated_bands",
"effective_from": "2026-09-01",
"frequency": "monthly",
"authority_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"effective_to": "2026-09-01",
"threshold_note": "example"
}
],
"published_at": "2026-09-01T09:00:00Z",
"classification": "employment",
"rule_pack_version": "2026.08.1",
"supersedes": "example"
}
],
"has_more": true
}
/v1/rail/instruments#List instruments
rail.instruments.list
Instruments across your engagements, including superseded versions. The chain is the record.
Query parameters
limit
integer
optionalHow many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.
starting_after
string
optionalThe id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.
engagement_id
string
optionalOnly instruments on one engagement, by its id, which starts with rail_engagement_ and comes from POST /v1/rail/engagements or GET /v1/rail/engagements. Leave it out to list across all your engagements.
status
string
optionalOnly instruments in one status: drafted, awaiting_assent, executed, expired (the window passed with no execution) or superseded (a later variation replaced it). Leave it out to list every status, superseded versions included.
draftedawaiting_assentexecutedexpiredsupersededReturns
A page of instruments.
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 Instrument
requiredThe records on this page, in the order the list promises. Empty when nothing matched.
16 fields of Instrument
id
string
requiredThe instrument's identifier, starting with rail_instrument_, from rail.instruments.create or, for a variation, rail.instruments.supersede. Pass it as instrument_id on every call about this instrument; it never changes.
object
always "instrument"
requiredAlways instrument. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
engagement_id
string
requiredThe id of the engagement this instrument papers, starting with rail_engagement_, as returned by POST /v1/rail/engagements. A variation drafted by supersede stays on the same engagement.
status
string
requireddraftedawaiting_assentexecutedexpiredsupersededtemplate_code
string
requiredA template from the approved set. Not wording.
clauses
array of object
optionalThe clauses used, by reference. Every one comes from the approved library, as written there.
2 fields
clause_code
string
requiredThe code of a clause from the approved library. Look it up by the same clause_code in the clause library to read what the instrument contains.
clause_version
string
requiredThe version of that clause as it stood in the library the instrument was built from. The instrument holds this version even after the library moves on.
clause_library_version
string
requiredThe clause library version this instrument was assembled from, such as 2026.08.1. Pass it as version to the clause library endpoint to read exactly what was used.
rule_pack_version
string
optionalThe version of the rule pack this instrument rests on, such as 2026.08.1: the rules the classification behind it was judged against. It stays on the instrument once executed, whatever pack comes later.
legal_approval_reference
string
optionalThe reference of the legal approval behind the clause library this instrument was built from. Cite it when you're asked what approved the wording; it matches the library's own.
seal
one of
optionalThe tamper evident seal, applied when the instrument is executed; null until then. Send its hash to the verify endpoint to prove the document you hold is the one that was executed.
Sealorsupersedes
string · nullable
optionalThe id of the instrument this one was drafted as a variation of, starting with rail_instrument_, or null for an original. The earlier instrument is never edited or removed.
superseded_by
string · nullable
optionalThe id of the variation drafted to replace this instrument, starting with rail_instrument_, or null while none has been. Follow it forward to reach the newest version.
expires_at
string · date-time · nullable
optionalAn unexecuted instrument lapses after this, and the lapse is recorded.
executed_at
string · date-time · nullable
optionalWhen both parties' assent was complete and the instrument was executed, as an RFC 3339 timestamp in UTC. null until then; nothing binds anyone before it.
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: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
curl -X GET "https://sandbox.droomwork.io/v1/rail/instruments?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=drafted" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
// query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
const result = await api.railInstrumentsList({ limit: 25, engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', status: 'drafted' });// query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/instruments?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=drafted', {
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.RAILApi(client)
# query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
result = api.rail_instruments_list(limit=25, engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', status='drafted')import os
import requests
# query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/instruments?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=drafted',
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\RAILApi(new GuzzleHttp\Client(), $config);
# query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
$result = $api->railInstrumentsList(limit: 25, engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', status: 'drafted');<?php
// query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/instruments?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=drafted');
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.RailApi;
import com.droomwork.sdk.model.*;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
// query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
var result = api.railInstrumentsList(25, null, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", RailInstrumentStatus.fromValue("drafted"));// query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/instruments?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=drafted"))
.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 RAILApi(config);
// query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
var result = api.RailInstrumentsList(limit: 25, engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", status: RailInstrumentStatus.Drafted);// query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/instruments?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=drafted");
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: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
result, _, err := client.RAILAPI.RailInstrumentsList(ctx).Limit(25).EngagementId("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Status(droomwork.RailInstrumentStatus("drafted")).Execute()// query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/instruments?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=drafted", 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": "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "instrument",
"livemode": true,
"mocked": true,
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"status": "drafted",
"template_code": "no_payee_destination",
"clause_library_version": "2026.08.1",
"clauses": [
{
"clause_code": "no_payee_destination",
"clause_version": "2026.08.1"
}
],
"rule_pack_version": "2026.08.1",
"legal_approval_reference": "paye-2026-09-rivers",
"seal": {
"hash": "sha256:9f2c1e0043a1b8",
"previous_hash": "sha256:9f2c1e0043a1b8",
"sealed_at": "2026-09-01T09:00:00Z",
"algorithm": "sha256"
},
"supersedes": "example",
"superseded_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"expires_at": "2026-09-01T09:00:00Z",
"executed_at": "2026-09-01T09:00:00Z"
}
],
"has_more": true
}
/v1/rail/instruments/verify#Verify a sealed instrument independently
rail.instruments.verify
Checks a tamper evident hash against the chain and tells you whether the document you hold is the document that was executed.
You don't need to hold the engagement to call this.
Body
hash
string
requiredThe seal hash from the document you hold.
instrument_id
string
optionalThe id of the instrument the document you hold claims to be, starting with rail_instrument_, from rail.instruments.create or rail.instruments.supersede. Pass it when you know it; the hash alone is enough to verify.
Returns
The verification result.
object
always "instrument_verification"
requiredAlways instrument_verification. Tells you which kind of record you are looking at, so one handler can read any response.
valid
boolean
requiredWhether this hash matches an instrument that was executed.
chain_intact
boolean
requiredWhether the chain from that instrument back to its origin is unbroken.
instrument_id
string · nullable
optionalThe id of the executed instrument the hash matched, starting with rail_instrument_: the same id you see at GET /v1/rail/instruments. null when valid is false and nothing matched.
executed_at
string · date-time · nullable
optionalWhen the matched instrument was executed, as an RFC 3339 timestamp in UTC. null when nothing matched.
superseded_by
string · nullable
optionalSet when a later variation replaced the instrument you are holding.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/rail/instruments/verify" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"hash":"sha256:9f2c1e0043a1b8","instrument_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}'import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railInstrumentsVerify({
railInstrumentVerifyRequest: {"hash":"sha256:9f2c1e0043a1b8","instrumentId":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"},
});const response = await fetch('https://sandbox.droomwork.io/v1/rail/instruments/verify', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"hash": "sha256:9f2c1e0043a1b8",
"instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}),
});
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.RAILApi(client)
result = api.rail_instruments_verify(body={"hash": "sha256:9f2c1e0043a1b8", "instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"})import os
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/instruments/verify',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
json={"hash": "sha256:9f2c1e0043a1b8", "instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"},
)
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railInstrumentsVerify(json_decode('{"hash":"sha256:9f2c1e0043a1b8","instrument_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/instruments/verify');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{"hash":"sha256:9f2c1e0043a1b8","instrument_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}',
]);
$result = json_decode(curl_exec($ch), true);import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railInstrumentsVerify(body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/instruments/verify"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"hash": "sha256:9f2c1e0043a1b8",
"instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
"""))
.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 RAILApi(config);
var result = api.RailInstrumentsVerify(body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/instruments/verify");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Content = new StringContent("""
{
"hash": "sha256:9f2c1e0043a1b8",
"instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
""", 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.RAILAPI.RailInstrumentsVerify(ctx).RailInstrumentVerifyRequest(body).Execute()body := strings.NewReader(`{
"hash": "sha256:9f2c1e0043a1b8",
"instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/instruments/verify", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"object": "instrument_verification",
"valid": true,
"chain_intact": true,
"instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"executed_at": "2026-09-01T09:00:00Z",
"superseded_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
/v1/rail/clause_library#Read the approved clause library
rail.clause_library.retrieve
Every clause an instrument can be built from, with the legal approval reference behind it.
Read only. There is no endpoint that changes it, and nothing drafts, edits or paraphrases a clause for you.
Query parameters
version
string
optionalThe library version to read, such as 2026.08.1: the clause_library_version on an instrument from GET /v1/rail/instruments, or the version on this response. Leave it out to get the version currently in force.
Returns
The clause library.
object
always "clause_library"
requiredAlways clause_library. Tells you which kind of record you are looking at, so one handler can read any response.
version
string
requiredThe library version you are reading, such as 2026.08.1. You get the version in force unless you asked for another with version; an instrument records the one it was built from.
legal_approval_reference
string
requiredThe reference under which this version of the library was legally approved. Every instrument built from it carries the same reference, so you can cite the approval from either.
effective_from
string · date
optionalThe date this version of the library came into force, as YYYY-MM-DD. Drafting uses whichever version is in force unless you name one with clause_library_version.
clauses
array of object
requiredEvery clause an instrument can be built from, in this version. Each gives its code, version, heading, the classifications it may appear in and whether it is mandatory.
5 fields
clause_code
string
requiredThe clause's code. It is what an instrument's clauses and a template's clause_codes refer to, so match on it rather than on the heading.
clause_version
string
requiredThe clause's version in this library. An instrument records the version it used, so a later change never alters what was executed.
heading
string
optionalThe clause's heading, in plain words, so you can tell one clause from another when you show the library to someone.
applies_to
array of Classification
requiredClassifications this clause may appear in.
mandatory
boolean
optionaltrue when the clause must appear in every instrument for the classifications in applies_to; false when a template may include it or not.
templates
array of object
optionalThe templates you can draft from, one template_code each. Each names the classification it serves and the clauses it assembles; pick one by code when drafting an instrument.
3 fields
template_code
string
requiredThe code you pass as template_code when drafting an instrument. It selects a template, never wording.
classification
string
requiredDecided by the versioned pack. No model decides it and you can't assert one yourself.
employmentfixed_term_employmentindependent_contractingapprenticeshipcasualtask_basedclause_codes
array of string
optionalThe clause_code of each clause the template assembles. Look each one up in clauses; a variable you supply at drafting can't add to this list or change a clause.
Other responses
Errors it can return
# query parameters: version (optional)
curl -X GET "https://sandbox.droomwork.io/v1/rail/clause_library?version=2026.08.1" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
// query parameters: version (optional)
const result = await api.railClauseLibraryRetrieve({ version: '2026.08.1' });// query parameters: version (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/clause_library?version=2026.08.1', {
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.RAILApi(client)
# query parameters: version (optional)
result = api.rail_clause_library_retrieve(version='2026.08.1')import os
import requests
# query parameters: version (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/clause_library?version=2026.08.1',
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\RAILApi(new GuzzleHttp\Client(), $config);
# query parameters: version (optional)
$result = $api->railClauseLibraryRetrieve(version: '2026.08.1');<?php
// query parameters: version (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/clause_library?version=2026.08.1');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
// query parameters: version (optional)
var result = api.railClauseLibraryRetrieve("2026.08.1");// query parameters: version (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/clause_library?version=2026.08.1"))
.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 RAILApi(config);
// query parameters: version (optional)
var result = api.RailClauseLibraryRetrieve(version: "2026.08.1");// query parameters: version (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/clause_library?version=2026.08.1");
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: version (optional)
result, _, err := client.RAILAPI.RailClauseLibraryRetrieve(ctx).Version("2026.08.1").Execute()// query parameters: version (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/clause_library?version=2026.08.1", 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": "clause_library",
"version": "2026.08.1",
"legal_approval_reference": "paye-2026-09-rivers",
"clauses": [
{
"clause_code": "no_payee_destination",
"clause_version": "2026.08.1",
"applies_to": [
"employment"
],
"heading": "example",
"mandatory": true
}
],
"effective_from": "2026-09-01",
"templates": [
{
"template_code": "no_payee_destination",
"classification": "employment",
"clause_codes": [
"no_payee_destination"
]
}
]
}
/v1/rail/remediation_cases#List remediation cases
rail.remediation_cases.list
Opened when a rule pack change makes one of your live engagements non compliant. Each case names the specific change and the action you need to take.
Query parameters
limit
integer
optionalHow many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.
starting_after
string
optionalThe id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.
status
string
optionalOnly cases in one status: open until someone acts, in_progress while the action is under way, or resolved once you closed it with what was done. Leave it out to list all three.
openin_progressresolvedReturns
A page of cases.
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 RemediationCase
requiredThe records on this page, in the order the list promises. Empty when nothing matched.
10 fields of RemediationCase
id
string
requiredThe case's identifier, starting with rail_engagement_, as listed at GET /v1/rail/remediation_cases or in data on the remediation_case.opened event. Pass it as remediation_case_id to retrieve or resolve it; it never changes.
object
always "remediation_case"
requiredAlways remediation_case. Tells you which kind of record you are looking at, so one handler can read any response.
engagement_id
string
requiredThe id of the engagement the rule pack change made non compliant, starting with rail_engagement_, as returned by POST /v1/rail/engagements. Its status has moved; engagement_moved_to says where.
status
string
requiredopenin_progressresolvedpack_change
object
requiredThe rule pack change that opened this case: the version the engagement was compliant under, the version it isn't, and a plain words summary of what changed.
3 fields
from_version
string
requiredThe rule pack version the engagement was compliant under before the change, such as 2026.08.1. summary says what moved between this and to_version.
to_version
string
requiredThe rule pack version whose publication opened this case, such as 2026.08.1. The engagement was compliant under from_version and is not under this one.
summary
string
requiredWhat actually changed in the rules, in plain words.
why_non_compliant
string
optionalWhy the engagement no longer complies under the new pack version, in plain words. Read it with pack_change.summary to see which rule change caused it.
required_action
string
requiredWhat you need to do to bring the engagement back into compliance, such as draft a variation reclassifying this engagement as employment. Report what you did as action_taken when you close the case.
engagement_moved_to
string
optionalTransitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.
classifiedpaperedexecutedregisteredcurrentlapseddisputedterminatedopened_at
string · date-time
optionalWhen the case was opened, as an RFC 3339 timestamp in UTC.
resolved_by
string · nullable
optionalWho closed the case: the actor that called resolve. null until the case is resolved.
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: limit (optional), starting_after (optional), status (optional)
curl -X GET "https://sandbox.droomwork.io/v1/rail/remediation_cases?limit=25&status=open" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
// query parameters: limit (optional), starting_after (optional), status (optional)
const result = await api.railRemediationCasesList({ limit: 25, status: 'open' });// query parameters: limit (optional), starting_after (optional), status (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/remediation_cases?limit=25&status=open', {
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.RAILApi(client)
# query parameters: limit (optional), starting_after (optional), status (optional)
result = api.rail_remediation_cases_list(limit=25, status='open')import os
import requests
# query parameters: limit (optional), starting_after (optional), status (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/remediation_cases?limit=25&status=open',
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\RAILApi(new GuzzleHttp\Client(), $config);
# query parameters: limit (optional), starting_after (optional), status (optional)
$result = $api->railRemediationCasesList(limit: 25, status: 'open');<?php
// query parameters: limit (optional), starting_after (optional), status (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/remediation_cases?limit=25&status=open');
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.RailApi;
import com.droomwork.sdk.model.*;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
// query parameters: limit (optional), starting_after (optional), status (optional)
var result = api.railRemediationCasesList(25, null, RailRemediationStatus.fromValue("open"));// query parameters: limit (optional), starting_after (optional), status (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/remediation_cases?limit=25&status=open"))
.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 RAILApi(config);
// query parameters: limit (optional), starting_after (optional), status (optional)
var result = api.RailRemediationCasesList(limit: 25, status: RailRemediationStatus.Open);// query parameters: limit (optional), starting_after (optional), status (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/remediation_cases?limit=25&status=open");
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: limit (optional), starting_after (optional), status (optional)
result, _, err := client.RAILAPI.RailRemediationCasesList(ctx).Limit(25).Status(droomwork.RailRemediationStatus("open")).Execute()// query parameters: limit (optional), starting_after (optional), status (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/remediation_cases?limit=25&status=open", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "remediation_case",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"status": "open",
"pack_change": {
"from_version": "2026.08.1",
"to_version": "2026.08.1",
"summary": "example"
},
"required_action": "draft a variation reclassifying this engagement as employment",
"why_non_compliant": "example",
"engagement_moved_to": "classified",
"opened_at": "2026-09-01T09:00:00Z",
"resolved_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
],
"has_more": true
}
/v1/rail/remediation_cases/{remediation_case_id}#Retrieve a remediation case
rail.remediation_cases.retrieve
What changed in the pack, what it means for this engagement, and what you need to do.
Path parameters
remediation_case_id
string
requiredThe case's id, as listed at GET /v1/rail/remediation_cases or carried in data on the remediation_case.opened event. It starts with rail_engagement_.
Returns
The case.
id
string
requiredThe case's identifier, starting with rail_engagement_, as listed at GET /v1/rail/remediation_cases or in data on the remediation_case.opened event. Pass it as remediation_case_id to retrieve or resolve it; it never changes.
object
always "remediation_case"
requiredAlways remediation_case. Tells you which kind of record you are looking at, so one handler can read any response.
engagement_id
string
requiredThe id of the engagement the rule pack change made non compliant, starting with rail_engagement_, as returned by POST /v1/rail/engagements. Its status has moved; engagement_moved_to says where.
status
string
requiredopenin_progressresolvedpack_change
object
requiredThe rule pack change that opened this case: the version the engagement was compliant under, the version it isn't, and a plain words summary of what changed.
3 fields
from_version
string
requiredThe rule pack version the engagement was compliant under before the change, such as 2026.08.1. summary says what moved between this and to_version.
to_version
string
requiredThe rule pack version whose publication opened this case, such as 2026.08.1. The engagement was compliant under from_version and is not under this one.
summary
string
requiredWhat actually changed in the rules, in plain words.
why_non_compliant
string
optionalWhy the engagement no longer complies under the new pack version, in plain words. Read it with pack_change.summary to see which rule change caused it.
required_action
string
requiredWhat you need to do to bring the engagement back into compliance, such as draft a variation reclassifying this engagement as employment. Report what you did as action_taken when you close the case.
engagement_moved_to
string
optionalTransitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.
classifiedpaperedexecutedregisteredcurrentlapseddisputedterminatedopened_at
string · date-time
optionalWhen the case was opened, as an RFC 3339 timestamp in UTC.
resolved_by
string · nullable
optionalWho closed the case: the actor that called resolve. null until the case is resolved.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railRemediationCasesRetrieve({ remediationCaseId: '{remediation_case_id}' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D', {
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.RAILApi(client)
result = api.rail_remediation_cases_retrieve(remediation_case_id='{remediation_case_id}')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railRemediationCasesRetrieve(remediation_case_id: '{remediation_case_id}');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railRemediationCasesRetrieve("{remediation_case_id}");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D"))
.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 RAILApi(config);
var result = api.RailRemediationCasesRetrieve(remediationCaseId: "{remediation_case_id}");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D");
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.RAILAPI.RailRemediationCasesRetrieve(ctx, "{remediation_case_id}").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "remediation_case",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"status": "open",
"pack_change": {
"from_version": "2026.08.1",
"to_version": "2026.08.1",
"summary": "example"
},
"required_action": "draft a variation reclassifying this engagement as employment",
"why_non_compliant": "example",
"engagement_moved_to": "classified",
"opened_at": "2026-09-01T09:00:00Z",
"resolved_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
/v1/rail/remediation_cases/{remediation_case_id}/resolve#Close a remediation case
rail.remediation_cases.resolve
Records what you did and who did it.
Path parameters
remediation_case_id
string
requiredThe case's id, as listed at GET /v1/rail/remediation_cases or carried in data on the remediation_case.opened event. It starts with rail_engagement_.
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
action_taken
string
requiredWhat you did to bring the engagement back into compliance, in your words. It is recorded on the case with who did it, so write what a reviewer would need to read.
new_instrument_id
string
optionalThe id of the instrument you drafted to remedy the case, starting with rail_instrument_, from rail.instruments.supersede or rail.instruments.create. Leave it out when no new instrument was needed.
Returns
The resolved case.
id
string
requiredThe case's identifier, starting with rail_engagement_, as listed at GET /v1/rail/remediation_cases or in data on the remediation_case.opened event. Pass it as remediation_case_id to retrieve or resolve it; it never changes.
object
always "remediation_case"
requiredAlways remediation_case. Tells you which kind of record you are looking at, so one handler can read any response.
engagement_id
string
requiredThe id of the engagement the rule pack change made non compliant, starting with rail_engagement_, as returned by POST /v1/rail/engagements. Its status has moved; engagement_moved_to says where.
status
string
requiredopenin_progressresolvedpack_change
object
requiredThe rule pack change that opened this case: the version the engagement was compliant under, the version it isn't, and a plain words summary of what changed.
3 fields
from_version
string
requiredThe rule pack version the engagement was compliant under before the change, such as 2026.08.1. summary says what moved between this and to_version.
to_version
string
requiredThe rule pack version whose publication opened this case, such as 2026.08.1. The engagement was compliant under from_version and is not under this one.
summary
string
requiredWhat actually changed in the rules, in plain words.
why_non_compliant
string
optionalWhy the engagement no longer complies under the new pack version, in plain words. Read it with pack_change.summary to see which rule change caused it.
required_action
string
requiredWhat you need to do to bring the engagement back into compliance, such as draft a variation reclassifying this engagement as employment. Report what you did as action_taken when you close the case.
engagement_moved_to
string
optionalTransitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.
classifiedpaperedexecutedregisteredcurrentlapseddisputedterminatedopened_at
string · date-time
optionalWhen the case was opened, as an RFC 3339 timestamp in UTC.
resolved_by
string · nullable
optionalWho closed the case: the actor that called resolve. null until the case is resolved.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D/resolve" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"action_taken":"example","new_instrument_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}'import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railRemediationCasesResolve({
remediationCaseId: '{remediation_case_id}',
idempotencyKey: crypto.randomUUID(),
railRemediationResolveRequest: {"actionTaken":"example","newInstrumentId":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"},
});const response = await fetch('https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D/resolve', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"action_taken": "example",
"new_instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}),
});
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.RAILApi(client)
result = api.rail_remediation_cases_resolve(remediation_case_id='{remediation_case_id}', body={"action_taken": "example", "new_instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"})import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D/resolve',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"action_taken": "example", "new_instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"},
)
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railRemediationCasesResolve($idempotencyKey, json_decode('{"action_taken":"example","new_instrument_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D/resolve');
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 => '{"action_taken":"example","new_instrument_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}',
]);
$result = json_decode(curl_exec($ch), true);import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railRemediationCasesResolve("{remediation_case_id}", idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D/resolve"))
.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("""
{
"action_taken": "example",
"new_instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
"""))
.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 RAILApi(config);
var result = api.RailRemediationCasesResolve(remediationCaseId: "{remediation_case_id}", idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D/resolve");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"action_taken": "example",
"new_instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
""", 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.RAILAPI.RailRemediationCasesResolve(ctx, "{remediation_case_id}").IdempotencyKey(key).RailRemediationResolveRequest(body).Execute()body := strings.NewReader(`{
"action_taken": "example",
"new_instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D/resolve", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "remediation_case",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"status": "open",
"pack_change": {
"from_version": "2026.08.1",
"to_version": "2026.08.1",
"summary": "example"
},
"required_action": "draft a variation reclassifying this engagement as employment",
"why_non_compliant": "example",
"engagement_moved_to": "classified",
"opened_at": "2026-09-01T09:00:00Z",
"resolved_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
/v1/rail/gating_contract#What downstream modules require of an engagement
rail.gating_contract.retrieve
What each module needs an engagement to be before it will act. Dispatch needs an executed engagement. Payroll needs one that is registered or current. Read this before you build rather than finding out when a run is refused.
Returns
The gating contract.
object
always "gating_contract"
requiredAlways gating_contract. Tells you which kind of record you are looking at, so one handler can read any response.
version
string
requiredThe version of the gating contract these rules come from, such as 2026.08.1. Rules can change between versions, so record which one you built against.
rules
array of object
requiredThe rules, each naming a module and the engagement statuses it needs before it will act. Check your engagement's status against the rules for the module you are about to call.
4 fields
module
string
requiredThe module this rule applies to: match (allocation of work), run (payroll), remit (statutory remittance) or route (disbursement). That module refuses an engagement that is not in one of its requires_status statuses.
matchrunremitrouterequires_status
array of EngagementStatus
requiredThe engagement statuses the module will act on. An engagement in any other status is refused by that module, so compare its status with this list before you call.
detail
string
optionalThe rule in plain words, where there is more to say than the status list. Not present on every rule.
contract_row
string
requiredThe contract row this rule is published under, such as 6.1.13. Quote it when you need to point at where a rule comes from.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/rail/gating_contract" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railGatingContractRetrieve({});const response = await fetch('https://sandbox.droomwork.io/v1/rail/gating_contract', {
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.RAILApi(client)
result = api.rail_gating_contract_retrieve()import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/gating_contract',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railGatingContractRetrieve();<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/gating_contract');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railGatingContractRetrieve();var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/gating_contract"))
.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 RAILApi(config);
var result = api.RailGatingContractRetrieve();using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/gating_contract");
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.RAILAPI.RailGatingContractRetrieve(ctx).Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/gating_contract", 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": "gating_contract",
"version": "2026.08.1",
"rules": [
{
"module": "match",
"requires_status": [
"classified"
],
"contract_row": "6.1.13",
"detail": "The payee has no verified destination, so this line cannot be paid."
}
]
}
/v1/rail/readiness#What RAIL needs, what you have, and what is missing
rail.readiness.retrieve
Both parties need an anchored identity before an instrument can execute. This tells you whether that comes from ANCHOR or you supply it, and whether a clause library and classification pack are in force.
Returns
The readiness report.
object
always "readiness_report"
requiredAlways readiness_report. Tells you which kind of record you are looking at, so one handler can read any response.
module
string
requiredThe module whose readiness endpoint you called, such as GET /v1/payroll/readiness: anchor (identity), proof (credentials), rail (engagements), flow (sourcing), match (allocation), run (payroll), remit (remittance) or route (payouts).
anchorproofrailflowmatchrunremitroutemode
string
requiredintegrated means a required fact comes from the Droomwork module that owns it; standalone means you supply it yourself under an attestation. The checks are the same in both, and what each fact is worth is recorded on its row as held.
integratedstandaloneready
boolean
requiredtrue when every row is satisfied and the module has what it needs from you. false when a required fact is missing or held at too low an assurance; rows says which.
rows
array of ReadinessRow
requiredOne row per fact the module requires: who owns it, the assurance it needs, what you hold and whether that satisfies it. The rows that are not satisfied are what to bring.
8 fields of ReadinessRow
fact
string
requiredNamed for what it is, not for who supplies it.
owner
string
requiredThe sibling module that owns this fact when running integrated.
contract_row
string
optionalrequired
string
requiredHow a fact was established. Recorded on the fact rather than in configuration, so an attested identity and a verified one stay distinguishable a year later, which is the distinction that matters when something is disputed.
attestedverifiedheld
one of
requiredAssuranceorsource
string · nullable
optionalintegratedstandalonenullsatisfied
boolean
requiredmissing_because
string · nullable
optionalnot_suppliedassurance_too_lownullalways_enforced
array of string
optionalGates that hold in every mode and cannot be attested away. Consent before any source is queried, bank account validation, the tax identifiers returns are filed under, and duplicate detection within the organisation's own population.
clause_library_in_force
string · nullable
optionalNo instrument can be drafted without one.
classification_pack_in_force
string · nullable
optionalThe classification rule pack in force, or null when there is none. No engagement can be classified without one, so check it before you call classify.
parties_without_identity
integer
optionalCounts both sides. An engaging party without an identity blocks execution too.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/rail/readiness" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railReadinessRetrieve({});const response = await fetch('https://sandbox.droomwork.io/v1/rail/readiness', {
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.RAILApi(client)
result = api.rail_readiness_retrieve()import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/readiness',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railReadinessRetrieve();<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/readiness');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railReadinessRetrieve();var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/readiness"))
.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 RAILApi(config);
var result = api.RailReadinessRetrieve();using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/readiness");
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.RAILAPI.RailReadinessRetrieve(ctx).Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/readiness", 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": "readiness_report",
"module": "anchor",
"mode": "integrated",
"ready": true,
"rows": [
{
"fact": "anchored_subject",
"owner": "anchor",
"required": "attested",
"held": "attested",
"satisfied": true,
"contract_row": "6.1.13",
"source": "integrated",
"missing_because": "not_supplied"
}
],
"always_enforced": [
"example"
],
"clause_library_in_force": "example",
"classification_pack_in_force": "example",
"parties_without_identity": 1
}
/v1/rail/engagements/{engagement_id}#Retrieve an engagement
rail.engagements.retrieve
The engagement in its current state.
Pass as_of to get what was in force on a given date.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
Query parameters
as_of
string
optionalAnswer as at this date rather than now, as a calendar date in YYYY-MM-DD: you get the engagement as it stood on that date. Leave it out to get it as it stands now.
Returns
The engagement.
id
string
requiredThe engagement's identifier, starting with rail_engagement_. You get it from POST /v1/rail/engagements and pass it as engagement_id on every call about this engagement; it never changes.
object
always "engagement"
requiredAlways engagement. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
status
string
requiredTransitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.
classifiedpaperedexecutedregisteredcurrentlapseddisputedterminatedengaging_party_ref
string
requiredThe party engaging the worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the worker.
worker_ref
string
requiredThe worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the engaging party.
classification
one of
optionalThe classification once classify has run, or null before then: employment, fixed_term_employment, independent_contracting, apprenticeship, casual or task_based. It decides which instrument you can paper.
Classificationorclassification_review_required
boolean
optionalTrue when confidence was low or the result was contested. A person decides.
instrument_in_force_id
string · nullable
optionalThe id of the instrument in force, starting with rail_instrument_, from rail.instruments.create or, for a variation, rail.instruments.supersede. null until one is executed; a variation that supersedes it takes its place here once executed.
envelope_id
string · nullable
optionalThe id of the obligation envelope published for this engagement, starting with rail_envelope_; null until one is published. Retrieve it at GET /v1/rail/engagements/{engagement_id}/envelope or list it at GET /v1/rail/envelopes.
starts_on
string · date
optionalThe date the engagement starts, as a calendar date in YYYY-MM-DD. As you sent it when you opened the engagement.
ends_on
string · date · nullable
optionalThe date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.
as_of
string · date · nullable
optionalPresent when this was answered as at a past date rather than now.
created_at
string · date-time
optionalWhen the record was created, as an RFC 3339 timestamp in UTC.
Other responses
Errors it can return
# query parameters: as_of (optional)
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z?as_of=Tue%20Sep%2001%202026%2001%3A00%3A00%20GMT%2B0100%20(West%20Africa%20Time)" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
// query parameters: as_of (optional)
const result = await api.railEngagementsRetrieve({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', asOf: 'Tue Sep 01 2026 01:00:00 GMT+0100 (West Africa Time)' });// query parameters: as_of (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z?as_of=Tue%20Sep%2001%202026%2001%3A00%3A00%20GMT%2B0100%20(West%20Africa%20Time)', {
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.RAILApi(client)
# query parameters: as_of (optional)
result = api.rail_engagements_retrieve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', as_of='Tue Sep 01 2026 01:00:00 GMT+0100 (West Africa Time)')import os
import requests
# query parameters: as_of (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z?as_of=Tue%20Sep%2001%202026%2001%3A00%3A00%20GMT%2B0100%20(West%20Africa%20Time)',
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\RAILApi(new GuzzleHttp\Client(), $config);
# query parameters: as_of (optional)
$result = $api->railEngagementsRetrieve(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', as_of: 'Tue Sep 01 2026 01:00:00 GMT+0100 (West Africa Time)');<?php
// query parameters: as_of (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z?as_of=Tue%20Sep%2001%202026%2001%3A00%3A00%20GMT%2B0100%20(West%20Africa%20Time)');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
// query parameters: as_of (optional)
var result = api.railEngagementsRetrieve("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "Tue Sep 01 2026 01:00:00 GMT+0100 (West Africa Time)");// query parameters: as_of (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z?as_of=Tue%20Sep%2001%202026%2001%3A00%3A00%20GMT%2B0100%20(West%20Africa%20Time)"))
.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 RAILApi(config);
// query parameters: as_of (optional)
var result = api.RailEngagementsRetrieve(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", asOf: "Tue Sep 01 2026 01:00:00 GMT+0100 (West Africa Time)");// query parameters: as_of (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z?as_of=Tue%20Sep%2001%202026%2001%3A00%3A00%20GMT%2B0100%20(West%20Africa%20Time)");
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: as_of (optional)
result, _, err := client.RAILAPI.RailEngagementsRetrieve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").AsOf("Tue Sep 01 2026 01:00:00 GMT+0100 (West Africa Time)").Execute()// query parameters: as_of (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z?as_of=Tue%20Sep%2001%202026%2001%3A00%3A00%20GMT%2B0100%20(West%20Africa%20Time)", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "engagement",
"livemode": true,
"mocked": true,
"status": "classified",
"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"classification": "employment",
"classification_review_required": true,
"instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"starts_on": "2026-09-01",
"ends_on": "2026-09-01",
"as_of": "2026-09-01",
"created_at": "2026-09-01T09:00:00Z"
}
/v1/rail/engagements/{engagement_id}/classify#Classify the engagement
rail.engagements.classify
Gives you a classification from the versioned pack. No model decides it and you can't assert one yourself.
A low confidence or contested result is held for review and the engagement waits. You can't draft on it until the review is done.
Classification on its own confers no rights. An unexecuted instrument binds no one.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
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 engagement, classified or awaiting review.
id
string
requiredThe engagement's identifier, starting with rail_engagement_. You get it from POST /v1/rail/engagements and pass it as engagement_id on every call about this engagement; it never changes.
object
always "engagement"
requiredAlways engagement. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
status
string
requiredTransitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.
classifiedpaperedexecutedregisteredcurrentlapseddisputedterminatedengaging_party_ref
string
requiredThe party engaging the worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the worker.
worker_ref
string
requiredThe worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the engaging party.
classification
one of
optionalThe classification once classify has run, or null before then: employment, fixed_term_employment, independent_contracting, apprenticeship, casual or task_based. It decides which instrument you can paper.
Classificationorclassification_review_required
boolean
optionalTrue when confidence was low or the result was contested. A person decides.
instrument_in_force_id
string · nullable
optionalThe id of the instrument in force, starting with rail_instrument_, from rail.instruments.create or, for a variation, rail.instruments.supersede. null until one is executed; a variation that supersedes it takes its place here once executed.
envelope_id
string · nullable
optionalThe id of the obligation envelope published for this engagement, starting with rail_envelope_; null until one is published. Retrieve it at GET /v1/rail/engagements/{engagement_id}/envelope or list it at GET /v1/rail/envelopes.
starts_on
string · date
optionalThe date the engagement starts, as a calendar date in YYYY-MM-DD. As you sent it when you opened the engagement.
ends_on
string · date · nullable
optionalThe date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.
as_of
string · date · nullable
optionalPresent when this was answered as at a past date rather than now.
created_at
string · date-time
optionalWhen 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/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classify" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railEngagementsClassify({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classify', {
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.RAILApi(client)
result = api.rail_engagements_classify(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classify',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railEngagementsClassify(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classify');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railEngagementsClassify("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classify"))
.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 RAILApi(config);
var result = api.RailEngagementsClassify(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classify");
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.RAILAPI.RailEngagementsClassify(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classify", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "engagement",
"livemode": true,
"mocked": true,
"status": "classified",
"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"classification": "employment",
"classification_review_required": true,
"instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"starts_on": "2026-09-01",
"ends_on": "2026-09-01",
"as_of": "2026-09-01",
"created_at": "2026-09-01T09:00:00Z"
}
/v1/rail/engagements/{engagement_id}/classification#Retrieve the classification
rail.classifications.retrieve
The classification, the confidence, and the pack that decided it.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
Returns
The classification.
object
always "classification"
requiredAlways classification. Tells you which kind of record you are looking at, so one handler can read any response.
engagement_id
string
requiredThe engagement this classification was decided for: its id from POST /v1/rail/engagements, starting with rail_engagement_, and the engagement_id you passed in the path.
classification
string
requiredDecided by the versioned pack. No model decides it and you can't assert one yourself.
employmentfixed_term_employmentindependent_contractingapprenticeshipcasualtask_basedconfidence
string
requiredHow firmly the indicators pointed one way: high, medium, low, or contested when they pointed different ways. A low or contested result is held for review, and you can't draft until the review is done.
highmediumlowcontestedrule_pack
object
requiredThe rule pack that decided this classification: its pack_id, version and, where it carries one, the legal_approval_reference. Keep it; it is what you cite if the classification is questioned.
3 fields
pack_id
string
requiredWhich rule pack decided the classification, by its identifier, such as ng-classification. We publish the packs and you never send this; with version, it names exactly what your facts were judged against.
version
string
requiredThe version of the pack the facts were judged against, such as 2026.08.1. The same version appears as rule_pack_version on the rationale.
legal_approval_reference
string
optionalThe reference under which this pack version was legally approved, such as paye-2026-09-rivers. Cite it with the version if the classification is questioned.
decided_by
always "rule_pack"
requiredAlways the pack. No model determines a classification and you can't assert one. Where a person reviewed a low confidence result, they appear as the reviewer and not as the decider.
reviewed_by
string · nullable
optionalThe person who reviewed the result where confidence was low or contested, or null when no review was needed. A review never changes who decided: decided_by is always the pack.
decided_at
string · date-time
optionalWhen the pack decided the classification, as an RFC 3339 timestamp in UTC.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railClassificationsRetrieve({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification', {
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.RAILApi(client)
result = api.rail_classifications_retrieve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railClassificationsRetrieve(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railClassificationsRetrieve("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification"))
.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 RAILApi(config);
var result = api.RailClassificationsRetrieve(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification");
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.RAILAPI.RailClassificationsRetrieve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification", 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": "classification",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"classification": "employment",
"confidence": "high",
"rule_pack": {
"pack_id": "ng-classification",
"version": "2026.08.1",
"legal_approval_reference": "paye-2026-09-rivers"
},
"decided_by": "rule_pack",
"reviewed_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"decided_at": "2026-09-01T09:00:00Z"
}
/v1/rail/engagements/{engagement_id}/classification/rationale#Retrieve the full classification rationale
rail.classifications.retrieve_rationale
Every indicator considered, whether it fired, its weight, the pack version and the facts you attested to.
This is what an inspector asks for and what an appeal argues with.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
Returns
The rationale.
object
always "classification_rationale"
requiredAlways classification_rationale. Tells you which kind of record you are looking at, so one handler can read any response.
engagement_id
string
requiredThe engagement this rationale belongs to: its id from POST /v1/rail/engagements, starting with rail_engagement_, and the engagement_id you passed in the path.
indicators
array of object
requiredEvery indicator the pack considered, one entry each: whether it fired, its weight, the classification it points toward and, where named, the fact it read. Nothing is left out or summarised.
5 fields
indicator
string
requiredThe indicator's name, such as can_send_a_substitute. Each one is a single test the pack applied to the facts you attested to.
fired
boolean
requiredtrue when the facts you attested to met this indicator, false when they didn't. An indicator that didn't fire is still listed, so you can see everything that was considered.
weight
number
requiredHow much this indicator counts toward points_toward when it fires, as a number set by the pack version. A heavier indicator moves the classification more.
points_toward
string
requiredDecided by the versioned pack. No model decides it and you can't assert one yourself.
employmentfixed_term_employmentindependent_contractingapprenticeshipcasualtask_basedsource_fact
string
optionalThe attested fact this indicator was judged on, so you can trace the result back to what you stated in attested_facts. Not present on every indicator.
rule_pack_version
string
requiredThe version of the pack every indicator here was judged against, such as 2026.08.1. It matches rule_pack.version on the classification.
attested_facts
AttestedFacts
requiredThe facts the classification reads. You state them, and they're kept permanently with who stated them.
9 fields of AttestedFacts
controls_how_work_is_done
boolean
optionaltrue when the engaging party directs how the work is carried out, not only what is delivered. Control is one of the indicators the classification reads.
controls_when_work_is_done
boolean
optionaltrue when the engaging party sets the hours or days the work is done, rather than the worker choosing them.
provides_equipment
boolean
optionaltrue when the engaging party supplies the tools and equipment the work is done with, rather than the worker bringing their own.
exclusive_to_this_party
boolean
optionaltrue when the worker works for this engaging party alone and takes no engagements from anyone else.
can_send_a_substitute
boolean
optionaltrue when the worker may send someone else to do the work in their place, rather than having to do it personally.
bears_financial_risk
boolean
optionaltrue when the worker carries the financial risk of the work: their own costs, and the loss if it goes wrong or is not paid for.
integrated_into_organisation
boolean
optionaltrue when the worker is part of the engaging party's organisation, its teams and its management, rather than an outside supplier to it.
duration_months
integer · minimum 0
optionalHow long the engagement is expected to run, in whole months, 0 or more.
occupation_code
string
optionalThe occupation the work falls under, as a code such as ng-7412. Give the code for the work actually done: the classification reads it with the other facts.
attesting_individual
string
optionalThe named person who attested to the facts, as you gave them on the attestation. A person, not a system account, and recorded permanently.
attested_at
string · date-time
optionalWhen the facts were attested to, as an RFC 3339 timestamp in UTC.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification/rationale" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railClassificationsRetrieveRationale({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification/rationale', {
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.RAILApi(client)
result = api.rail_classifications_retrieve_rationale(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification/rationale',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railClassificationsRetrieveRationale(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification/rationale');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railClassificationsRetrieveRationale("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification/rationale"))
.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 RAILApi(config);
var result = api.RailClassificationsRetrieveRationale(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification/rationale");
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.RAILAPI.RailClassificationsRetrieveRationale(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification/rationale", 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": "classification_rationale",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"indicators": [
{
"indicator": "can_send_a_substitute",
"fired": true,
"weight": 1,
"points_toward": "employment",
"source_fact": "example"
}
],
"rule_pack_version": "2026.08.1",
"attested_facts": {
"controls_how_work_is_done": true,
"controls_when_work_is_done": true,
"provides_equipment": true,
"exclusive_to_this_party": true,
"can_send_a_substitute": true,
"bears_financial_risk": true,
"integrated_into_organisation": true,
"duration_months": 0,
"occupation_code": "ng-7412"
},
"attesting_individual": "example",
"attested_at": "2026-09-01T09:00:00Z"
}
/v1/rail/engagements/{engagement_id}/terminate#Terminate an engagement
rail.engagements.terminate
Refused while the engagement is disputed. A dispute suspends termination until it resolves, so a disagreement can't be ended by ending the relationship.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
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
reason
string
requiredWhy you are ending the engagement, in your words. It is recorded on the termination, so write what a reviewer would need to read.
effective_on
string · date
requiredThe date the engagement ends, as YYYY-MM-DD. Name the day the relationship ends, not the day you make the call.
Returns
The terminated engagement.
id
string
requiredThe engagement's identifier, starting with rail_engagement_. You get it from POST /v1/rail/engagements and pass it as engagement_id on every call about this engagement; it never changes.
object
always "engagement"
requiredAlways engagement. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
status
string
requiredTransitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.
classifiedpaperedexecutedregisteredcurrentlapseddisputedterminatedengaging_party_ref
string
requiredThe party engaging the worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the worker.
worker_ref
string
requiredThe worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the engaging party.
classification
one of
optionalThe classification once classify has run, or null before then: employment, fixed_term_employment, independent_contracting, apprenticeship, casual or task_based. It decides which instrument you can paper.
Classificationorclassification_review_required
boolean
optionalTrue when confidence was low or the result was contested. A person decides.
instrument_in_force_id
string · nullable
optionalThe id of the instrument in force, starting with rail_instrument_, from rail.instruments.create or, for a variation, rail.instruments.supersede. null until one is executed; a variation that supersedes it takes its place here once executed.
envelope_id
string · nullable
optionalThe id of the obligation envelope published for this engagement, starting with rail_envelope_; null until one is published. Retrieve it at GET /v1/rail/engagements/{engagement_id}/envelope or list it at GET /v1/rail/envelopes.
starts_on
string · date
optionalThe date the engagement starts, as a calendar date in YYYY-MM-DD. As you sent it when you opened the engagement.
ends_on
string · date · nullable
optionalThe date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.
as_of
string · date · nullable
optionalPresent when this was answered as at a past date rather than now.
created_at
string · date-time
optionalWhen 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/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/terminate" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"reason":"The requester confirmed the work in person.","effective_on":"2026-09-01"}'import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railEngagementsTerminate({
engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
idempotencyKey: crypto.randomUUID(),
railTerminateRequest: {"reason":"The requester confirmed the work in person.","effectiveOn":"2026-09-01"},
});const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/terminate', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"reason": "The requester confirmed the work in person.",
"effective_on": "2026-09-01"
}),
});
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.RAILApi(client)
result = api.rail_engagements_terminate(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"reason": "The requester confirmed the work in person.", "effective_on": "2026-09-01"})import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/terminate',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"reason": "The requester confirmed the work in person.", "effective_on": "2026-09-01"},
)
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railEngagementsTerminate($idempotencyKey, json_decode('{"reason":"The requester confirmed the work in person.","effective_on":"2026-09-01"}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/terminate');
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 => '{"reason":"The requester confirmed the work in person.","effective_on":"2026-09-01"}',
]);
$result = json_decode(curl_exec($ch), true);import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railEngagementsTerminate("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/terminate"))
.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("""
{
"reason": "The requester confirmed the work in person.",
"effective_on": "2026-09-01"
}
"""))
.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 RAILApi(config);
var result = api.RailEngagementsTerminate(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/terminate");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"reason": "The requester confirmed the work in person.",
"effective_on": "2026-09-01"
}
""", 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.RAILAPI.RailEngagementsTerminate(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).RailTerminateRequest(body).Execute()body := strings.NewReader(`{
"reason": "The requester confirmed the work in person.",
"effective_on": "2026-09-01"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/terminate", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "engagement",
"livemode": true,
"mocked": true,
"status": "classified",
"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"classification": "employment",
"classification_review_required": true,
"instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"starts_on": "2026-09-01",
"ends_on": "2026-09-01",
"as_of": "2026-09-01",
"created_at": "2026-09-01T09:00:00Z"
}
/v1/rail/engagements/{engagement_id}/attestations#Attest to the facts relied upon
rail.attestations.create
State the facts the classification will read. The attesting individual, the timestamp and the version of the attestation wording are recorded permanently.
If no other Droomwork module supplies the facts, this is where you send them. The wording is versioned and effective dated, so the record pins the version you attested under.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
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
facts
AttestedFacts
requiredThe facts the classification reads. You state them, and they're kept permanently with who stated them.
9 fields of AttestedFacts
controls_how_work_is_done
boolean
optionaltrue when the engaging party directs how the work is carried out, not only what is delivered. Control is one of the indicators the classification reads.
controls_when_work_is_done
boolean
optionaltrue when the engaging party sets the hours or days the work is done, rather than the worker choosing them.
provides_equipment
boolean
optionaltrue when the engaging party supplies the tools and equipment the work is done with, rather than the worker bringing their own.
exclusive_to_this_party
boolean
optionaltrue when the worker works for this engaging party alone and takes no engagements from anyone else.
can_send_a_substitute
boolean
optionaltrue when the worker may send someone else to do the work in their place, rather than having to do it personally.
bears_financial_risk
boolean
optionaltrue when the worker carries the financial risk of the work: their own costs, and the loss if it goes wrong or is not paid for.
integrated_into_organisation
boolean
optionaltrue when the worker is part of the engaging party's organisation, its teams and its management, rather than an outside supplier to it.
duration_months
integer · minimum 0
optionalHow long the engagement is expected to run, in whole months, 0 or more.
occupation_code
string
optionalThe occupation the work falls under, as a code such as ng-7412. Give the code for the work actually done: the classification reads it with the other facts.
attesting_individual
string
requiredThe named person attesting to these facts, not a system account. Recorded permanently with the facts and the timestamp.
wording_version
string
requiredThe version of the attestation wording the person accepted, such as 2026.08.1. The record pins this version, so what they accepted is answered from the record, never from the current page.
Returns
The attestation.
id
string
requiredThe attestation's identifier, returned when you attested at POST /v1/rail/engagements/{engagement_id}/attestations. It never changes; pass it as attestation_id to GET /v1/rail/engagements/{engagement_id}/attestations/{attestation_id}.
object
always "attestation"
requiredAlways attestation. Tells you which kind of record you are looking at, so one handler can read any response.
engagement_id
string
requiredThe engagement these facts were attested for: its id from POST /v1/rail/engagements, starting with rail_engagement_, and the engagement_id in the path you attested under.
facts
AttestedFacts
optionalThe facts the classification reads. You state them, and they're kept permanently with who stated them.
9 fields of AttestedFacts
controls_how_work_is_done
boolean
optionaltrue when the engaging party directs how the work is carried out, not only what is delivered. Control is one of the indicators the classification reads.
controls_when_work_is_done
boolean
optionaltrue when the engaging party sets the hours or days the work is done, rather than the worker choosing them.
provides_equipment
boolean
optionaltrue when the engaging party supplies the tools and equipment the work is done with, rather than the worker bringing their own.
exclusive_to_this_party
boolean
optionaltrue when the worker works for this engaging party alone and takes no engagements from anyone else.
can_send_a_substitute
boolean
optionaltrue when the worker may send someone else to do the work in their place, rather than having to do it personally.
bears_financial_risk
boolean
optionaltrue when the worker carries the financial risk of the work: their own costs, and the loss if it goes wrong or is not paid for.
integrated_into_organisation
boolean
optionaltrue when the worker is part of the engaging party's organisation, its teams and its management, rather than an outside supplier to it.
duration_months
integer · minimum 0
optionalHow long the engagement is expected to run, in whole months, 0 or more.
occupation_code
string
optionalThe occupation the work falls under, as a code such as ng-7412. Give the code for the work actually done: the classification reads it with the other facts.
attesting_individual
string
requiredThe named person who attested to these facts. Recorded permanently; a person, not a system account.
wording_version
string
requiredVersioned and effective dated. What someone accepted is answered from the record rather than from the current page.
attested_at
string · date-time
requiredWhen the facts were attested, as an RFC 3339 timestamp in UTC. Recorded permanently beside the individual and the wording version.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"facts":{"controls_how_work_is_done":true,"controls_when_work_is_done":true,"provides_equipment":true,"exclusive_to_this_party":true,"can_send_a_substitute":true,"bears_financial_risk":true,"integrated_into_organisation":true,"duration_months":0,"occupation_code":"ng-7412"},"attesting_individual":"example","wording_version":"2026.08.1"}'import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railAttestationsCreate({
engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
idempotencyKey: crypto.randomUUID(),
railAttestationCreateRequest: {"facts":{"controlsHowWorkIsDone":true,"controlsWhenWorkIsDone":true,"providesEquipment":true,"exclusiveToThisParty":true,"canSendASubstitute":true,"bearsFinancialRisk":true,"integratedIntoOrganisation":true,"durationMonths":0,"occupationCode":"ng-7412"},"attestingIndividual":"example","wordingVersion":"2026.08.1"},
});const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"facts": {
"controls_how_work_is_done": true,
"controls_when_work_is_done": true,
"provides_equipment": true,
"exclusive_to_this_party": true,
"can_send_a_substitute": true,
"bears_financial_risk": true,
"integrated_into_organisation": true,
"duration_months": 0,
"occupation_code": "ng-7412"
},
"attesting_individual": "example",
"wording_version": "2026.08.1"
}),
});
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.RAILApi(client)
result = api.rail_attestations_create(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"facts": {"controls_how_work_is_done": True, "controls_when_work_is_done": True, "provides_equipment": True, "exclusive_to_this_party": True, "can_send_a_substitute": True, "bears_financial_risk": True, "integrated_into_organisation": True, "duration_months": 0, "occupation_code": "ng-7412"}, "attesting_individual": "example", "wording_version": "2026.08.1"})import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"facts": {"controls_how_work_is_done": True, "controls_when_work_is_done": True, "provides_equipment": True, "exclusive_to_this_party": True, "can_send_a_substitute": True, "bears_financial_risk": True, "integrated_into_organisation": True, "duration_months": 0, "occupation_code": "ng-7412"}, "attesting_individual": "example", "wording_version": "2026.08.1"},
)
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railAttestationsCreate($idempotencyKey, json_decode('{"facts":{"controls_how_work_is_done":true,"controls_when_work_is_done":true,"provides_equipment":true,"exclusive_to_this_party":true,"can_send_a_substitute":true,"bears_financial_risk":true,"integrated_into_organisation":true,"duration_months":0,"occupation_code":"ng-7412"},"attesting_individual":"example","wording_version":"2026.08.1"}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations');
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 => '{"facts":{"controls_how_work_is_done":true,"controls_when_work_is_done":true,"provides_equipment":true,"exclusive_to_this_party":true,"can_send_a_substitute":true,"bears_financial_risk":true,"integrated_into_organisation":true,"duration_months":0,"occupation_code":"ng-7412"},"attesting_individual":"example","wording_version":"2026.08.1"}',
]);
$result = json_decode(curl_exec($ch), true);import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railAttestationsCreate("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations"))
.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("""
{
"facts": {
"controls_how_work_is_done": true,
"controls_when_work_is_done": true,
"provides_equipment": true,
"exclusive_to_this_party": true,
"can_send_a_substitute": true,
"bears_financial_risk": true,
"integrated_into_organisation": true,
"duration_months": 0,
"occupation_code": "ng-7412"
},
"attesting_individual": "example",
"wording_version": "2026.08.1"
}
"""))
.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 RAILApi(config);
var result = api.RailAttestationsCreate(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"facts": {
"controls_how_work_is_done": true,
"controls_when_work_is_done": true,
"provides_equipment": true,
"exclusive_to_this_party": true,
"can_send_a_substitute": true,
"bears_financial_risk": true,
"integrated_into_organisation": true,
"duration_months": 0,
"occupation_code": "ng-7412"
},
"attesting_individual": "example",
"wording_version": "2026.08.1"
}
""", 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.RAILAPI.RailAttestationsCreate(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).RailAttestationCreateRequest(body).Execute()body := strings.NewReader(`{
"facts": {
"controls_how_work_is_done": true,
"controls_when_work_is_done": true,
"provides_equipment": true,
"exclusive_to_this_party": true,
"can_send_a_substitute": true,
"bears_financial_risk": true,
"integrated_into_organisation": true,
"duration_months": 0,
"occupation_code": "ng-7412"
},
"attesting_individual": "example",
"wording_version": "2026.08.1"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "attestation",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"attesting_individual": "example",
"wording_version": "2026.08.1",
"attested_at": "2026-09-01T09:00:00Z",
"facts": {
"controls_how_work_is_done": true,
"controls_when_work_is_done": true,
"provides_equipment": true,
"exclusive_to_this_party": true,
"can_send_a_substitute": true,
"bears_financial_risk": true,
"integrated_into_organisation": true,
"duration_months": 0,
"occupation_code": "ng-7412"
}
}
/v1/rail/engagements/{engagement_id}/attestations/{attestation_id}#Retrieve an attestation
rail.attestations.retrieve
What was attested, by whom, when, and under which wording version.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
attestation_id
string
requiredThe attestation's identifier: the id returned when you attested at POST /v1/rail/engagements/{engagement_id}/attestations.
Returns
The attestation.
id
string
requiredThe attestation's identifier, returned when you attested at POST /v1/rail/engagements/{engagement_id}/attestations. It never changes; pass it as attestation_id to GET /v1/rail/engagements/{engagement_id}/attestations/{attestation_id}.
object
always "attestation"
requiredAlways attestation. Tells you which kind of record you are looking at, so one handler can read any response.
engagement_id
string
requiredThe engagement these facts were attested for: its id from POST /v1/rail/engagements, starting with rail_engagement_, and the engagement_id in the path you attested under.
facts
AttestedFacts
optionalThe facts the classification reads. You state them, and they're kept permanently with who stated them.
9 fields of AttestedFacts
controls_how_work_is_done
boolean
optionaltrue when the engaging party directs how the work is carried out, not only what is delivered. Control is one of the indicators the classification reads.
controls_when_work_is_done
boolean
optionaltrue when the engaging party sets the hours or days the work is done, rather than the worker choosing them.
provides_equipment
boolean
optionaltrue when the engaging party supplies the tools and equipment the work is done with, rather than the worker bringing their own.
exclusive_to_this_party
boolean
optionaltrue when the worker works for this engaging party alone and takes no engagements from anyone else.
can_send_a_substitute
boolean
optionaltrue when the worker may send someone else to do the work in their place, rather than having to do it personally.
bears_financial_risk
boolean
optionaltrue when the worker carries the financial risk of the work: their own costs, and the loss if it goes wrong or is not paid for.
integrated_into_organisation
boolean
optionaltrue when the worker is part of the engaging party's organisation, its teams and its management, rather than an outside supplier to it.
duration_months
integer · minimum 0
optionalHow long the engagement is expected to run, in whole months, 0 or more.
occupation_code
string
optionalThe occupation the work falls under, as a code such as ng-7412. Give the code for the work actually done: the classification reads it with the other facts.
attesting_individual
string
requiredThe named person who attested to these facts. Recorded permanently; a person, not a system account.
wording_version
string
requiredVersioned and effective dated. What someone accepted is answered from the record rather than from the current page.
attested_at
string · date-time
requiredWhen the facts were attested, as an RFC 3339 timestamp in UTC. Recorded permanently beside the individual and the wording version.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations/%7Battestation_id%7D" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railAttestationsRetrieve({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', attestationId: '{attestation_id}' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations/%7Battestation_id%7D', {
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.RAILApi(client)
result = api.rail_attestations_retrieve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', attestation_id='{attestation_id}')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations/%7Battestation_id%7D',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railAttestationsRetrieve(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', attestation_id: '{attestation_id}');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations/%7Battestation_id%7D');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railAttestationsRetrieve("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{attestation_id}");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations/%7Battestation_id%7D"))
.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 RAILApi(config);
var result = api.RailAttestationsRetrieve(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", attestationId: "{attestation_id}");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations/%7Battestation_id%7D");
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.RAILAPI.RailAttestationsRetrieve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{attestation_id}").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations/%7Battestation_id%7D", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "attestation",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"attesting_individual": "example",
"wording_version": "2026.08.1",
"attested_at": "2026-09-01T09:00:00Z",
"facts": {
"controls_how_work_is_done": true,
"controls_when_work_is_done": true,
"provides_equipment": true,
"exclusive_to_this_party": true,
"can_send_a_substitute": true,
"bears_financial_risk": true,
"integrated_into_organisation": true,
"duration_months": 0,
"occupation_code": "ng-7412"
}
}
/v1/rail/engagements/{engagement_id}/instruments#Draft an instrument from the clause library
rail.instruments.create
Assembles a document from approved clauses. You choose a template, not wording; there is no field here that accepts prose.
A contractor agreement over facts classified as employment is refused. So is drafting before classification has completed.
The clause library version, the rule pack version and the legal approval reference are recorded on what you get back.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
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
template_code
string
requiredThe template to draft from, by its template_code in the clause library, such as no_payee_destination. Each template belongs to a classification; one that doesn't match the engagement's is refused.
clause_library_version
string
optionalDefaults to the version currently in force.
variables
object
optionalValues the template needs, such as dates and party names. Values only. A variable cannot introduce a clause and cannot change one.
expires_in_days
integer · minimum 1
optionalHow many days the draft stays open for assent and execution, at least 1. An instrument not executed by then lapses, and the lapse is recorded.
Returns
The drafted instrument, binding nobody until executed.
id
string
requiredThe instrument's identifier, starting with rail_instrument_, from rail.instruments.create or, for a variation, rail.instruments.supersede. Pass it as instrument_id on every call about this instrument; it never changes.
object
always "instrument"
requiredAlways instrument. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
engagement_id
string
requiredThe id of the engagement this instrument papers, starting with rail_engagement_, as returned by POST /v1/rail/engagements. A variation drafted by supersede stays on the same engagement.
status
string
requireddraftedawaiting_assentexecutedexpiredsupersededtemplate_code
string
requiredA template from the approved set. Not wording.
clauses
array of object
optionalThe clauses used, by reference. Every one comes from the approved library, as written there.
2 fields
clause_code
string
requiredThe code of a clause from the approved library. Look it up by the same clause_code in the clause library to read what the instrument contains.
clause_version
string
requiredThe version of that clause as it stood in the library the instrument was built from. The instrument holds this version even after the library moves on.
clause_library_version
string
requiredThe clause library version this instrument was assembled from, such as 2026.08.1. Pass it as version to the clause library endpoint to read exactly what was used.
rule_pack_version
string
optionalThe version of the rule pack this instrument rests on, such as 2026.08.1: the rules the classification behind it was judged against. It stays on the instrument once executed, whatever pack comes later.
legal_approval_reference
string
optionalThe reference of the legal approval behind the clause library this instrument was built from. Cite it when you're asked what approved the wording; it matches the library's own.
seal
one of
optionalThe tamper evident seal, applied when the instrument is executed; null until then. Send its hash to the verify endpoint to prove the document you hold is the one that was executed.
Sealorsupersedes
string · nullable
optionalThe id of the instrument this one was drafted as a variation of, starting with rail_instrument_, or null for an original. The earlier instrument is never edited or removed.
superseded_by
string · nullable
optionalThe id of the variation drafted to replace this instrument, starting with rail_instrument_, or null while none has been. Follow it forward to reach the newest version.
expires_at
string · date-time · nullable
optionalAn unexecuted instrument lapses after this, and the lapse is recorded.
executed_at
string · date-time · nullable
optionalWhen both parties' assent was complete and the instrument was executed, as an RFC 3339 timestamp in UTC. null until then; nothing binds anyone before it.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"template_code":"no_payee_destination","clause_library_version":"2026.08.1","variables":{},"expires_in_days":1}'import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railInstrumentsCreate({
engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
idempotencyKey: crypto.randomUUID(),
railInstrumentCreateRequest: {"templateCode":"no_payee_destination","clauseLibraryVersion":"2026.08.1","variables":{},"expiresInDays":1},
});const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"template_code": "no_payee_destination",
"clause_library_version": "2026.08.1",
"variables": {},
"expires_in_days": 1
}),
});
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.RAILApi(client)
result = api.rail_instruments_create(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"template_code": "no_payee_destination", "clause_library_version": "2026.08.1", "variables": {}, "expires_in_days": 1})import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"template_code": "no_payee_destination", "clause_library_version": "2026.08.1", "variables": {}, "expires_in_days": 1},
)
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railInstrumentsCreate($idempotencyKey, json_decode('{"template_code":"no_payee_destination","clause_library_version":"2026.08.1","variables":{},"expires_in_days":1}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments');
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 => '{"template_code":"no_payee_destination","clause_library_version":"2026.08.1","variables":{},"expires_in_days":1}',
]);
$result = json_decode(curl_exec($ch), true);import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railInstrumentsCreate("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments"))
.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("""
{
"template_code": "no_payee_destination",
"clause_library_version": "2026.08.1",
"variables": {},
"expires_in_days": 1
}
"""))
.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 RAILApi(config);
var result = api.RailInstrumentsCreate(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"template_code": "no_payee_destination",
"clause_library_version": "2026.08.1",
"variables": {},
"expires_in_days": 1
}
""", 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.RAILAPI.RailInstrumentsCreate(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).RailInstrumentCreateRequest(body).Execute()body := strings.NewReader(`{
"template_code": "no_payee_destination",
"clause_library_version": "2026.08.1",
"variables": {},
"expires_in_days": 1
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments", 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": "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "instrument",
"livemode": true,
"mocked": true,
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"status": "drafted",
"template_code": "no_payee_destination",
"clause_library_version": "2026.08.1",
"clauses": [
{
"clause_code": "no_payee_destination",
"clause_version": "2026.08.1"
}
],
"rule_pack_version": "2026.08.1",
"legal_approval_reference": "paye-2026-09-rivers",
"seal": {
"hash": "sha256:9f2c1e0043a1b8",
"previous_hash": "sha256:9f2c1e0043a1b8",
"sealed_at": "2026-09-01T09:00:00Z",
"algorithm": "sha256"
},
"supersedes": "example",
"superseded_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"expires_at": "2026-09-01T09:00:00Z",
"executed_at": "2026-09-01T09:00:00Z"
}
/v1/rail/engagements/{engagement_id}/instruments/{instrument_id}#Retrieve an instrument
rail.instruments.retrieve
The instrument, the clauses it was built from, and its seal once executed.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
instrument_id
string
requiredThe instrument's identifier: the id returned when you drafted it at POST /v1/rail/engagements/{engagement_id}/instruments, or listed at GET /v1/rail/instruments. It starts with rail_instrument_.
Returns
The instrument.
id
string
requiredThe instrument's identifier, starting with rail_instrument_, from rail.instruments.create or, for a variation, rail.instruments.supersede. Pass it as instrument_id on every call about this instrument; it never changes.
object
always "instrument"
requiredAlways instrument. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
engagement_id
string
requiredThe id of the engagement this instrument papers, starting with rail_engagement_, as returned by POST /v1/rail/engagements. A variation drafted by supersede stays on the same engagement.
status
string
requireddraftedawaiting_assentexecutedexpiredsupersededtemplate_code
string
requiredA template from the approved set. Not wording.
clauses
array of object
optionalThe clauses used, by reference. Every one comes from the approved library, as written there.
2 fields
clause_code
string
requiredThe code of a clause from the approved library. Look it up by the same clause_code in the clause library to read what the instrument contains.
clause_version
string
requiredThe version of that clause as it stood in the library the instrument was built from. The instrument holds this version even after the library moves on.
clause_library_version
string
requiredThe clause library version this instrument was assembled from, such as 2026.08.1. Pass it as version to the clause library endpoint to read exactly what was used.
rule_pack_version
string
optionalThe version of the rule pack this instrument rests on, such as 2026.08.1: the rules the classification behind it was judged against. It stays on the instrument once executed, whatever pack comes later.
legal_approval_reference
string
optionalThe reference of the legal approval behind the clause library this instrument was built from. Cite it when you're asked what approved the wording; it matches the library's own.
seal
one of
optionalThe tamper evident seal, applied when the instrument is executed; null until then. Send its hash to the verify endpoint to prove the document you hold is the one that was executed.
Sealorsupersedes
string · nullable
optionalThe id of the instrument this one was drafted as a variation of, starting with rail_instrument_, or null for an original. The earlier instrument is never edited or removed.
superseded_by
string · nullable
optionalThe id of the variation drafted to replace this instrument, starting with rail_instrument_, or null while none has been. Follow it forward to reach the newest version.
expires_at
string · date-time · nullable
optionalAn unexecuted instrument lapses after this, and the lapse is recorded.
executed_at
string · date-time · nullable
optionalWhen both parties' assent was complete and the instrument was executed, as an RFC 3339 timestamp in UTC. null until then; nothing binds anyone before it.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railInstrumentsRetrieve({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrumentId: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_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.RAILApi(client)
result = api.rail_instruments_retrieve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id='rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railInstrumentsRetrieve(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railInstrumentsRetrieve("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_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 RAILApi(config);
var result = api.RailInstrumentsRetrieve(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", instrumentId: "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_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.RAILAPI.RailInstrumentsRetrieve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_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": "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "instrument",
"livemode": true,
"mocked": true,
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"status": "drafted",
"template_code": "no_payee_destination",
"clause_library_version": "2026.08.1",
"clauses": [
{
"clause_code": "no_payee_destination",
"clause_version": "2026.08.1"
}
],
"rule_pack_version": "2026.08.1",
"legal_approval_reference": "paye-2026-09-rivers",
"seal": {
"hash": "sha256:9f2c1e0043a1b8",
"previous_hash": "sha256:9f2c1e0043a1b8",
"sealed_at": "2026-09-01T09:00:00Z",
"algorithm": "sha256"
},
"supersedes": "example",
"superseded_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"expires_at": "2026-09-01T09:00:00Z",
"executed_at": "2026-09-01T09:00:00Z"
}
/v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/execute#Execute an instrument once both parties have assented
rail.instruments.execute
Refused unless both parties hold a live, unrevoked Passport. That includes the engaging party, so check your own side first.
Execution seals the document with a tamper evident hash chained to earlier records. No manual legal step is needed to get here.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
instrument_id
string
requiredThe instrument's identifier: the id returned when you drafted it at POST /v1/rail/engagements/{engagement_id}/instruments, or listed at GET /v1/rail/instruments. It starts with rail_instrument_.
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 executed and sealed instrument.
id
string
requiredThe instrument's identifier, starting with rail_instrument_, from rail.instruments.create or, for a variation, rail.instruments.supersede. Pass it as instrument_id on every call about this instrument; it never changes.
object
always "instrument"
requiredAlways instrument. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
engagement_id
string
requiredThe id of the engagement this instrument papers, starting with rail_engagement_, as returned by POST /v1/rail/engagements. A variation drafted by supersede stays on the same engagement.
status
string
requireddraftedawaiting_assentexecutedexpiredsupersededtemplate_code
string
requiredA template from the approved set. Not wording.
clauses
array of object
optionalThe clauses used, by reference. Every one comes from the approved library, as written there.
2 fields
clause_code
string
requiredThe code of a clause from the approved library. Look it up by the same clause_code in the clause library to read what the instrument contains.
clause_version
string
requiredThe version of that clause as it stood in the library the instrument was built from. The instrument holds this version even after the library moves on.
clause_library_version
string
requiredThe clause library version this instrument was assembled from, such as 2026.08.1. Pass it as version to the clause library endpoint to read exactly what was used.
rule_pack_version
string
optionalThe version of the rule pack this instrument rests on, such as 2026.08.1: the rules the classification behind it was judged against. It stays on the instrument once executed, whatever pack comes later.
legal_approval_reference
string
optionalThe reference of the legal approval behind the clause library this instrument was built from. Cite it when you're asked what approved the wording; it matches the library's own.
seal
one of
optionalThe tamper evident seal, applied when the instrument is executed; null until then. Send its hash to the verify endpoint to prove the document you hold is the one that was executed.
Sealorsupersedes
string · nullable
optionalThe id of the instrument this one was drafted as a variation of, starting with rail_instrument_, or null for an original. The earlier instrument is never edited or removed.
superseded_by
string · nullable
optionalThe id of the variation drafted to replace this instrument, starting with rail_instrument_, or null while none has been. Follow it forward to reach the newest version.
expires_at
string · date-time · nullable
optionalAn unexecuted instrument lapses after this, and the lapse is recorded.
executed_at
string · date-time · nullable
optionalWhen both parties' assent was complete and the instrument was executed, as an RFC 3339 timestamp in UTC. null until then; nothing binds anyone before it.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railInstrumentsExecute({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrumentId: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute', {
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.RAILApi(client)
result = api.rail_instruments_execute(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id='rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railInstrumentsExecute(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railInstrumentsExecute("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute"))
.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 RAILApi(config);
var result = api.RailInstrumentsExecute(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", instrumentId: "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute");
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.RAILAPI.RailInstrumentsExecute(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute", 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": "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "instrument",
"livemode": true,
"mocked": true,
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"status": "drafted",
"template_code": "no_payee_destination",
"clause_library_version": "2026.08.1",
"clauses": [
{
"clause_code": "no_payee_destination",
"clause_version": "2026.08.1"
}
],
"rule_pack_version": "2026.08.1",
"legal_approval_reference": "paye-2026-09-rivers",
"seal": {
"hash": "sha256:9f2c1e0043a1b8",
"previous_hash": "sha256:9f2c1e0043a1b8",
"sealed_at": "2026-09-01T09:00:00Z",
"algorithm": "sha256"
},
"supersedes": "example",
"superseded_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"expires_at": "2026-09-01T09:00:00Z",
"executed_at": "2026-09-01T09:00:00Z"
}
/v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/supersede#Amend by drafting a variation
rail.instruments.supersede
Drafts a new instrument from the one in force and points the chain forward. The original is never edited and never destroyed.
A reclassification takes this path too. History is never restated.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
instrument_id
string
requiredThe instrument's identifier: the id returned when you drafted it at POST /v1/rail/engagements/{engagement_id}/instruments, or listed at GET /v1/rail/instruments. It starts with rail_instrument_.
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
reason
string
requiredWhy you're varying the instrument in force, in your own words. Required on every variation, a reclassification included.
template_code
string
optionalDefaults to the template of the instrument being superseded.
variables
object
optionalValues the variation's template needs, such as dates and party names, each as a string. Values only: a variable cannot introduce a clause and cannot change one.
Returns
The variation, drafted and awaiting assent.
id
string
requiredThe instrument's identifier, starting with rail_instrument_, from rail.instruments.create or, for a variation, rail.instruments.supersede. Pass it as instrument_id on every call about this instrument; it never changes.
object
always "instrument"
requiredAlways instrument. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
engagement_id
string
requiredThe id of the engagement this instrument papers, starting with rail_engagement_, as returned by POST /v1/rail/engagements. A variation drafted by supersede stays on the same engagement.
status
string
requireddraftedawaiting_assentexecutedexpiredsupersededtemplate_code
string
requiredA template from the approved set. Not wording.
clauses
array of object
optionalThe clauses used, by reference. Every one comes from the approved library, as written there.
2 fields
clause_code
string
requiredThe code of a clause from the approved library. Look it up by the same clause_code in the clause library to read what the instrument contains.
clause_version
string
requiredThe version of that clause as it stood in the library the instrument was built from. The instrument holds this version even after the library moves on.
clause_library_version
string
requiredThe clause library version this instrument was assembled from, such as 2026.08.1. Pass it as version to the clause library endpoint to read exactly what was used.
rule_pack_version
string
optionalThe version of the rule pack this instrument rests on, such as 2026.08.1: the rules the classification behind it was judged against. It stays on the instrument once executed, whatever pack comes later.
legal_approval_reference
string
optionalThe reference of the legal approval behind the clause library this instrument was built from. Cite it when you're asked what approved the wording; it matches the library's own.
seal
one of
optionalThe tamper evident seal, applied when the instrument is executed; null until then. Send its hash to the verify endpoint to prove the document you hold is the one that was executed.
Sealorsupersedes
string · nullable
optionalThe id of the instrument this one was drafted as a variation of, starting with rail_instrument_, or null for an original. The earlier instrument is never edited or removed.
superseded_by
string · nullable
optionalThe id of the variation drafted to replace this instrument, starting with rail_instrument_, or null while none has been. Follow it forward to reach the newest version.
expires_at
string · date-time · nullable
optionalAn unexecuted instrument lapses after this, and the lapse is recorded.
executed_at
string · date-time · nullable
optionalWhen both parties' assent was complete and the instrument was executed, as an RFC 3339 timestamp in UTC. null until then; nothing binds anyone before it.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/supersede" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"reason":"The requester confirmed the work in person.","template_code":"no_payee_destination","variables":{}}'import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railInstrumentsSupersede({
engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
instrumentId: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
idempotencyKey: crypto.randomUUID(),
railSupersedeRequest: {"reason":"The requester confirmed the work in person.","templateCode":"no_payee_destination","variables":{}},
});const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/supersede', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"reason": "The requester confirmed the work in person.",
"template_code": "no_payee_destination",
"variables": {}
}),
});
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.RAILApi(client)
result = api.rail_instruments_supersede(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id='rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"reason": "The requester confirmed the work in person.", "template_code": "no_payee_destination", "variables": {}})import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/supersede',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"reason": "The requester confirmed the work in person.", "template_code": "no_payee_destination", "variables": {}},
)
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railInstrumentsSupersede($idempotencyKey, json_decode('{"reason":"The requester confirmed the work in person.","template_code":"no_payee_destination","variables":{}}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/supersede');
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 => '{"reason":"The requester confirmed the work in person.","template_code":"no_payee_destination","variables":{}}',
]);
$result = json_decode(curl_exec($ch), true);import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railInstrumentsSupersede("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/supersede"))
.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("""
{
"reason": "The requester confirmed the work in person.",
"template_code": "no_payee_destination",
"variables": {}
}
"""))
.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 RAILApi(config);
var result = api.RailInstrumentsSupersede(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", instrumentId: "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/supersede");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"reason": "The requester confirmed the work in person.",
"template_code": "no_payee_destination",
"variables": {}
}
""", 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.RAILAPI.RailInstrumentsSupersede(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).RailSupersedeRequest(body).Execute()body := strings.NewReader(`{
"reason": "The requester confirmed the work in person.",
"template_code": "no_payee_destination",
"variables": {}
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/supersede", 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": "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "instrument",
"livemode": true,
"mocked": true,
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"status": "drafted",
"template_code": "no_payee_destination",
"clause_library_version": "2026.08.1",
"clauses": [
{
"clause_code": "no_payee_destination",
"clause_version": "2026.08.1"
}
],
"rule_pack_version": "2026.08.1",
"legal_approval_reference": "paye-2026-09-rivers",
"seal": {
"hash": "sha256:9f2c1e0043a1b8",
"previous_hash": "sha256:9f2c1e0043a1b8",
"sealed_at": "2026-09-01T09:00:00Z",
"algorithm": "sha256"
},
"supersedes": "example",
"superseded_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"expires_at": "2026-09-01T09:00:00Z",
"executed_at": "2026-09-01T09:00:00Z"
}
/v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/assents#Record a party assenting to an instrument
rail.assents.create
Binds a signature to a live Passport and to the hash of the exact instrument version the party was shown.
Send the hash of what they saw. It's what makes the assent provably about this document rather than one with the same name.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
instrument_id
string
requiredThe instrument's identifier: the id returned when you drafted it at POST /v1/rail/engagements/{engagement_id}/instruments, or listed at GET /v1/rail/instruments. It starts with rail_instrument_.
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
actor_ref
string
requiredWho is giving this assent: the opaque reference you sent as engaging_party_ref or worker_ref when you opened the engagement at POST /v1/rail/engagements. Send one of those two, exactly as you sent it then.
identity_ref
string
requiredThe id of the party's Passport, starting with anchor_pro_passport_: passport_id on their verification at POST /v1/identity/verifications, or id at GET /v1/identity/passports. Its status must be live, or the assent is refused.
mode
string
requiredclick_throughdrawn_signatureone_time_codein_person_witnessedchannel
string
requiredWhere the party was shown the instrument and assented: web (a browser), whatsapp, email or in_person (face to face). Their executed copy is delivered on the channel they used.
webwhatsappemailin_persondevice
string
optionalThe device the party gave assent on, as free text. Recorded as evidence beside the mode and channel.
instrument_hash
string
requiredThe hash of the exact instrument version the party was shown, prefixed with the algorithm, such as sha256:. Send the hash of what they saw; it's what makes the assent provably about this document.
Returns
The assent record.
id
string
requiredThe assent record's identifier, returned when you recorded it at POST /v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/assents. It never changes; pass it as assent_id when you retrieve this record.
object
always "assent"
requiredAlways assent. Tells you which kind of record you are looking at, so one handler can read any response.
instrument_id
string
requiredThe instrument this assent binds to: its id, starting with rail_instrument_, from POST /v1/rail/engagements/{engagement_id}/instruments, and the instrument_id you passed in the path.
actor_ref
string
requiredWho gave the assent: the opaque reference you sent as engaging_party_ref or worker_ref when you opened the engagement at POST /v1/rail/engagements. party tells you which of the two it is.
identity_ref
string
requiredThe Passport this signature is bound to, as its id starting with anchor_pro_passport_. It was live when the assent was recorded; read it at GET /v1/identity/passports/{passport_id}.
party
string
optionalWhich side of the engagement assented: engaging_party (the party engaging the worker) or worker. Both must assent before the instrument can execute.
engaging_partyworkermode
string
requiredclick_throughdrawn_signatureone_time_codein_person_witnessedchannel
string
requiredWhere the assent was given, as you sent it: web (a browser), whatsapp, email or in_person (face to face). The party's executed copy goes out on this channel.
webwhatsappemailin_persondevice
string
optionalThe device the assent was given on, recorded as evidence.
instrument_hash
string
requiredThe hash of the exact version presented. This is what makes the assent provably about this document rather than one with the same name.
assented_at
string · date-time
requiredWhen the assent was given, as an RFC 3339 timestamp in UTC. Part of the evidence, beside the mode, channel and device.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"actor_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","identity_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","mode":"click_through","channel":"web","instrument_hash":"sha256:9f2c1e0043a1b8","device":"example"}'import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railAssentsCreate({
engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
instrumentId: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
idempotencyKey: crypto.randomUUID(),
railAssentCreateRequest: {"actorRef":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","identityRef":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","mode":"click_through","channel":"web","instrumentHash":"sha256:9f2c1e0043a1b8","device":"example"},
});const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"actor_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"identity_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"mode": "click_through",
"channel": "web",
"instrument_hash": "sha256:9f2c1e0043a1b8",
"device": "example"
}),
});
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.RAILApi(client)
result = api.rail_assents_create(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id='rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"actor_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "identity_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "mode": "click_through", "channel": "web", "instrument_hash": "sha256:9f2c1e0043a1b8", "device": "example"})import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"actor_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "identity_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "mode": "click_through", "channel": "web", "instrument_hash": "sha256:9f2c1e0043a1b8", "device": "example"},
)
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railAssentsCreate($idempotencyKey, json_decode('{"actor_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","identity_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","mode":"click_through","channel":"web","instrument_hash":"sha256:9f2c1e0043a1b8","device":"example"}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents');
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 => '{"actor_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","identity_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","mode":"click_through","channel":"web","instrument_hash":"sha256:9f2c1e0043a1b8","device":"example"}',
]);
$result = json_decode(curl_exec($ch), true);import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railAssentsCreate("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents"))
.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("""
{
"actor_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"identity_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"mode": "click_through",
"channel": "web",
"instrument_hash": "sha256:9f2c1e0043a1b8",
"device": "example"
}
"""))
.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 RAILApi(config);
var result = api.RailAssentsCreate(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", instrumentId: "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"actor_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"identity_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"mode": "click_through",
"channel": "web",
"instrument_hash": "sha256:9f2c1e0043a1b8",
"device": "example"
}
""", 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.RAILAPI.RailAssentsCreate(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).RailAssentCreateRequest(body).Execute()body := strings.NewReader(`{
"actor_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"identity_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"mode": "click_through",
"channel": "web",
"instrument_hash": "sha256:9f2c1e0043a1b8",
"device": "example"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "assent",
"instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"actor_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"identity_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"mode": "click_through",
"channel": "web",
"instrument_hash": "sha256:9f2c1e0043a1b8",
"assented_at": "2026-09-01T09:00:00Z",
"party": "engaging_party",
"device": "example"
}
/v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/assents/{assent_id}#Retrieve an assent record
rail.assents.retrieve
The actor, the identity reference, the mode, the timestamp, the channel, the device, and the hash of the exact version presented.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
instrument_id
string
requiredThe instrument's identifier: the id returned when you drafted it at POST /v1/rail/engagements/{engagement_id}/instruments, or listed at GET /v1/rail/instruments. It starts with rail_instrument_.
assent_id
string
requiredThe assent record's identifier: the id returned when you recorded it at POST /v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/assents.
Returns
The assent record.
id
string
requiredThe assent record's identifier, returned when you recorded it at POST /v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/assents. It never changes; pass it as assent_id when you retrieve this record.
object
always "assent"
requiredAlways assent. Tells you which kind of record you are looking at, so one handler can read any response.
instrument_id
string
requiredThe instrument this assent binds to: its id, starting with rail_instrument_, from POST /v1/rail/engagements/{engagement_id}/instruments, and the instrument_id you passed in the path.
actor_ref
string
requiredWho gave the assent: the opaque reference you sent as engaging_party_ref or worker_ref when you opened the engagement at POST /v1/rail/engagements. party tells you which of the two it is.
identity_ref
string
requiredThe Passport this signature is bound to, as its id starting with anchor_pro_passport_. It was live when the assent was recorded; read it at GET /v1/identity/passports/{passport_id}.
party
string
optionalWhich side of the engagement assented: engaging_party (the party engaging the worker) or worker. Both must assent before the instrument can execute.
engaging_partyworkermode
string
requiredclick_throughdrawn_signatureone_time_codein_person_witnessedchannel
string
requiredWhere the assent was given, as you sent it: web (a browser), whatsapp, email or in_person (face to face). The party's executed copy goes out on this channel.
webwhatsappemailin_persondevice
string
optionalThe device the assent was given on, recorded as evidence.
instrument_hash
string
requiredThe hash of the exact version presented. This is what makes the assent provably about this document rather than one with the same name.
assented_at
string · date-time
requiredWhen the assent was given, as an RFC 3339 timestamp in UTC. Part of the evidence, beside the mode, channel and device.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents/%7Bassent_id%7D" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railAssentsRetrieve({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrumentId: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', assentId: '{assent_id}' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents/%7Bassent_id%7D', {
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.RAILApi(client)
result = api.rail_assents_retrieve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id='rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', assent_id='{assent_id}')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents/%7Bassent_id%7D',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railAssentsRetrieve(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', assent_id: '{assent_id}');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents/%7Bassent_id%7D');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railAssentsRetrieve("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{assent_id}");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents/%7Bassent_id%7D"))
.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 RAILApi(config);
var result = api.RailAssentsRetrieve(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", instrumentId: "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", assentId: "{assent_id}");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents/%7Bassent_id%7D");
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.RAILAPI.RailAssentsRetrieve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{assent_id}").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents/%7Bassent_id%7D", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "assent",
"instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"actor_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"identity_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"mode": "click_through",
"channel": "web",
"instrument_hash": "sha256:9f2c1e0043a1b8",
"assented_at": "2026-09-01T09:00:00Z",
"party": "engaging_party",
"device": "example"
}
/v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/copy#Retrieve your executed copy
rail.executed_copies.retrieve
Each party gets a copy on the channel they used. The worker can retrieve theirs here without going through the engaging party.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
instrument_id
string
requiredThe instrument's identifier: the id returned when you drafted it at POST /v1/rail/engagements/{engagement_id}/instruments, or listed at GET /v1/rail/instruments. It starts with rail_instrument_.
Returns
The executed copy.
object
always "executed_copy"
requiredAlways executed_copy. Tells you which kind of record you are looking at, so one handler can read any response.
instrument_id
string
requiredThe executed instrument this copy is of: its id, starting with rail_instrument_, from POST /v1/rail/engagements/{engagement_id}/instruments, and the instrument_id you passed in the path.
document_url
string · uri
requiredThe address you fetch the executed document from. Verify it with the hash in seal at POST /v1/rail/instruments/verify before you rely on it.
seal
Seal
requiredTamper evident and chained to prior records.
4 fields of Seal
hash
string
requiredThe tamper evident hash of the executed document, such as sha256:9f2c1e0043a1b8. Send it to the verify endpoint to prove the document you hold is the one that was executed.
previous_hash
string
requiredThe hash of the record sealed before this one, which chains this seal to the one before it. The verify endpoint reports chain_intact when that chain runs unbroken back to the origin.
algorithm
string
optionalThe hash algorithm the seal uses, such as sha256. The hash and previous_hash values are prefixed with it.
sealed_at
string · date-time
requiredWhen the seal was applied, as an RFC 3339 timestamp in UTC. An instrument is sealed when it is executed, so a draft carries no seal and no sealed_at.
delivered_on_channel
string
optionalThe channel this party's copy went out on: web (a browser), whatsapp, email or in_person (face to face). It's the channel you sent on their assent at POST /v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/assents.
webwhatsappemailin_personOther responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/copy" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railExecutedCopiesRetrieve({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrumentId: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/copy', {
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.RAILApi(client)
result = api.rail_executed_copies_retrieve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id='rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/copy',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railExecutedCopiesRetrieve(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/copy');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railExecutedCopiesRetrieve("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/copy"))
.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 RAILApi(config);
var result = api.RailExecutedCopiesRetrieve(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", instrumentId: "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/copy");
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.RAILAPI.RailExecutedCopiesRetrieve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/copy", 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": "executed_copy",
"instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"document_url": "https://files.sandbox.droomwork.com/example",
"seal": {
"hash": "sha256:9f2c1e0043a1b8",
"previous_hash": "sha256:9f2c1e0043a1b8",
"sealed_at": "2026-09-01T09:00:00Z",
"algorithm": "sha256"
},
"delivered_on_channel": "web"
}
/v1/rail/engagements/{engagement_id}/envelope#Retrieve the obligation envelope
rail.envelopes.retrieve
What is owed, to which authority, on what basis, from what date and how often.
The basis, never the figure. Your payroll computes the amount from this and the statutory rules. An obligation that does not attach, because a threshold or exemption applies, is absent rather than present and zero.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
Returns
The obligation envelope.
id
string
requiredThe envelope's identifier, starting with rail_envelope_. You get it at GET /v1/rail/engagements/{engagement_id}/envelope, as envelope_id on the engagement and in data on the envelope.published event; it never changes.
object
always "obligation_envelope"
requiredAlways obligation_envelope. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
engagement_id
string
requiredThe engagement this envelope states the obligations for: its id, starting with rail_engagement_, as returned by POST /v1/rail/engagements. Pass it as engagement_id to retrieve either record.
classification
string
optionalDecided by the versioned pack. No model decides it and you can't assert one yourself.
employmentfixed_term_employmentindependent_contractingapprenticeshipcasualtask_basedobligations
array of object
requiredOne entry per obligation that attaches: to which authority, on what basis, from what date and how often, never an amount. One a threshold or exemption removes is absent, not present and zero.
7 fields
authority
string
requiredThe authority the obligation is owed to, by name, such as Rivers State Internal Revenue Service. For people to read; match records on authority_id.
authority_id
string
optionalThe id of the authority the obligation is owed to, as listed at GET /v1/remittance/authorities; it starts with remit_authority_rail_obligation_. Match on it rather than on authority, which is the name.
basis
string
requiredHow an amount is worked out, not what it is. Your payroll computes the figure from it.
effective_from
string · date
requiredThe date from which the obligation applies, as a calendar date in YYYY-MM-DD. Nothing is owed under it for any earlier date.
effective_to
string · date · nullable
optionalThe last date the obligation applies, as a calendar date in YYYY-MM-DD. null while it has no end date.
frequency
string
requiredHow often the obligation falls due: monthly, quarterly, annual, or per_engagement for one that is owed once for the engagement rather than by period.
monthlyquarterlyannualper_engagementthreshold_note
string
optionalWhy a threshold or exemption did or did not apply here.
rule_pack_version
string
optionalThe version of the rule pack these obligations were stated under, such as 2026.08.1. Keep it with the envelope: it names the rules your payroll's figures trace back to.
published_at
string · date-time
requiredWhen this envelope was published for your payroll and remittance to read, as an RFC 3339 timestamp in UTC.
supersedes
string · nullable
optionalThe id of the envelope this one replaces, starting with rail_envelope_, or null for the first envelope on this engagement. Follow it back to see what was owed before.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/envelope" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railEnvelopesRetrieve({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/envelope', {
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.RAILApi(client)
result = api.rail_envelopes_retrieve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/envelope',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railEnvelopesRetrieve(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/envelope');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railEnvelopesRetrieve("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/envelope"))
.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 RAILApi(config);
var result = api.RailEnvelopesRetrieve(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/envelope");
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.RAILAPI.RailEnvelopesRetrieve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/envelope", 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": "rail_envelope_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "obligation_envelope",
"livemode": true,
"mocked": true,
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"obligations": [
{
"authority": "Rivers State Internal Revenue Service",
"basis": "paye_graduated_bands",
"effective_from": "2026-09-01",
"frequency": "monthly",
"authority_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"effective_to": "2026-09-01",
"threshold_note": "example"
}
],
"published_at": "2026-09-01T09:00:00Z",
"classification": "employment",
"rule_pack_version": "2026.08.1",
"supersedes": "example"
}
/v1/rail/engagements/{engagement_id}/discharges#List discharge confirmations against this engagement
rail.discharges.list
Payment and filing references that came back from payroll, remittance and disbursement, as evidence that each obligation was met.
RAIL records that an obligation was discharged. It never moves the money that discharged it.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
Query parameters
limit
integer
optionalHow many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.
Returns
A page of discharges.
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 Discharge
requiredThe records on this page, in the order the list promises. Empty when nothing matched.
8 fields of Discharge
id
string
requiredThe discharge record's identifier, as it appears on each entry at GET /v1/rail/engagements/{engagement_id}/discharges. It never changes, so you can tell a confirmation you've already seen from a new one.
object
always "discharge"
requiredAlways discharge. Tells you which kind of record you are looking at, so one handler can read any response.
engagement_id
string
requiredThe engagement whose obligation this discharge is evidence for: its id from POST /v1/rail/engagements, starting with rail_engagement_, and the engagement_id in the path you listed under.
authority_id
string
requiredThe authority the obligation was owed to. It matches authority_id on that obligation in the engagement's envelope at GET /v1/rail/engagements/{engagement_id}/envelope.
period
string
requiredThe period the obligation was discharged for, as a calendar month in YYYY-MM, such as 2026-09.
reference
string
requiredThe receipt or filing reference from the module that discharged it.
discharged_by
string
requiredWhich module met the obligation: run for payroll, remit for statutory remittance or route for disbursement. reference is that module's receipt or filing reference; RAIL never moves the money itself.
runremitrouteconfirmed_at
string · date-time
optionalWhen the obligation was confirmed as met, 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: limit (optional)
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/discharges?limit=25" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
// query parameters: limit (optional)
const result = await api.railDischargesList({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', limit: 25 });// query parameters: limit (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/discharges?limit=25', {
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.RAILApi(client)
# query parameters: limit (optional)
result = api.rail_discharges_list(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', limit=25)import os
import requests
# query parameters: limit (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/discharges?limit=25',
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\RAILApi(new GuzzleHttp\Client(), $config);
# query parameters: limit (optional)
$result = $api->railDischargesList(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', limit: 25);<?php
// query parameters: limit (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/discharges?limit=25');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
// query parameters: limit (optional)
var result = api.railDischargesList("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", 25);// query parameters: limit (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/discharges?limit=25"))
.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 RAILApi(config);
// query parameters: limit (optional)
var result = api.RailDischargesList(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", limit: 25);// query parameters: limit (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/discharges?limit=25");
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: limit (optional)
result, _, err := client.RAILAPI.RailDischargesList(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Limit(25).Execute()// query parameters: limit (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/discharges?limit=25", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "discharge",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"authority_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"period": "2026-09",
"reference": "example",
"discharged_by": "run",
"confirmed_at": "2026-09-01T09:00:00Z"
}
],
"has_more": true
}
/v1/rail/engagements/{engagement_id}/disputes#Raise a dispute
rail.disputes.create
Freezes the evidence bundle as it stands and suspends termination until the dispute resolves. Either party may raise one.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
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
raised_by
string
requiredWhich party is raising the dispute: engaging_party (the party engaging the worker) or worker. Either may raise one.
engaging_partyworkergrounds
string
requiredWhy the dispute is raised, in your own words, such as the classification not reflecting how the work is controlled. Recorded on the dispute.
Returns
The dispute, with the evidence frozen.
id
string
requiredThe dispute's identifier, returned when you raised it at POST /v1/rail/engagements/{engagement_id}/disputes. It never changes; pass it as dispute_id to POST /v1/rail/engagements/{engagement_id}/disputes/{dispute_id}/resolve.
object
always "dispute"
requiredAlways dispute. Tells you which kind of record you are looking at, so one handler can read any response.
engagement_id
string
requiredThe engagement the dispute is about: its id from POST /v1/rail/engagements, starting with rail_engagement_, and the engagement_id in the path you raised the dispute on.
raised_by
string
requiredWhich side raised the dispute, as you sent it: engaging_party (the party engaging the worker) or worker. Either side may raise one, and the evidence freezes the same way whichever it was.
engaging_partyworkergrounds
string
optionalWhy the dispute was raised, in the words of the side raising it. We keep it on the record and never rule on it: a dispute records that a disagreement exists, it doesn't decide it.
status
string
requiredopen from the moment it is raised, with evidence frozen and termination held; resolved once you record an outcome at POST /v1/rail/engagements/{engagement_id}/disputes/{dispute_id}/resolve; withdrawn if dropped before one was recorded.
openresolvedwithdrawnevidence_frozen_at
string · date-time
requiredThe bundle is fixed from this moment and cannot change while the dispute runs.
termination_suspended
boolean
optionaltrue while the dispute holds termination: POST /v1/rail/engagements/{engagement_id}/terminate is refused until it resolves. false once the hold is lifted.
outcome
string · nullable
optionalHow the dispute was settled, in the words you passed when you resolved it; null until then. We record the outcome; we don't decide it.
resolved_at
string · date-time · nullable
optionalWhen the outcome was recorded, as an RFC 3339 timestamp in UTC. null while the dispute is still open.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"raised_by":"engaging_party","grounds":"The classification does not reflect how the work is actually controlled."}'import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railDisputesCreate({
engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
idempotencyKey: crypto.randomUUID(),
railDisputeCreateRequest: {"raisedBy":"engaging_party","grounds":"The classification does not reflect how the work is actually controlled."},
});const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"raised_by": "engaging_party",
"grounds": "The classification does not reflect how the work is actually controlled."
}),
});
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.RAILApi(client)
result = api.rail_disputes_create(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"raised_by": "engaging_party", "grounds": "The classification does not reflect how the work is actually controlled."})import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"raised_by": "engaging_party", "grounds": "The classification does not reflect how the work is actually controlled."},
)
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railDisputesCreate($idempotencyKey, json_decode('{"raised_by":"engaging_party","grounds":"The classification does not reflect how the work is actually controlled."}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes');
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 => '{"raised_by":"engaging_party","grounds":"The classification does not reflect how the work is actually controlled."}',
]);
$result = json_decode(curl_exec($ch), true);import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railDisputesCreate("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes"))
.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("""
{
"raised_by": "engaging_party",
"grounds": "The classification does not reflect how the work is actually controlled."
}
"""))
.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 RAILApi(config);
var result = api.RailDisputesCreate(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"raised_by": "engaging_party",
"grounds": "The classification does not reflect how the work is actually controlled."
}
""", 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.RAILAPI.RailDisputesCreate(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).RailDisputeCreateRequest(body).Execute()body := strings.NewReader(`{
"raised_by": "engaging_party",
"grounds": "The classification does not reflect how the work is actually controlled."
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "dispute",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"raised_by": "engaging_party",
"status": "open",
"evidence_frozen_at": "2026-09-01T09:00:00Z",
"grounds": "The classification does not reflect how the work is actually controlled.",
"termination_suspended": true,
"outcome": "example",
"resolved_at": "2026-09-01T09:00:00Z"
}
/v1/rail/engagements/{engagement_id}/disputes/{dispute_id}/resolve#Resolve a dispute
rail.disputes.resolve
Records the outcome and lifts the suspension on termination.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
dispute_id
string
requiredThe id of the dispute you want to resolve, from the response to POST /v1/rail/engagements/{engagement_id}/disputes (rail.disputes.create) when you raised it. No endpoint lists disputes, so keep it from that response.
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
outcome
string
requiredHow the dispute was settled, in your words; we record it and don't judge it. It goes on the dispute as outcome, marks it resolved and lifts the hold on termination.
note
string
optionalAnything worth recording beside the outcome, such as who agreed it and where. Optional, and it isn't shown on the dispute you get back.
Returns
The resolved dispute.
id
string
requiredThe dispute's identifier, returned when you raised it at POST /v1/rail/engagements/{engagement_id}/disputes. It never changes; pass it as dispute_id to POST /v1/rail/engagements/{engagement_id}/disputes/{dispute_id}/resolve.
object
always "dispute"
requiredAlways dispute. Tells you which kind of record you are looking at, so one handler can read any response.
engagement_id
string
requiredThe engagement the dispute is about: its id from POST /v1/rail/engagements, starting with rail_engagement_, and the engagement_id in the path you raised the dispute on.
raised_by
string
requiredWhich side raised the dispute, as you sent it: engaging_party (the party engaging the worker) or worker. Either side may raise one, and the evidence freezes the same way whichever it was.
engaging_partyworkergrounds
string
optionalWhy the dispute was raised, in the words of the side raising it. We keep it on the record and never rule on it: a dispute records that a disagreement exists, it doesn't decide it.
status
string
requiredopen from the moment it is raised, with evidence frozen and termination held; resolved once you record an outcome at POST /v1/rail/engagements/{engagement_id}/disputes/{dispute_id}/resolve; withdrawn if dropped before one was recorded.
openresolvedwithdrawnevidence_frozen_at
string · date-time
requiredThe bundle is fixed from this moment and cannot change while the dispute runs.
termination_suspended
boolean
optionaltrue while the dispute holds termination: POST /v1/rail/engagements/{engagement_id}/terminate is refused until it resolves. false once the hold is lifted.
outcome
string · nullable
optionalHow the dispute was settled, in the words you passed when you resolved it; null until then. We record the outcome; we don't decide it.
resolved_at
string · date-time · nullable
optionalWhen the outcome was recorded, as an RFC 3339 timestamp in UTC. null while the dispute is still open.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes/%7Bdispute_id%7D/resolve" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"outcome":"example","note":"example"}'import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railDisputesResolve({
engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
disputeId: '{dispute_id}',
idempotencyKey: crypto.randomUUID(),
railDisputeResolveRequest: {"outcome":"example","note":"example"},
});const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes/%7Bdispute_id%7D/resolve', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"outcome": "example",
"note": "example"
}),
});
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.RAILApi(client)
result = api.rail_disputes_resolve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', dispute_id='{dispute_id}', body={"outcome": "example", "note": "example"})import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes/%7Bdispute_id%7D/resolve',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"outcome": "example", "note": "example"},
)
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railDisputesResolve($idempotencyKey, json_decode('{"outcome":"example","note":"example"}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes/%7Bdispute_id%7D/resolve');
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 => '{"outcome":"example","note":"example"}',
]);
$result = json_decode(curl_exec($ch), true);import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railDisputesResolve("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{dispute_id}", idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes/%7Bdispute_id%7D/resolve"))
.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("""
{
"outcome": "example",
"note": "example"
}
"""))
.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 RAILApi(config);
var result = api.RailDisputesResolve(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", disputeId: "{dispute_id}", idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes/%7Bdispute_id%7D/resolve");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"outcome": "example",
"note": "example"
}
""", 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.RAILAPI.RailDisputesResolve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{dispute_id}").IdempotencyKey(key).RailDisputeResolveRequest(body).Execute()body := strings.NewReader(`{
"outcome": "example",
"note": "example"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes/%7Bdispute_id%7D/resolve", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "dispute",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"raised_by": "engaging_party",
"status": "open",
"evidence_frozen_at": "2026-09-01T09:00:00Z",
"grounds": "The classification does not reflect how the work is actually controlled.",
"termination_suspended": true,
"outcome": "example",
"resolved_at": "2026-09-01T09:00:00Z"
}
/v1/rail/engagements/{engagement_id}/audit_bundles#Export an audit bundle for an engagement
rail.audit_bundles.create
Everything on the record for this engagement, with integrity proofs a recipient can verify without trusting us.
The record is write once and append only. There is no endpoint that edits or removes anything in it.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
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 audit bundle.
id
string
requiredThe bundle's identifier, given when you export at POST /v1/rail/engagements/{engagement_id}/audit_bundles. It never changes; pass it as audit_bundle_id to rail.audit_bundles.retrieve to see whether status is ready.
object
always "audit_bundle"
requiredAlways audit_bundle. Tells you which kind of record you are looking at, so one handler can read any response.
engagement_id
string
requiredThe engagement the bundle covers, as its id from POST /v1/rail/engagements or GET /v1/rail/engagements; it starts with rail_engagement_ and is the engagement_id in the path you exported on. One bundle covers one engagement.
status
string
requiredgenerating while the export is still being put together, ready once download_url is set, failed if it couldn't be produced. Fetch it again at GET /v1/rail/engagements/{engagement_id}/audit_bundles/{audit_bundle_id} until it leaves generating.
generatingreadyfaileddownload_url
string · uri · nullable
optionalWhere to fetch the export once status is ready; null until then. Hand the file to counsel or an auditor together with integrity_proof, so they can check it without trusting us.
integrity_proof
object
requiredYou can verify it without trusting us.
3 fields
root_hash
string
optionalThe hash covering everything in the bundle, written as the algorithm, a colon and the digest, such as sha256:9f2c1e0043a1b8. Follow verification_instructions_url to check it yourself.
algorithm
string
optionalThe hash algorithm behind root_hash, such as sha256. Use the same one when you check the bundle yourself.
verification_instructions_url
string · uri
optionalA page that tells a recipient, step by step, how to check the bundle against root_hash without trusting us. Pass it on with the download.
contents
array of string
optionalWhich parts of the record the bundle holds, one entry per part: classification, rationale, attestations, instruments, assents, envelopes, discharges and disputes.
created_at
string · date-time
optionalWhen 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/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railAuditBundlesCreate({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles', {
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.RAILApi(client)
result = api.rail_audit_bundles_create(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railAuditBundlesCreate(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railAuditBundlesCreate("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles"))
.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 RAILApi(config);
var result = api.RailAuditBundlesCreate(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles");
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.RAILAPI.RailAuditBundlesCreate(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "audit_bundle",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"status": "generating",
"integrity_proof": {
"root_hash": "sha256:9f2c1e0043a1b8",
"algorithm": "sha256",
"verification_instructions_url": "https://files.sandbox.droomwork.com/example"
},
"download_url": "https://files.sandbox.droomwork.com/example",
"contents": [
"classification"
],
"created_at": "2026-09-01T09:00:00Z"
}
/v1/rail/engagements/{engagement_id}/audit_bundles/{audit_bundle_id}#Retrieve an audit bundle
rail.audit_bundles.retrieve
The bundle, with instructions for verifying its integrity proofs.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
audit_bundle_id
string
requiredThe id of the bundle you want, from the response to POST /v1/rail/engagements/{engagement_id}/audit_bundles (rail.audit_bundles.create) when you exported it. No endpoint lists bundles, so keep it from that response.
Returns
The audit bundle.
id
string
requiredThe bundle's identifier, given when you export at POST /v1/rail/engagements/{engagement_id}/audit_bundles. It never changes; pass it as audit_bundle_id to rail.audit_bundles.retrieve to see whether status is ready.
object
always "audit_bundle"
requiredAlways audit_bundle. Tells you which kind of record you are looking at, so one handler can read any response.
engagement_id
string
requiredThe engagement the bundle covers, as its id from POST /v1/rail/engagements or GET /v1/rail/engagements; it starts with rail_engagement_ and is the engagement_id in the path you exported on. One bundle covers one engagement.
status
string
requiredgenerating while the export is still being put together, ready once download_url is set, failed if it couldn't be produced. Fetch it again at GET /v1/rail/engagements/{engagement_id}/audit_bundles/{audit_bundle_id} until it leaves generating.
generatingreadyfaileddownload_url
string · uri · nullable
optionalWhere to fetch the export once status is ready; null until then. Hand the file to counsel or an auditor together with integrity_proof, so they can check it without trusting us.
integrity_proof
object
requiredYou can verify it without trusting us.
3 fields
root_hash
string
optionalThe hash covering everything in the bundle, written as the algorithm, a colon and the digest, such as sha256:9f2c1e0043a1b8. Follow verification_instructions_url to check it yourself.
algorithm
string
optionalThe hash algorithm behind root_hash, such as sha256. Use the same one when you check the bundle yourself.
verification_instructions_url
string · uri
optionalA page that tells a recipient, step by step, how to check the bundle against root_hash without trusting us. Pass it on with the download.
contents
array of string
optionalWhich parts of the record the bundle holds, one entry per part: classification, rationale, attestations, instruments, assents, envelopes, discharges and disputes.
created_at
string · date-time
optionalWhen 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/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles/%7Baudit_bundle_id%7D" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railAuditBundlesRetrieve({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', auditBundleId: '{audit_bundle_id}' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles/%7Baudit_bundle_id%7D', {
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.RAILApi(client)
result = api.rail_audit_bundles_retrieve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', audit_bundle_id='{audit_bundle_id}')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles/%7Baudit_bundle_id%7D',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railAuditBundlesRetrieve(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', audit_bundle_id: '{audit_bundle_id}');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles/%7Baudit_bundle_id%7D');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railAuditBundlesRetrieve("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{audit_bundle_id}");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles/%7Baudit_bundle_id%7D"))
.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 RAILApi(config);
var result = api.RailAuditBundlesRetrieve(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", auditBundleId: "{audit_bundle_id}");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles/%7Baudit_bundle_id%7D");
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.RAILAPI.RailAuditBundlesRetrieve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{audit_bundle_id}").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles/%7Baudit_bundle_id%7D", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "audit_bundle",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"status": "generating",
"integrity_proof": {
"root_hash": "sha256:9f2c1e0043a1b8",
"algorithm": "sha256",
"verification_instructions_url": "https://files.sandbox.droomwork.com/example"
},
"download_url": "https://files.sandbox.droomwork.com/example",
"contents": [
"classification"
],
"created_at": "2026-09-01T09:00:00Z"
}
/v1/rail/engagements/{engagement_id}/paper#Paper an engagement
rail.engagements.paper
Builds the instrument from the clause library for the classification the engagement carries. Refused while a classification review is outstanding: you can't paper a classification that isn't settled.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
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 engagement in its new state.
id
string
requiredThe engagement's identifier, starting with rail_engagement_. You get it from POST /v1/rail/engagements and pass it as engagement_id on every call about this engagement; it never changes.
object
always "engagement"
requiredAlways engagement. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
status
string
requiredTransitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.
classifiedpaperedexecutedregisteredcurrentlapseddisputedterminatedengaging_party_ref
string
requiredThe party engaging the worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the worker.
worker_ref
string
requiredThe worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the engaging party.
classification
one of
optionalThe classification once classify has run, or null before then: employment, fixed_term_employment, independent_contracting, apprenticeship, casual or task_based. It decides which instrument you can paper.
Classificationorclassification_review_required
boolean
optionalTrue when confidence was low or the result was contested. A person decides.
instrument_in_force_id
string · nullable
optionalThe id of the instrument in force, starting with rail_instrument_, from rail.instruments.create or, for a variation, rail.instruments.supersede. null until one is executed; a variation that supersedes it takes its place here once executed.
envelope_id
string · nullable
optionalThe id of the obligation envelope published for this engagement, starting with rail_envelope_; null until one is published. Retrieve it at GET /v1/rail/engagements/{engagement_id}/envelope or list it at GET /v1/rail/envelopes.
starts_on
string · date
optionalThe date the engagement starts, as a calendar date in YYYY-MM-DD. As you sent it when you opened the engagement.
ends_on
string · date · nullable
optionalThe date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.
as_of
string · date · nullable
optionalPresent when this was answered as at a past date rather than now.
created_at
string · date-time
optionalWhen 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/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/paper" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railEngagementsPaper({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/paper', {
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.RAILApi(client)
result = api.rail_engagements_paper(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/paper',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railEngagementsPaper(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/paper');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railEngagementsPaper("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/paper"))
.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 RAILApi(config);
var result = api.RailEngagementsPaper(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/paper");
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.RAILAPI.RailEngagementsPaper(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/paper", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "engagement",
"livemode": true,
"mocked": true,
"status": "classified",
"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"classification": "employment",
"classification_review_required": true,
"instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"starts_on": "2026-09-01",
"ends_on": "2026-09-01",
"as_of": "2026-09-01",
"created_at": "2026-09-01T09:00:00Z"
}
/v1/rail/engagements/{engagement_id}/execute#Execute an engagement
rail.engagements.execute
Both sides have assented and the instrument is sealed. This is the point the agreement binds, and the earliest point at which anybody may be paid under it.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
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 engagement in its new state.
id
string
requiredThe engagement's identifier, starting with rail_engagement_. You get it from POST /v1/rail/engagements and pass it as engagement_id on every call about this engagement; it never changes.
object
always "engagement"
requiredAlways engagement. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
status
string
requiredTransitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.
classifiedpaperedexecutedregisteredcurrentlapseddisputedterminatedengaging_party_ref
string
requiredThe party engaging the worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the worker.
worker_ref
string
requiredThe worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the engaging party.
classification
one of
optionalThe classification once classify has run, or null before then: employment, fixed_term_employment, independent_contracting, apprenticeship, casual or task_based. It decides which instrument you can paper.
Classificationorclassification_review_required
boolean
optionalTrue when confidence was low or the result was contested. A person decides.
instrument_in_force_id
string · nullable
optionalThe id of the instrument in force, starting with rail_instrument_, from rail.instruments.create or, for a variation, rail.instruments.supersede. null until one is executed; a variation that supersedes it takes its place here once executed.
envelope_id
string · nullable
optionalThe id of the obligation envelope published for this engagement, starting with rail_envelope_; null until one is published. Retrieve it at GET /v1/rail/engagements/{engagement_id}/envelope or list it at GET /v1/rail/envelopes.
starts_on
string · date
optionalThe date the engagement starts, as a calendar date in YYYY-MM-DD. As you sent it when you opened the engagement.
ends_on
string · date · nullable
optionalThe date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.
as_of
string · date · nullable
optionalPresent when this was answered as at a past date rather than now.
created_at
string · date-time
optionalWhen 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/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railEngagementsExecute({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute', {
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.RAILApi(client)
result = api.rail_engagements_execute(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railEngagementsExecute(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railEngagementsExecute("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute"))
.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 RAILApi(config);
var result = api.RailEngagementsExecute(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute");
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.RAILAPI.RailEngagementsExecute(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "engagement",
"livemode": true,
"mocked": true,
"status": "classified",
"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"classification": "employment",
"classification_review_required": true,
"instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"starts_on": "2026-09-01",
"ends_on": "2026-09-01",
"as_of": "2026-09-01",
"created_at": "2026-09-01T09:00:00Z"
}
/v1/rail/engagements/{engagement_id}/register#Register an engagement
rail.engagements.register
Files the engagement where the jurisdiction requires it to be registered. A separate step from execution: the two happen at different times, and one can fail without the other.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
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 engagement in its new state.
id
string
requiredThe engagement's identifier, starting with rail_engagement_. You get it from POST /v1/rail/engagements and pass it as engagement_id on every call about this engagement; it never changes.
object
always "engagement"
requiredAlways engagement. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
status
string
requiredTransitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.
classifiedpaperedexecutedregisteredcurrentlapseddisputedterminatedengaging_party_ref
string
requiredThe party engaging the worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the worker.
worker_ref
string
requiredThe worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the engaging party.
classification
one of
optionalThe classification once classify has run, or null before then: employment, fixed_term_employment, independent_contracting, apprenticeship, casual or task_based. It decides which instrument you can paper.
Classificationorclassification_review_required
boolean
optionalTrue when confidence was low or the result was contested. A person decides.
instrument_in_force_id
string · nullable
optionalThe id of the instrument in force, starting with rail_instrument_, from rail.instruments.create or, for a variation, rail.instruments.supersede. null until one is executed; a variation that supersedes it takes its place here once executed.
envelope_id
string · nullable
optionalThe id of the obligation envelope published for this engagement, starting with rail_envelope_; null until one is published. Retrieve it at GET /v1/rail/engagements/{engagement_id}/envelope or list it at GET /v1/rail/envelopes.
starts_on
string · date
optionalThe date the engagement starts, as a calendar date in YYYY-MM-DD. As you sent it when you opened the engagement.
ends_on
string · date · nullable
optionalThe date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.
as_of
string · date · nullable
optionalPresent when this was answered as at a past date rather than now.
created_at
string · date-time
optionalWhen 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/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/register" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railEngagementsRegister({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/register', {
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.RAILApi(client)
result = api.rail_engagements_register(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/register',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railEngagementsRegister(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/register');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railEngagementsRegister("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/register"))
.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 RAILApi(config);
var result = api.RailEngagementsRegister(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/register");
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.RAILAPI.RailEngagementsRegister(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/register", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "engagement",
"livemode": true,
"mocked": true,
"status": "classified",
"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"classification": "employment",
"classification_review_required": true,
"instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"starts_on": "2026-09-01",
"ends_on": "2026-09-01",
"as_of": "2026-09-01",
"created_at": "2026-09-01T09:00:00Z"
}
/v1/rail/engagements/{engagement_id}/activate#Activate an engagement
rail.engagements.activate
Marks the engagement current, so work can be allocated against it. MATCH and RUN both wait for this status.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
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 engagement in its new state.
id
string
requiredThe engagement's identifier, starting with rail_engagement_. You get it from POST /v1/rail/engagements and pass it as engagement_id on every call about this engagement; it never changes.
object
always "engagement"
requiredAlways engagement. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
status
string
requiredTransitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.
classifiedpaperedexecutedregisteredcurrentlapseddisputedterminatedengaging_party_ref
string
requiredThe party engaging the worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the worker.
worker_ref
string
requiredThe worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the engaging party.
classification
one of
optionalThe classification once classify has run, or null before then: employment, fixed_term_employment, independent_contracting, apprenticeship, casual or task_based. It decides which instrument you can paper.
Classificationorclassification_review_required
boolean
optionalTrue when confidence was low or the result was contested. A person decides.
instrument_in_force_id
string · nullable
optionalThe id of the instrument in force, starting with rail_instrument_, from rail.instruments.create or, for a variation, rail.instruments.supersede. null until one is executed; a variation that supersedes it takes its place here once executed.
envelope_id
string · nullable
optionalThe id of the obligation envelope published for this engagement, starting with rail_envelope_; null until one is published. Retrieve it at GET /v1/rail/engagements/{engagement_id}/envelope or list it at GET /v1/rail/envelopes.
starts_on
string · date
optionalThe date the engagement starts, as a calendar date in YYYY-MM-DD. As you sent it when you opened the engagement.
ends_on
string · date · nullable
optionalThe date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.
as_of
string · date · nullable
optionalPresent when this was answered as at a past date rather than now.
created_at
string · date-time
optionalWhen 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/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/activate" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railEngagementsActivate({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/activate', {
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.RAILApi(client)
result = api.rail_engagements_activate(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/activate',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railEngagementsActivate(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/activate');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railEngagementsActivate("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/activate"))
.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 RAILApi(config);
var result = api.RailEngagementsActivate(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/activate");
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.RAILAPI.RailEngagementsActivate(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/activate", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "engagement",
"livemode": true,
"mocked": true,
"status": "classified",
"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"classification": "employment",
"classification_review_required": true,
"instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"starts_on": "2026-09-01",
"ends_on": "2026-09-01",
"as_of": "2026-09-01",
"created_at": "2026-09-01T09:00:00Z"
}
/v1/rail/engagements/{engagement_id}/lapse#Lapse an engagement
rail.engagements.lapse
The term ended and nothing renewed it. A lapsed engagement is not terminated and is not disputed: it stopped, and the record says so.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
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 engagement in its new state.
id
string
requiredThe engagement's identifier, starting with rail_engagement_. You get it from POST /v1/rail/engagements and pass it as engagement_id on every call about this engagement; it never changes.
object
always "engagement"
requiredAlways engagement. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
status
string
requiredTransitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.
classifiedpaperedexecutedregisteredcurrentlapseddisputedterminatedengaging_party_ref
string
requiredThe party engaging the worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the worker.
worker_ref
string
requiredThe worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the engaging party.
classification
one of
optionalThe classification once classify has run, or null before then: employment, fixed_term_employment, independent_contracting, apprenticeship, casual or task_based. It decides which instrument you can paper.
Classificationorclassification_review_required
boolean
optionalTrue when confidence was low or the result was contested. A person decides.
instrument_in_force_id
string · nullable
optionalThe id of the instrument in force, starting with rail_instrument_, from rail.instruments.create or, for a variation, rail.instruments.supersede. null until one is executed; a variation that supersedes it takes its place here once executed.
envelope_id
string · nullable
optionalThe id of the obligation envelope published for this engagement, starting with rail_envelope_; null until one is published. Retrieve it at GET /v1/rail/engagements/{engagement_id}/envelope or list it at GET /v1/rail/envelopes.
starts_on
string · date
optionalThe date the engagement starts, as a calendar date in YYYY-MM-DD. As you sent it when you opened the engagement.
ends_on
string · date · nullable
optionalThe date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.
as_of
string · date · nullable
optionalPresent when this was answered as at a past date rather than now.
created_at
string · date-time
optionalWhen 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/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/lapse" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railEngagementsLapse({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/lapse', {
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.RAILApi(client)
result = api.rail_engagements_lapse(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/lapse',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railEngagementsLapse(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/lapse');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railEngagementsLapse("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/lapse"))
.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 RAILApi(config);
var result = api.RailEngagementsLapse(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/lapse");
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.RAILAPI.RailEngagementsLapse(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/lapse", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "engagement",
"livemode": true,
"mocked": true,
"status": "classified",
"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"classification": "employment",
"classification_review_required": true,
"instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"starts_on": "2026-09-01",
"ends_on": "2026-09-01",
"as_of": "2026-09-01",
"created_at": "2026-09-01T09:00:00Z"
}
/v1/rail/engagements/{engagement_id}/dispute#Dispute an engagement
rail.engagements.dispute
One side says the agreement is not what was agreed. Work carries on or stops by the terms of the instrument. This records that the disagreement exists; it doesn't decide it.
Path parameters
engagement_id
string
requiredThe engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.
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 engagement in its new state.
id
string
requiredThe engagement's identifier, starting with rail_engagement_. You get it from POST /v1/rail/engagements and pass it as engagement_id on every call about this engagement; it never changes.
object
always "engagement"
requiredAlways engagement. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
status
string
requiredTransitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.
classifiedpaperedexecutedregisteredcurrentlapseddisputedterminatedengaging_party_ref
string
requiredThe party engaging the worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the worker.
worker_ref
string
requiredThe worker, as the subject_ref you chose for them at POST /v1/identity/consent_tokens and sent when you opened the engagement at POST /v1/rail/engagements. Anchored, the same as the engaging party.
classification
one of
optionalThe classification once classify has run, or null before then: employment, fixed_term_employment, independent_contracting, apprenticeship, casual or task_based. It decides which instrument you can paper.
Classificationorclassification_review_required
boolean
optionalTrue when confidence was low or the result was contested. A person decides.
instrument_in_force_id
string · nullable
optionalThe id of the instrument in force, starting with rail_instrument_, from rail.instruments.create or, for a variation, rail.instruments.supersede. null until one is executed; a variation that supersedes it takes its place here once executed.
envelope_id
string · nullable
optionalThe id of the obligation envelope published for this engagement, starting with rail_envelope_; null until one is published. Retrieve it at GET /v1/rail/engagements/{engagement_id}/envelope or list it at GET /v1/rail/envelopes.
starts_on
string · date
optionalThe date the engagement starts, as a calendar date in YYYY-MM-DD. As you sent it when you opened the engagement.
ends_on
string · date · nullable
optionalThe date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.
as_of
string · date · nullable
optionalPresent when this was answered as at a past date rather than now.
created_at
string · date-time
optionalWhen 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/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/dispute" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railEngagementsDispute({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/dispute', {
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.RAILApi(client)
result = api.rail_engagements_dispute(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/dispute',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railEngagementsDispute(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/dispute');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railEngagementsDispute("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/dispute"))
.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 RAILApi(config);
var result = api.RailEngagementsDispute(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/dispute");
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.RAILAPI.RailEngagementsDispute(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/dispute", 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": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "engagement",
"livemode": true,
"mocked": true,
"status": "classified",
"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"classification": "employment",
"classification_review_required": true,
"instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"starts_on": "2026-09-01",
"ends_on": "2026-09-01",
"as_of": "2026-09-01",
"created_at": "2026-09-01T09:00:00Z"
}
/v1/rail/events#List events
rail.events.list
The append only record of everything RAIL is the authority for, oldest first. Every event here is one the webhook catalogue declares, so the code that handles a delivery handles a replay too.
Two ways to read it. Pass stream with after to replay one stream from the sequence you last handled; it's exact and needs no cursor, because the sequence counts per organisation and per stream. Without stream, you get events across streams in the order they were recorded, paged with starting_after.
Query parameters
stream
string
optionalThe id of the record whose history you want: for engagement.* events, the engagement's id from POST /v1/rail/engagements, starting rail_engagement_; pair it with after. Leave it out to get every stream, oldest first, paged with starting_after.
after
integer
optionalReplay from here: the sequence of the last event you handled on that stream, as it reads on each event, so you get everything after it; 0 reads from the start. Needs stream beside it, because a sequence counts within one stream.
limit
integer
optionalHow many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.
starting_after
string
optionalThe id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.
Returns
A page of events.
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 StoredEvent
requiredThe records on this page, in the order the list promises. Empty when nothing matched.
13 fields of StoredEvent
id
string
requiredThe event's identifier, starting with evt_, the same on a webhook delivery and on the module's events list, such as GET /v1/payroll/events. It never changes: a redelivery carries the same id, so you can recognise an event you have already handled.
type
string
requiredWhat happened, as module.resource.past_tense_verb, for example run.payslip.calculated. Pick your handler on it; data takes the shape this type promises.
schema_version
integer · minimum 1
requiredThe version of the shape data takes for this type, starting at 1. A change to the shape raises it, so check it before you read data.
org_id
string
requiredThe organisation the event belongs to, by its id, which starts with org_: the one POST /v1/registrations gave you and GET /v1/me returns. You only ever receive events for your own organisation.
sequence
integer · minimum 0
requiredPer organisation and per stream. It is how a consumer tells a replay from a new event, and it is what the delivery guarantee rests on.
occurred_at
string · date-time
requiredWhen the event happened, as an RFC 3339 timestamp in UTC. Not when it was delivered: a redelivery carries the original value.
request_id
string
optionalThe request that caused this event, where one did: the Droomwork-Request-Id that request returned, starting with req_. Absent for an event a schedule raised, such as an engagement lapsing on its end date.
livemode
boolean
requiredWhich realm the event happened in. False is the sandbox.
mocked
boolean
requiredWhether a mock produced this fact, rather than an engine computing it. Recorded on the event when it was appended and never worked out afterwards from the realm: the two answers agree while every module is on its mock and part on the day the first engine ships. See ADR-0011.
source
string
requiredWhich part of Droomwork is the authority for this fact: anchor (identity), proof (credentials), rail (engagements), flow (sourcing), match (allocation), run (payroll), remit (remittance), route (payouts), gateway (the API's front door), iam (accounts and API keys), ledger (the books), registry (rule packs), delivery (webhooks and messages), documents (rendered payslips and instruments) or intelligence (AI decisions). Read the fact from there when it matters; your own copy is never the authority.
anchorproofrailflowmatchrunremitroutegatewayiamledgerregistrydeliverydocumentsintelligenceobject
always "event"
requiredAlways event. Tells you which kind of record you are looking at, so one handler can read any response.
stream
string
requiredThe id of the record this event is about: for engagement.* events, the engagement's id from POST /v1/rail/engagements, starting rail_engagement_. sequence counts per stream; pass this as stream with after to GET /v1/rail/events to replay.
data
object
requiredThe record the event is about, in the shape its type names: an engagement, an instrument, an obligation envelope or a remediation case. The same body a webhook delivery carries.
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: stream (optional), after (optional), limit (optional), starting_after (optional)
curl -X GET "https://sandbox.droomwork.io/v1/rail/events?stream=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
const result = await api.railEventsList({ stream: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', after: 0, limit: 25 });// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/events?stream=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25', {
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.RAILApi(client)
# query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
result = api.rail_events_list(stream='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', after=0, limit=25)import os
import requests
# query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/events?stream=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25',
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\RAILApi(new GuzzleHttp\Client(), $config);
# query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
$result = $api->railEventsList(stream: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', after: 0, limit: 25);<?php
// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/events?stream=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
var result = api.railEventsList("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", 0, 25, null);// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/events?stream=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25"))
.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 RAILApi(config);
// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
var result = api.RailEventsList(stream: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", after: 0, limit: 25);// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/events?stream=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25");
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: stream (optional), after (optional), limit (optional), starting_after (optional)
result, _, err := client.RAILAPI.RailEventsList(ctx).Stream("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").After(0).Limit(25).Execute()// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/events?stream=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25", 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": "evt_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"type": "run.payslip.calculated",
"schema_version": 1,
"org_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"sequence": 0,
"occurred_at": "2026-09-01T09:00:00Z",
"livemode": true,
"mocked": true,
"source": "anchor",
"request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "event",
"stream": "example",
"data": {}
}
],
"has_more": true
}
/v1/rail/events/{event_id}#Retrieve an event
rail.events.retrieve
Returns one event. An identifier belonging to another organisation is not found rather than refused, so nothing confirms it exists.
Path parameters
event_id
string
requiredThe id of the event you want, from an entry on GET /v1/rail/events (rail.events.list) or the id on an event delivered to your webhook. It starts with evt_.
Returns
The event.
id
string
requiredThe event's identifier, starting with evt_, the same on a webhook delivery and on the module's events list, such as GET /v1/payroll/events. It never changes: a redelivery carries the same id, so you can recognise an event you have already handled.
type
string
requiredWhat happened, as module.resource.past_tense_verb, for example run.payslip.calculated. Pick your handler on it; data takes the shape this type promises.
schema_version
integer · minimum 1
requiredThe version of the shape data takes for this type, starting at 1. A change to the shape raises it, so check it before you read data.
org_id
string
requiredThe organisation the event belongs to, by its id, which starts with org_: the one POST /v1/registrations gave you and GET /v1/me returns. You only ever receive events for your own organisation.
sequence
integer · minimum 0
requiredPer organisation and per stream. It is how a consumer tells a replay from a new event, and it is what the delivery guarantee rests on.
occurred_at
string · date-time
requiredWhen the event happened, as an RFC 3339 timestamp in UTC. Not when it was delivered: a redelivery carries the original value.
request_id
string
optionalThe request that caused this event, where one did: the Droomwork-Request-Id that request returned, starting with req_. Absent for an event a schedule raised, such as an engagement lapsing on its end date.
livemode
boolean
requiredWhich realm the event happened in. False is the sandbox.
mocked
boolean
requiredWhether a mock produced this fact, rather than an engine computing it. Recorded on the event when it was appended and never worked out afterwards from the realm: the two answers agree while every module is on its mock and part on the day the first engine ships. See ADR-0011.
source
string
requiredWhich part of Droomwork is the authority for this fact: anchor (identity), proof (credentials), rail (engagements), flow (sourcing), match (allocation), run (payroll), remit (remittance), route (payouts), gateway (the API's front door), iam (accounts and API keys), ledger (the books), registry (rule packs), delivery (webhooks and messages), documents (rendered payslips and instruments) or intelligence (AI decisions). Read the fact from there when it matters; your own copy is never the authority.
anchorproofrailflowmatchrunremitroutegatewayiamledgerregistrydeliverydocumentsintelligenceobject
always "event"
requiredAlways event. Tells you which kind of record you are looking at, so one handler can read any response.
stream
string
requiredThe id of the record this event is about: for engagement.* events, the engagement's id from POST /v1/rail/engagements, starting rail_engagement_. sequence counts per stream; pass this as stream with after to GET /v1/rail/events to replay.
data
object
requiredThe record the event is about, in the shape its type names: an engagement, an instrument, an obligation envelope or a remediation case. The same body a webhook delivery carries.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/rail/events/%7Bevent_id%7D" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railEventsRetrieve({ eventId: '{event_id}' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/events/%7Bevent_id%7D', {
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.RAILApi(client)
result = api.rail_events_retrieve(event_id='{event_id}')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/events/%7Bevent_id%7D',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railEventsRetrieve(event_id: '{event_id}');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/events/%7Bevent_id%7D');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railEventsRetrieve("{event_id}");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/events/%7Bevent_id%7D"))
.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 RAILApi(config);
var result = api.RailEventsRetrieve(eventId: "{event_id}");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/events/%7Bevent_id%7D");
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.RAILAPI.RailEventsRetrieve(ctx, "{event_id}").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/events/%7Bevent_id%7D", 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": "evt_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"type": "run.payslip.calculated",
"schema_version": 1,
"org_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"sequence": 0,
"occurred_at": "2026-09-01T09:00:00Z",
"livemode": true,
"mocked": true,
"source": "anchor",
"request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "event",
"stream": "example",
"data": {}
}
/v1/rail/audit_entries#List audit entries
rail.audit_entries.list
Who did what, newest first. One row per attempt rather than per success, so refusals are here too.
A read that succeeded is not recorded.
Query parameters
action
string
optionalOnly attempts at one route, matched exactly against action on each entry: the method and route pattern, such as POST /v1/rail/engagements, so copy it from an entry. Leave it out to get every route.
actor_id
string
optionalOnly one caller's attempts, as actor_id reads on each entry: an API key's id from GET /v1/api_keys (it starts with key_), the client_id you send to POST /v1/oauth/token, or a staff member's id. Leave it out to get every caller.
resource
string
optionalOnly attempts on one kind of record, as resource reads on each entry: the collection segment of the route, such as engagements, instruments or disputes. Leave it out to get every kind.
resource_id
string
optionalOnly attempts on one record, as resource_id reads on each entry: the identifier that was in the route, such as an engagement's id from POST /v1/rail/engagements, starting rail_engagement_. Pair it with resource, and leave it out to get every record.
outcome
string
optionalOnly attempts that ended one way: succeeded (a 2xx answer), refused (a 4xx) or failed (a 5xx). Ask for refused to see every attempt that was turned away, and leave it out to get all three.
succeededrefusedfailedrecorded_after
string
optionalOnly entries recorded after this moment, as an RFC 3339 timestamp in UTC such as 2026-01-01T00:00:00Z; exclusive, so an entry at exactly this instant is left out. Leave it out to start from your oldest entry.
recorded_before
string
optionalOnly entries recorded before this moment, as an RFC 3339 timestamp in UTC; exclusive, so an entry at exactly this instant is left out. Pair it with recorded_after to read one window, and leave it out to run up to the newest entry.
limit
integer
optionalHow many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.
starting_after
string
optionalThe id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.
Returns
A page of audit entries.
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 AuditEntry
requiredThe records on this page, in the order the list promises. Empty when nothing matched.
13 fields of AuditEntry
id
string
requiredThe entry's identifier, as it appears on GET /v1/rail/audit_entries; it starts with audit_entry_ and never changes. Pass it as audit_entry_id to GET /v1/rail/audit_entries/{audit_entry_id}, or as starting_after to page on from it.
object
always "audit_entry"
requiredAlways audit_entry. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm the action happened in. False is the sandbox.
mocked
boolean
requiredWhether the route this attempt was aimed at is served by a mock. It describes the route, not the answer: a refused request and a replayed idempotent request both count as attempts against a mocked route.
at
string · date-time
requiredWhen the attempt was made, as an RFC 3339 timestamp in UTC. The list is ordered by it, newest first.
request_id
string
requiredThe Droomwork-Request-Id of the call that made this attempt. It starts with req_; match it to your own logs, and quote it when you ask us about the call.
actor_type
string
requiredWhat kind of caller made the attempt: client for one of your own API keys or OAuth clients, staff for a Droomwork staff member acting under a support grant, service when Droomwork acted for you.
actor_id
string
requiredWho made the attempt, as the id of what actor_type names: an API key's id from GET /v1/api_keys, starting key_; the client_id you sent to POST /v1/oauth/token; or a staff member's id. Filter the list on actor_id to follow one caller.
action
string
requiredWhat was attempted, as the method and the route pattern.
resource
string · nullable
optionalThe kind of record the attempt was aimed at, as the collection in the route, such as engagements or disputes. null when the route names none.
resource_id
string · nullable
optionalThe identifier of the record the attempt was aimed at, where the route named one: the engagement's id for POST /v1/rail/engagements/{engagement_id}/terminate, say. null for an attempt on a collection, such as creating or listing.
outcome
string
requiredHow the attempt ended: succeeded for a 2xx answer, refused for a 4xx, failed for a 5xx. A refusal is recorded like anything else; filter the list on outcome to read only one kind.
succeededrefusedfailedstatus
integer
requiredThe HTTP status the caller was given, such as 201 or 403. It says more than outcome does: 404 and 409 are both refused, and only this tells them apart.
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: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
curl -X GET "https://sandbox.droomwork.io/v1/rail/audit_entries?action=POST%20%2Fv1%2Frail%2Fengagements&actor_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=engagements&resource_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
const result = await api.railAuditEntriesList({ action: 'POST /v1/rail/engagements', actorId: 'sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', resource: 'engagements', resourceId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', outcome: 'succeeded', recordedAfter: '2026-01-01T00:00:00Z', recordedBefore: '2027-01-01T00:00:00Z', limit: 25 });// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/audit_entries?action=POST%20%2Fv1%2Frail%2Fengagements&actor_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=engagements&resource_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25', {
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.RAILApi(client)
# query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
result = api.rail_audit_entries_list(action='POST /v1/rail/engagements', actor_id='sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', resource='engagements', resource_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', outcome='succeeded', recorded_after='2026-01-01T00:00:00Z', recorded_before='2027-01-01T00:00:00Z', limit=25)import os
import requests
# query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/audit_entries?action=POST%20%2Fv1%2Frail%2Fengagements&actor_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=engagements&resource_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25',
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\RAILApi(new GuzzleHttp\Client(), $config);
# query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
$result = $api->railAuditEntriesList(action: 'POST /v1/rail/engagements', actor_id: 'sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', resource: 'engagements', resource_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', outcome: 'succeeded', recorded_after: '2026-01-01T00:00:00Z', recorded_before: '2027-01-01T00:00:00Z', limit: 25);<?php
// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/audit_entries?action=POST%20%2Fv1%2Frail%2Fengagements&actor_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=engagements&resource_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
var result = api.railAuditEntriesList("POST /v1/rail/engagements", "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "engagements", "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "succeeded", "2026-01-01T00:00:00Z", "2027-01-01T00:00:00Z", 25, null);// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/audit_entries?action=POST%20%2Fv1%2Frail%2Fengagements&actor_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=engagements&resource_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25"))
.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 RAILApi(config);
// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
var result = api.RailAuditEntriesList(action: "POST /v1/rail/engagements", actorId: "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", resource: "engagements", resourceId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", outcome: "succeeded", recordedAfter: "2026-01-01T00:00:00Z", recordedBefore: "2027-01-01T00:00:00Z", limit: 25);// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/audit_entries?action=POST%20%2Fv1%2Frail%2Fengagements&actor_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=engagements&resource_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25");
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: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
result, _, err := client.RAILAPI.RailAuditEntriesList(ctx).Action("POST /v1/rail/engagements").ActorId("sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Resource("engagements").ResourceId("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Outcome("succeeded").RecordedAfter("2026-01-01T00:00:00Z").RecordedBefore("2027-01-01T00:00:00Z").Limit(25).Execute()// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/audit_entries?action=POST%20%2Fv1%2Frail%2Fengagements&actor_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=engagements&resource_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25", 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": "audit_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "audit_entry",
"livemode": true,
"mocked": true,
"at": "2026-09-01T09:00:00Z",
"request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"actor_type": "example",
"actor_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"action": "example",
"outcome": "succeeded",
"status": 1,
"resource": "example",
"resource_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
],
"has_more": true
}
/v1/rail/audit_entries/{audit_entry_id}#Retrieve an audit entry
rail.audit_entries.retrieve
Returns one entry. An identifier belonging to another organisation is not found rather than refused, so nothing confirms it exists.
Path parameters
audit_entry_id
string
requiredThe id of the entry you want, from an entry on GET /v1/rail/audit_entries (rail.audit_entries.list). It starts with audit_entry_.
Returns
The audit entry.
id
string
requiredThe entry's identifier, as it appears on GET /v1/rail/audit_entries; it starts with audit_entry_ and never changes. Pass it as audit_entry_id to GET /v1/rail/audit_entries/{audit_entry_id}, or as starting_after to page on from it.
object
always "audit_entry"
requiredAlways audit_entry. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm the action happened in. False is the sandbox.
mocked
boolean
requiredWhether the route this attempt was aimed at is served by a mock. It describes the route, not the answer: a refused request and a replayed idempotent request both count as attempts against a mocked route.
at
string · date-time
requiredWhen the attempt was made, as an RFC 3339 timestamp in UTC. The list is ordered by it, newest first.
request_id
string
requiredThe Droomwork-Request-Id of the call that made this attempt. It starts with req_; match it to your own logs, and quote it when you ask us about the call.
actor_type
string
requiredWhat kind of caller made the attempt: client for one of your own API keys or OAuth clients, staff for a Droomwork staff member acting under a support grant, service when Droomwork acted for you.
actor_id
string
requiredWho made the attempt, as the id of what actor_type names: an API key's id from GET /v1/api_keys, starting key_; the client_id you sent to POST /v1/oauth/token; or a staff member's id. Filter the list on actor_id to follow one caller.
action
string
requiredWhat was attempted, as the method and the route pattern.
resource
string · nullable
optionalThe kind of record the attempt was aimed at, as the collection in the route, such as engagements or disputes. null when the route names none.
resource_id
string · nullable
optionalThe identifier of the record the attempt was aimed at, where the route named one: the engagement's id for POST /v1/rail/engagements/{engagement_id}/terminate, say. null for an attempt on a collection, such as creating or listing.
outcome
string
requiredHow the attempt ended: succeeded for a 2xx answer, refused for a 4xx, failed for a 5xx. A refusal is recorded like anything else; filter the list on outcome to read only one kind.
succeededrefusedfailedstatus
integer
requiredThe HTTP status the caller was given, such as 201 or 403. It says more than outcome does: 404 and 409 are both refused, and only this tells them apart.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/rail/audit_entries/%7Baudit_entry_id%7D" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@droomwork/sdk';
const api = new RAILApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.railAuditEntriesRetrieve({ auditEntryId: '{audit_entry_id}' });const response = await fetch('https://sandbox.droomwork.io/v1/rail/audit_entries/%7Baudit_entry_id%7D', {
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.RAILApi(client)
result = api.rail_audit_entries_retrieve(audit_entry_id='{audit_entry_id}')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/rail/audit_entries/%7Baudit_entry_id%7D',
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\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railAuditEntriesRetrieve(audit_entry_id: '{audit_entry_id}');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/audit_entries/%7Baudit_entry_id%7D');
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.RailApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RailApi api = new RailApi(client);
var result = api.railAuditEntriesRetrieve("{audit_entry_id}");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/audit_entries/%7Baudit_entry_id%7D"))
.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 RAILApi(config);
var result = api.RailAuditEntriesRetrieve(auditEntryId: "{audit_entry_id}");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/audit_entries/%7Baudit_entry_id%7D");
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.RAILAPI.RailAuditEntriesRetrieve(ctx, "{audit_entry_id}").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/audit_entries/%7Baudit_entry_id%7D", 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": "audit_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "audit_entry",
"livemode": true,
"mocked": true,
"at": "2026-09-01T09:00:00Z",
"request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"actor_type": "example",
"actor_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"action": "example",
"outcome": "succeeded",
"status": 1,
"resource": "example",
"resource_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}