Version 1.0.0
Heros 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#Every engagement you hold
rail.engagements.list
The engagements your organisation has opened, which is where the identifier for everything else in RAIL comes from. A screen in your own product can list who you have engaged and link straight through to one of them.
You get your engagements, each one the record of a single worker engaged by a single party, with the standing it has reached.
Read one in full at GET /v1/rail/engagements/{engagement_id}.
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
Your engagements, each with its parties, its dates and its standing.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RailApi;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
result, _, err := client.RAILAPI.RailEngagementsList(ctx).Limit(25).Status(heros.RailEngagementStatus("classified")).Classification(heros.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": false,
"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
Opening an engagement is how a working relationship starts existing on the platform. The identifier it answers with is the one every other call in this module takes, so this is the call your own flow has to get through before it can record anything else about the relationship.
Send the two parties, the date the work starts and the facts you attest to about the relationship. You get the engagement back with an identifier beginning rail_engagement_, which every later call takes as engagement_id.
Both parties are named by the subject_ref you chose for them at POST /v1/identity/consent_tokens.
The engagement comes back already in the classified standing, so the next step that moves it is POST /v1/rail/engagements/{engagement_id}/paper.
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 you just opened, with the identifier every later call about it takes.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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": false,
"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
A consumer that fell behind, through downtime or a delivery it could not take, has a gap in what it knows about engagements it holds. This read is the way back: you ask RAIL what the standings are now, instead of rebuilding them from events you did not receive.
You get one row per engagement, each naming the engagement, the standing it is in and the sequence number of its last change, with the sequence the whole report is current to.
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
A standing for every engagement in the report, with the sequence it is current to.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: 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#See what falls due across your engagements
rail.envelopes.list
This is every obligation envelope your organisation holds, whichever engagement it came from. So one screen in your product can show what falls due across all of them, without going engagement by engagement.
Send nothing and the envelopes come back, each carrying the engagement_id of the engagement whose obligations it states, which is what you match on in your own records.
Read one of them through the engagement it belongs to at GET /v1/rail/engagements/{engagement_id}/envelope, passing a row's engagement_id in the path.
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 obligation envelopes, each naming the engagement whose obligations it states.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: 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": false,
"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#Find an instrument and the engagement it papers
rail.instruments.list
An instrument lives under the engagement it papers, and every call that changes one goes through that engagement. This list does not, so a product holding an instrument identifier and nothing else can get back to the engagement it belongs to.
Send nothing and the instruments come back, each with its engagement_id, the template_code and clause_library_version it was built from, its status, and its seal once executed. Superseded versions are on the list too.
A row's id is what you send as instrument_id to GET /v1/rail/engagements/{engagement_id}/instruments/{instrument_id}, with that row's engagement_id as the other half of the path.
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
Instruments with the engagement each one papers and the state each has reached.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RailApi;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
result, _, err := client.RAILAPI.RailInstrumentsList(ctx).Limit(25).EngagementId("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Status(heros.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": false,
"mocked": true,
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"status": "drafted",
"template_code": "example",
"clause_library_version": "2026.08.1",
"clauses": [
{
"clause_code": "example",
"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
A sealed document carries a hash. This call takes the hash from a document somebody is holding, so your own support screen can ask about it without holding the engagement it came from.
Send the hash from the document in front of you, and instrument_id as well when you know it. You get valid and chain_intact back, with executed_at and superseded_by beside them.
Nothing has to exist first, and you do not need to hold the engagement the document came from.
Read both answers before you act on either. A document can be valid and still be one nobody is bound by any more, which is what superseded_by tells you.
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, with both answers and whatever later variation has replaced the instrument.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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#Which clauses a template holds
rail.clause_library.retrieve
This answers what a template is made of. You can show a person the heading of every clause a template assembles, instead of a list of codes, and cite the reference the version was legally approved under.
You get the library version in force and every clause in it, with its code, its version, the classifications it may appear in and whether it is mandatory. Each template names the classification it serves and the clauses it assembles.
Match a clause by clause_code, which is what a template's list names; a heading is there for a person to read.
No call adds, edits or removes a clause, and there is no field for a clause list you assembled yourself. What you choose is a template.
Every instrument carries a clause_library_version, so you can tell which reading of the library it rests on.
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 approved clauses and templates for the version in force, with the approval reference behind it.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: 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": "example",
"clause_version": "2026.08.1",
"applies_to": [
"employment"
],
"heading": "example",
"mandatory": true
}
],
"effective_from": "2026-09-01",
"templates": [
{
"template_code": "example",
"classification": "employment",
"clause_codes": [
"example"
]
}
]
}
/v1/rail/remediation_cases#See the cases standing against your engagements
rail.remediation_cases.list
A remediation case is a rule pack change held against one of your engagements. It names the change, the engagement, and the action it asks of you. So a compliance screen in your product can carry a row for every engagement with work outstanding, and the action to take already written on each one.
Send nothing and every case your organisation holds comes back, each carrying its own id, its status and that required_action. The engagement_id on a row is the engagement the case is about.
Read one in full at GET /v1/rail/remediation_cases/{remediation_case_id}, passing a row's id as remediation_case_id.
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
Every remediation case your organisation holds, each with the action it asks of you.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RailApi;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: limit (optional), starting_after (optional), status (optional)
result, _, err := client.RAILAPI.RailRemediationCasesList(ctx).Limit(25).Status(heros.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}#Read what one case asks of you
rail.remediation_cases.retrieve
This reads one case back as it stands now, so you know whether it is still waiting on you before you spend time on it. A case you listed an hour ago may already have been closed by a colleague, and this call is where you find that out.
Send the case's id in the path. You get the whole record: status for where it stands, pack_change for what changed in the rules, and required_action for what it asks of you.
A case's id and an engagement's id both start rail_engagement_, so the value on its own will not tell you which of the two you are holding.
An identifier with no case behind it answers not_found. Once you have done the work, close the case at POST /v1/rail/remediation_cases/{remediation_case_id}/resolve.
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
One case in full, with the pack change it names and the action it asks of you.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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 case you have acted on
rail.remediation_cases.resolve
This closes a case once you have done what it asked, so the cases still open are the work actually left. Anyone reading the list later sees which were closed and which were not.
Send action_taken in your own words, saying what you did, and add new_instrument_id when you drafted one to settle it. You get the case back with its status now resolved. The case has to exist already: its id comes from GET /v1/rail/remediation_cases.
Leave new_instrument_id out unless you drafted an instrument. When you did, it is that instrument's id from POST /v1/rail/engagements/{engagement_id}/instruments, not the engagement's.
Closing a case changes nothing about the engagement it names, and this is the only call that moves a case's status. Whatever the required action asked you to do on the engagement itself is a separate call.
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 same case with its status now resolved, and nothing further to call on it.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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 each module needs an engagement to be
rail.gating_contract.retrieve
The requirements the rest of the platform puts on an engagement are collected here, one rule to a module. Read them while you are designing, and your own flow can bring an engagement to the point a module requires before you make the call that depends on it.
Send no parameters. You get back the rules, each one naming a module, the engagement statuses named against it, and the contract row that rule comes from. The whole set carries a version.
Read it before you build, not when a payroll run is refused. If you keep a copy, keep the version with it and read the rules again when that moves, because a rule can change between versions.
Returns
The gating rules as they stand, with the version to record against whatever you build on them.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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 before it papers an engagement
rail.readiness.retrieve
RAIL asks for facts it does not establish itself, and this call is where it says which ones. Read it before the first engagement is open, because a fact another module owns is a piece of integration of its own, and cheaper to know about on day one.
Send no parameters. You get rows, one per fact RAIL asks for, each naming the fact, its owner and the assurance it needs, and always_enforced beside them.
The answer is RAIL's requirement and not one engagement's progress. For where a single engagement has reached, read it at GET /v1/rail/engagements/{engagement_id}.
Nothing else waits on this call.
Returns
Every fact RAIL asks for, with the module that owns it and the assurance it needs.
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 Heros 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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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": "rail",
"mode": "integrated",
"ready": true,
"rows": [
{
"fact": "anchored_subject",
"owner": "anchor",
"contract_row": "6.6.9",
"required": "attested",
"held": "attested",
"source": "integrated",
"satisfied": true,
"missing_because": null
}
],
"always_enforced": [
"consent_token",
"bank_account_validation",
"tax_identifier_validation",
"duplicate_detection"
],
"clause_library_in_force": "example",
"classification_pack_in_force": "example",
"parties_without_identity": 1
}
/v1/rail/engagements/{engagement_id}#One engagement in full
rail.engagements.retrieve
One engagement, read whole. This is the call to make before you move an engagement on, and the call to make after a refusal, because the standing it comes back in is what decides which steps are open to you.
You get the engagement as it stands, with both party references, the dates it runs between, what it was classified as and the standing it has reached.
From here, the classification record is at GET /v1/rail/engagements/{engagement_id}/classification and the obligation envelope at GET /v1/rail/engagements/{engagement_id}/envelope.
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 as it stands now, with the standing that decides what you can do next.
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=2026-09-01" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RAILApi } from '@heros/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: '2026-09-01' });// query parameters: as_of (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z?as_of=2026-09-01', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RAILApi(client)
# query parameters: as_of (optional)
result = api.rail_engagements_retrieve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', as_of='2026-09-01')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=2026-09-01',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RAILApi(new GuzzleHttp\Client(), $config);
# query parameters: as_of (optional)
$result = $api->railEngagementsRetrieve(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', as_of: '2026-09-01');<?php
// query parameters: as_of (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z?as_of=2026-09-01');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.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", "2026-09-01");// 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=2026-09-01"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RAILApi(config);
// query parameters: as_of (optional)
var result = api.RailEngagementsRetrieve(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", asOf: "2026-09-01");// 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=2026-09-01");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: as_of (optional)
result, _, err := client.RAILAPI.RailEngagementsRetrieve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").AsOf("2026-09-01").Execute()// query parameters: as_of (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z?as_of=2026-09-01", 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": false,
"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#Record the classification step
rail.engagements.classify
This is the classification step in an engagement's life, the one your flow calls between opening an engagement and papering it.
Send no request body. You get the engagement back, and the classification record for it is read separately at GET /v1/rail/engagements/{engagement_id}/classification.
The engagement is one you opened at POST /v1/rail/engagements, and it is in the classified standing from the moment you opened it, so this call does not move it between standings.
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 as it stands after the classification step.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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": false,
"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#What this engagement is classified as
rail.classifications.retrieve
This tells you how one engagement is classified, at any point in its life, so you do not have to store the answer yourself. Read it before you draft and you know which part of the clause library applies: GET /v1/rail/clause_library names the classification each template is written for.
Give the engagement's id in the path and you get the classification it carries, with the rule pack version it is held under.
One entry for every indicator is published separately, at GET /v1/rail/engagements/{engagement_id}/classification/rationale, and it is the same engagement id in the path.
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 this engagement carries, which is what you read before choosing a template.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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#See every indicator on record
rail.classifications.retrieve_rationale
This is the indicator by indicator record kept with an engagement's classification. That is enough to render a breakdown beside it, where a reader sees each indicator and how much it weighs, in place of a single word they take on trust.
Name the engagement in the path and you get one entry for every indicator, each with its weight and the rule pack version it is held under. An indicator that does not hold is listed with the rest.
If what you read here is wrong, POST /v1/rail/engagements/{engagement_id}/disputes is the published path for saying so, in grounds you write yourself.
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 indicator by indicator record, which is what a breakdown beside the classification is built from.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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
Terminating ends the relationship. It is the last standing an engagement reaches, so nothing moves it afterwards and a flow that has to show a relationship as finished can read that straight off the standing.
Send the reason you are ending it and the date it takes effect. You get the engagement back in the terminated standing, and the engagement.terminated event is published to your endpoints.
Only an engagement that is current or lapsed can be terminated. One in any other standing answers conflict.
An engagement that is disputed cannot be terminated. A disagreement is not ended by ending the relationship it is about.
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 engagement, now terminated, which is the last standing it reaches.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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": false,
"mocked": true,
"status": "terminated",
"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 how the work actually runs
rail.attestations.create
This puts a statement of fact on one engagement's file, in the name of the person making it. Your product then keeps one identifier and cites that statement later, holding no copy of its own.
You send the facts, the individual attesting to them and the version of the wording that person accepted, and you get back an attestation with its own id. The engagement has to exist first, from POST /v1/rail/engagements.
attesting_individual is the field to get right: the person who stood behind these facts, not a service account and not a team name.
There is no update and no delete here, so an attestation cannot be edited or withdrawn once it is made. Keep the id: nothing lists an engagement's attestations, so it is your only route to GET /v1/rail/engagements/{engagement_id}/attestations/{attestation_id}.
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 just recorded, carrying the id you will need to read it again.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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}#What was attested on an engagement
rail.attestations.retrieve
This reaches one statement of fact that was already lodged against an engagement. An attestation is only ever read back, never redone, so the engagement can move on through papering, execution and registration while the statement stays where it was.
Pass the attestation_id you stored when you attested, on the engagement it was lodged against, and you get that one attestation back.
Both identifiers in the path have to agree. An attestation is addressed only under the engagement it was made against, so the same attestation_id asked for under a different engagement answers not_found.
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 one attestation lodged under that identifier, for the engagement named in the path.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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
This turns an engagement into the document the two parties will be bound by. A draft binds nobody, so your product can show both of them what they are being asked to agree to while there is still nothing to undo.
Send the template_code you picked from GET /v1/rail/clause_library. You get a new instrument back under its own id, starting rail_instrument_, in drafted and with no seal on it yet.
The engagement has to exist first, from POST /v1/rail/engagements.
You choose a template and supply values. No field here takes prose, and a value cannot add a clause or change one, so what the instrument says is what the library says.
Record each party's assent at POST /v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/assents, then execute at POST /v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/execute.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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": false,
"mocked": true,
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"status": "drafted",
"template_code": "example",
"clause_library_version": "2026.08.1",
"clauses": [
{
"clause_code": "example",
"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}#Read one instrument and what became of it
rail.instruments.retrieve
This reads one instrument back whole: the template it came from, the clauses behind it and the variation that replaced it. So your product can show somebody the agreement that governs them and know it is the version in force.
Send both identifiers in the path. You get the instrument back with its clauses as codes and versions, status for where it has reached, and superseded_by once a variation has taken over.
An instrument is addressed under the engagement it papers, so keep a rail_instrument_ identifier together with its engagement_id. The instrument identifier on its own will not reach this call.
The clauses are codes, not wording. Read what each one says at GET /v1/rail/clause_library, follow superseded_by to the instrument in force, and read the executed document each party keeps at GET /v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/copy once it has been 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
One instrument whole, with the clauses behind it and the seal it carries once 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 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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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": false,
"mocked": true,
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"status": "drafted",
"template_code": "example",
"clause_library_version": "2026.08.1",
"clauses": [
{
"clause_code": "example",
"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
This is the moment the agreement binds. Up to here the document is a draft nobody is held to, so your product gains one fixed point to show as the moment these two parties became bound.
Send both identifiers in the path and no request body. You get the instrument back in executed, carrying the seal a draft does not have and the time it was sealed.
Record each party's assent first at POST /v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/assents.
An instrument that has already been executed, or one a variation replaced, answers conflict.
Executing the instrument does not move the engagement. POST /v1/rail/engagements/{engagement_id}/execute is a separate call on the engagement itself, and the executed document each party keeps is at GET /v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/copy from here on.
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 instrument in executed and sealed, which is the point the agreement binds.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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": false,
"mocked": true,
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"status": "executed",
"template_code": "example",
"clause_library_version": "2026.08.1",
"clauses": [
{
"clause_code": "example",
"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
This is how an agreement already in force is changed. What was executed is never edited and never destroyed, so the terms somebody signed stay readable at their own id and a disagreement has something to argue with.
Send reason, in your own words, saying why the instrument is being varied. What comes back carries its own id, which is the one to store.
The instrument has to have been executed, by POST /v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/execute.
reason is the one field here that takes your own words. It says why you are varying the instrument and never becomes part of what the instrument says.
A draft, or an instrument already superseded, answers conflict: vary the one in force. A reclassification is varied the same way, which is what a remediation case at GET /v1/rail/remediation_cases/{remediation_case_id} asks for.
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 not yet in force.
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":"example","variables":{}}'import { Configuration, RAILApi } from '@heros/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":"example","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": "example",
"variables": {}
}),
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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": "example", "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": "example", "variables": {}},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RAILApi(new GuzzleHttp\Client(), $config);
$result = $api->railInstrumentsSupersede($idempotencyKey, json_decode('{"reason":"The requester confirmed the work in person.","template_code":"example","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":"example","variables":{}}',
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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": "example",
"variables": {}
}
"""))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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": "example",
"variables": {}
}
""", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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": "example",
"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": false,
"mocked": true,
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"status": "superseded",
"template_code": "example",
"clause_library_version": "2026.08.1",
"clauses": [
{
"clause_code": "example",
"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 one party's agreement as evidence
rail.assents.create
An assent is your record that one named party agreed to one instrument. Each side of an engagement gets its own, so the worker's agreement reads back without touching the engaging party's.
You send the party's reference, the Passport they agreed under, how they signed, where they saw the instrument, and the hash of that exact version. You get the record and its id.
Send actor_ref exactly as you sent engaging_party_ref or worker_ref when you opened the engagement.
An assent is written once, and no endpoint amends or withdraws one.
Call POST /v1/rail/engagements/{engagement_id}/instruments first; this needs the instrument it returns. The party's Passport comes from POST /v1/identity/verifications in ANCHOR. Keep the id, because nothing lists an engagement's assents. Execute the instrument next, at POST /v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/execute.
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 just written, which is your evidence that this party agreed.
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:","device":"example"}'import { Configuration, RAILApi } from '@heros/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:","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:",
"device": "example"
}),
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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:", "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:", "device": "example"},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\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:","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:","device":"example"}',
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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:",
"device": "example"
}
"""))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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:",
"device": "example"
}
""", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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:",
"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}#What a party agreed to, and how
rail.assents.retrieve
One assent on its own, read by its identifier. Nothing lists an engagement's assents, so the identifier you kept when you recorded it is your whole route back to the record.
You send the engagement, instrument and assent identifiers in the path. You get back that one record: the instrument it binds to, the party's own reference, the Passport reference they agreed under, and how they signed.
This record is not the document. For the executed instrument itself, read GET /v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/copy.
Call POST /v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/assents first; this needs the record it returns. Nothing else waits on this call.
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 one assent you asked for, naming the party and the instrument it binds to.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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#Produce the executed document
rail.executed_copies.retrieve
This hands you the executed document itself, not the instrument record that describes it. Your product can show the document and the seal on it while holding an address instead of the file.
Name the engagement and the instrument in the path and you get back an executed_copy: the instrument_id it is a copy of, an address to fetch the document from, and the seal put on it at execution.
A drafted instrument has no copy. Execute it at POST /v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/execute first, and the copy stays readable after the instrument is superseded. Both path identifiers have to name the same pair: an instrument drafted under a different engagement answers not_found.
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 document's address, and the seal put on the instrument at execution.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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#What one engagement makes owed
rail.envelopes.retrieve
This answers what one engagement makes owed. The envelope has its own address under the engagement, so a payroll run already holding a rail_engagement_ identifier can ask for that engagement's obligations and nothing else.
Pass the engagement's id as engagement_id in the path and you get its envelope, one entry for each obligation that attaches to the engagement.
An obligation that still attaches but that a threshold or an exemption bears on carries threshold_note, which says what happened. Read it before you treat an entry as unqualified.
Check what has already been met at GET /v1/rail/engagements/{engagement_id}/discharges, which carries a reference to what happened elsewhere and no amount. An identifier with no engagement behind it answers not_found.
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
One engagement's obligation envelope, for your payroll and remittance to read.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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": false,
"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#See the evidence an engagement's obligations were met
rail.discharges.list
A discharge is evidence that one obligation on an engagement was met. So your product can show an engagement's confirmations on one screen, and tell a new one from a confirmation you have already seen, because a discharge keeps its identifier.
You name the engagement and get every discharge recorded against it: the authority the obligation was owed to, the month it covers, and the receipt or filing reference it was met with.
RAIL records that an obligation was discharged. It never moves the money that discharged it, so what comes back is a reference and never an amount.
The envelope at GET /v1/rail/engagements/{engagement_id}/envelope says what the engagement makes owed; this says what has been met, so read the two together. Nothing else waits on this call.
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
The confirmations held against this engagement, each one evidence that an obligation was met.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: 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#Put a disagreement on the record
rail.disputes.create
A dispute records that one side disagrees about an engagement, and why. The grounds stay on the record in the words they were raised in, so whoever later asks what the objection was reads what was actually said and not a label somebody chose for it.
Send raised_by for the side the disagreement comes from, and grounds for why it is raised. You get the dispute back under its own id, with status open.
The engagement has to exist, from POST /v1/rail/engagements. No list and no retrieve exists for disputes, so keep the id you get back: it is the only way to this record again.
Heros never rules on a dispute. The record says a disagreement exists and nothing more. Settle it at POST /v1/rail/engagements/{engagement_id}/disputes/{dispute_id}/resolve, passing that id as dispute_id.
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 as raised, under the identifier you will resolve it with later.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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#Close a disagreement once it is settled
rail.disputes.resolve
This is where a dispute ends. The two sides settle it between themselves, and this call is what puts that on the record. So a product holding a dispute it raised has one call that turns it from live to settled, and a timestamp saying when.
Send outcome, which is how the dispute was settled in your own words. You get the dispute back with status now resolved and resolved_at carrying when that was recorded.
The dispute has to exist, from POST /v1/rail/engagements/{engagement_id}/disputes, and its id is what you pass as dispute_id.
Nothing else waits on this call. No list and no retrieve exists for disputes, so the response you get here is the last reading of this record you will have.
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 dispute now resolved, carrying the moment it was settled.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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 one engagement's record
rail.audit_bundles.create
This asks RAIL to export one engagement's record and hands back an audit bundle, a record of its own with its own identifier and date. So your product can store one identifier, read the export later, and know when it was taken.
Send no request body. The engagement in the path is the one the bundle covers, and you get the bundle back under its own id, with created_at for the moment you asked. The engagement has to exist first, from POST /v1/rail/engagements.
Keep the id you get back. RAIL publishes no endpoint that lists bundles, so it is the only way back to this one.
No endpoint in RAIL deletes a bundle or edits one, and none deletes an engagement, an attestation or an instrument.
Read it back at GET /v1/rail/engagements/{engagement_id}/audit_bundles/{audit_bundle_id}, passing the id as audit_bundle_id and the same engagement as engagement_id.
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 bundle you just exported, carrying the identifier you need to read it again.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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}#Look up an export you took
rail.audit_bundles.retrieve
This reads back one audit bundle you already exported. So a product that kept its identifiers can read that export as often as it needs, and reading it exports nothing new.
Pass the bundle's id as audit_bundle_id, and the engagement you exported it on as engagement_id. You get back the same bundle the export returned, under the same id.
Both identifiers have to match. A bundle is addressed under the engagement it covers, so passing another of your own engagements answers not_found, the same answer as an audit_bundle_id that was never issued.
Read status before you act on what came back, and call this again while it is generating. No endpoint in RAIL deletes a bundle or edits 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_.
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 bundle you exported earlier, read back under the engagement it covers.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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
Papering is where an engagement stops being a classification and becomes a document to be signed. Your own flow reads the papered standing to show that the wording is being drawn up and that nothing binds anybody yet.
Send no request body. You get the engagement back in the papered standing, which is the one the executing step moves on from.
Only an engagement in the classified standing can be papered. One in any other standing answers conflict.
The instrument is a separate object. Draft it at POST /v1/rail/engagements/{engagement_id}/instruments, naming a template from the library at GET /v1/rail/clause_library.
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, now papered, with its parties and classification unchanged.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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": false,
"mocked": true,
"status": "papered",
"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
Executing is the point the agreement binds. Up to here a classification and a drafted instrument hold nobody to anything, so this is the step that separates a relationship being set up from one that is in force.
Send no request body. You get the engagement back in the executed standing, and the engagement.executed event is published to your endpoints.
Only an engagement in the papered standing can be executed. One in any other standing answers conflict.
This executes the engagement, not the instrument. Sealing the instrument is its own call, at POST /v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/execute.
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, now executed, which is the standing the agreement binds from.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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": false,
"mocked": true,
"status": "executed",
"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
Registering is the filing step, where the engagement is lodged wherever the jurisdiction requires engagements to be filed. It is a step of its own because filing and signing happen at different times, and one can go through without the other.
Send no request body. You get the engagement back in the registered standing, and the engagement.registered event is published to your endpoints.
Only an engagement in the executed standing can be registered. One in any other standing answers conflict.
Activate it next, at POST /v1/rail/engagements/{engagement_id}/activate.
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, now registered, filed where the jurisdiction requires it.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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": false,
"mocked": true,
"status": "registered",
"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
Activating is the last step in setting an engagement up, and current is where it stays while the relationship runs.
Send no request body. You get the engagement back in the current standing, which is the one the lapse, dispute and terminate steps are reached from.
Only an engagement in the registered standing can be activated. One in any other standing answers conflict.
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, now current, which is the standing the ending steps are reached from.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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": false,
"mocked": true,
"status": "current",
"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#Record that the term ended
rail.engagements.lapse
A lapse is an engagement that ran out, because its term ended and nothing renewed it. It is not a termination and not a dispute, and recording it as one of those would put the wrong reason in front of whoever reads the history later.
Send no request body. You get the engagement back in the lapsed standing, and the engagement.lapsed event is published to your endpoints.
Only an engagement in the current standing can be lapsed. One in any other standing answers conflict.
A lapsed engagement can still be terminated at POST /v1/rail/engagements/{engagement_id}/terminate, so lapsing is not the last thing that can happen to 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, now lapsed, which can still be terminated but not disputed.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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": false,
"mocked": true,
"status": "lapsed",
"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#Record that a party disputes the engagement
rail.engagements.dispute
A dispute is one side saying the agreement is not what was agreed. This records that the disagreement exists and does not decide it. Whether work carries on is decided by the instrument, not by this call.
Send no request body. You get the engagement back in the disputed standing, and the engagement.disputed event is published to your endpoints.
Only an engagement that is current or registered can be disputed. One in any other standing answers conflict.
Where the grounds belong is the dispute record at POST /v1/rail/engagements/{engagement_id}/disputes. An engagement that is disputed cannot be terminated.
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, now disputed, with the disagreement recorded and nothing about it decided.
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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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": false,
"mocked": true,
"status": "disputed",
"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#Replay an engagement's status history
rail.events.list
RAIL's engagements move through statuses, and its events are how that movement is recorded. Reading them back here lets a consumer that missed a delivery start again from the change it last handled, and lets your product show how an engagement reached the status it holds.
Give stream the id of the engagement whose history you want, and after the sequence you last handled; you get that engagement's events after it, in sequence order. Leave it out and you get every stream together, in the order RAIL recorded them.
sequence numbers run inside one engagement, so a number kept from one means nothing against another. Send after only with the stream you read it from; on its own it has nothing to count from. Across streams, move forward with starting_after.
Read any one of them in full at GET /v1/rail/events/{event_id}; each entry's id starts with evt_.
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
Your events in the order RAIL recorded them, each with the record it is about.
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 Heros 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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: 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": false,
"mocked": true,
"source": "anchor",
"request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "event",
"stream": "example",
"data": {}
}
],
"has_more": true
}
/v1/rail/events/{event_id}#Check what one event says
rail.events.retrieve
An event is RAIL's note of one thing that happened to one record, and this opens a single one of them. So your product can answer what an engagement's status was on the day you acted on it, reading the event and not where that engagement stands now.
Send the event's identifier in the path; it starts with evt_. You get that one event, with stream naming the record it belongs to and sequence saying where in that record's history it sits.
Those two are what a replay needs: send them to GET /v1/rail/events as stream and after to read what followed on that record. An identifier nothing holds answers not_found.
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 as RAIL recorded it, with the record it was about at that moment.
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 Heros 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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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": false,
"mocked": true,
"source": "anchor",
"request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "event",
"stream": "example",
"data": {}
}
/v1/rail/audit_entries#Who changed an engagement, and what was refused
rail.audit_entries.list
This is who changed a working relationship, and who was refused when they tried. Every attempt on an engagement or an instrument is recorded against the caller behind it, so your product can answer who executed or terminated an engagement, and when.
Ask for everything, or narrow it: one caller with actor_id, one engagement or instrument with resource_id, only what was refused with outcome, or a window of time. Entries come back newest first.
action is matched exactly against what the entry stored, so read the value off an entry before you filter on it. An approximation matches nothing, and an empty page is all you are told.
A read of yours that succeeded leaves no row here. What is left is the changes, and the attempts that did not go through.
Open one entry in full at GET /v1/rail/audit_entries/{audit_entry_id}.
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 attempts, newest first, each naming its caller, its route and its status.
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 aud_ 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 Heros staff member acting under a support grant, service when Heros 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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: 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": "aud_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "audit_entry",
"livemode": false,
"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": "engagements",
"resource_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
],
"has_more": true
}
/v1/rail/audit_entries/{audit_entry_id}#Read back one attempt you noted
rail.audit_entries.retrieve
An audit entry is one attempt, however it ended, and this reads one back in full. Its identifier never changes, so your own record of an execution can keep a permanent reference to the row naming who performed it.
Send the entry's id in the path and you get that one attempt: when it was made, which caller made it, the route it aimed at, and the status it was answered with.
resource_id is null when the attempt was aimed at a collection, as creating an engagement is, so handle the null before you follow it. Where it names an engagement, it opens that engagement at GET /v1/rail/engagements/{engagement_id}.
An entry records the attempt and no more: no reason for a refusal, nothing about what the change did.
Read what a change did to the engagement at GET /v1/rail/events. An identifier matching no entry answers not_found.
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 aud_.
Returns
One attempt, with the caller behind it, the route it named and how it ended.
id
string
requiredThe entry's identifier, as it appears on GET /v1/rail/audit_entries; it starts with aud_ 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 Heros staff member acting under a support grant, service when Heros 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 '@heros/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 heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.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 = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\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.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.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 Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.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": "aud_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "audit_entry",
"livemode": false,
"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": "engagements",
"resource_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}