DDroomwork Developers

Version 1.0.0

Droomwork RAIL

Classification, instruments and the legal standing of a working relationship.

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

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?

GET/v1/rail/engagements#

List engagements

rail.engagements.list

Your engagements, newest first.

Query parameters

limit integer optional

How 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 optional

The 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 optional

Only engagements in one status: classified, papered, executed, registered, current, lapsed, disputed or terminated. Leave it out to get every status.

classifiedpaperedexecutedregisteredcurrentlapseddisputedterminated
classification string optional

Only 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_based

Returns

A page of engagements.

object always "list" required

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

data array of Engagement required

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

15 fields of Engagement
id string required

The 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" required

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

livemode boolean required

Which realm this record is in: false is the sandbox, true is live. Read it before you act on anything.

mocked boolean required

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

Transitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.

classifiedpaperedexecutedregisteredcurrentlapseddisputedterminated
engaging_party_ref string required

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

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 engaging party.

classification one of optional

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

Classificationor
classification_review_required boolean optional

True when confidence was low or the result was contested. A person decides.

instrument_in_force_id string · nullable optional

The 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 optional

The 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 optional

The 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 optional

The date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.

as_of string · date · nullable optional

Present when this was answered as at a past date rather than now.

created_at string · date-time optional

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

has_more boolean required

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements?limit=25&status=classified&classification=employment" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

// query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
const result = await api.railEngagementsList({ limit: 25, status: 'classified', classification: 'employment' });
// query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements?limit=25&status=classified&classification=employment', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

# query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
result = api.rail_engagements_list(limit=25, status='classified', classification='employment')
import os

import requests

# query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/rail/engagements?limit=25&status=classified&classification=employment',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
    },
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

# query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
$result = $api->railEngagementsList(limit: 25, status: 'classified', classification: 'employment');
<?php
// query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements?limit=25&status=classified&classification=employment');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;
import com.droomwork.sdk.model.*;

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

// query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
var result = api.railEngagementsList(25, null, RailEngagementStatus.fromValue("classified"), RailClassification.fromValue("employment"));
// query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements?limit=25&status=classified&classification=employment"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

// query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
var result = api.RailEngagementsList(limit: 25, status: RailEngagementStatus.Classified, classification: RailClassification.Employment);
// query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/engagements?limit=25&status=classified&classification=employment");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
result, _, err := client.RAILAPI.RailEngagementsList(ctx).Limit(25).Status(droomwork.RailEngagementStatus("classified")).Classification(droomwork.RailClassification("employment")).Execute()
// query parameters: limit (optional), starting_after (optional), status (optional), classification (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements?limit=25&status=classified&classification=employment", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "list",
  "data": [
    {
      "id": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "engagement",
      "livemode": true,
      "mocked": true,
      "status": "classified",
      "engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "classification": "employment",
      "classification_review_required": true,
      "instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "starts_on": "2026-09-01",
      "ends_on": "2026-09-01",
      "as_of": "2026-09-01",
      "created_at": "2026-09-01T09:00:00Z"
    }
  ],
  "has_more": true
}
POST/v1/rail/engagements#

Open an engagement

rail.engagements.create

Records the two parties and the facts you attest to. The attesting individual and the timestamp are kept permanently, so you can show what was relied on and who said it.

Creating an engagement classifies nothing. Call classify next.

Headers

Idempotency-Key string required

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

Body

engaging_party_ref string required

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

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

The date the engagement starts, as a calendar date in YYYY-MM-DD.

ends_on string · date optional

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

The 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 optional

true 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 optional

true when the engaging party sets the hours or days the work is done, rather than the worker choosing them.

provides_equipment boolean optional

true 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 optional

true when the worker works for this engaging party alone and takes no engagements from anyone else.

can_send_a_substitute boolean optional

true 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 optional

true 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 optional

true 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 optional

How long the engagement is expected to run, in whole months, 0 or more.

occupation_code string optional

The 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 optional

A named person, recorded permanently. Not a system account.

Returns

The engagement, awaiting classification.

id string required

The 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" required

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

livemode boolean required

Which realm this record is in: false is the sandbox, true is live. Read it before you act on anything.

mocked boolean required

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

Transitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.

classifiedpaperedexecutedregisteredcurrentlapseddisputedterminated
engaging_party_ref string required

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

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 engaging party.

classification one of optional

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

Classificationor
classification_review_required boolean optional

True when confidence was low or the result was contested. A person decides.

instrument_in_force_id string · nullable optional

The 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 optional

The 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 optional

The 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 optional

The date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.

as_of string · date · nullable optional

Present when this was answered as at a past date rather than now.

created_at string · date-time optional

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

Other responses

400The request could not be read, or a value was refused.
401No valid credential was presented.
403The credential does not carry the required scope.
422A required fact is missing. Call the readiness endpoint to see what.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"engaging_party_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","worker_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","starts_on":"2026-09-01","attested_facts":{"controls_how_work_is_done":true,"controls_when_work_is_done":true,"provides_equipment":true,"exclusive_to_this_party":true,"can_send_a_substitute":true,"bears_financial_risk":true,"integrated_into_organisation":true,"duration_months":0,"occupation_code":"ng-7412"},"ends_on":"2026-09-01","attesting_individual":"example"}'
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railEngagementsCreate({
  idempotencyKey: crypto.randomUUID(),
  railEngagementCreateRequest: {"engagingPartyRef":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","workerRef":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","startsOn":"2026-09-01","attestedFacts":{"controlsHowWorkIsDone":true,"controlsWhenWorkIsDone":true,"providesEquipment":true,"exclusiveToThisParty":true,"canSendASubstitute":true,"bearsFinancialRisk":true,"integratedIntoOrganisation":true,"durationMonths":0,"occupationCode":"ng-7412"},"endsOn":"2026-09-01","attestingIndividual":"example"},
});
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    "engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
    "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
    "starts_on": "2026-09-01",
    "attested_facts": {
      "controls_how_work_is_done": true,
      "controls_when_work_is_done": true,
      "provides_equipment": true,
      "exclusive_to_this_party": true,
      "can_send_a_substitute": true,
      "bears_financial_risk": true,
      "integrated_into_organisation": true,
      "duration_months": 0,
      "occupation_code": "ng-7412"
    },
    "ends_on": "2026-09-01",
    "attesting_individual": "example"
  }),
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_engagements_create(body={"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "starts_on": "2026-09-01", "attested_facts": {"controls_how_work_is_done": True, "controls_when_work_is_done": True, "provides_equipment": True, "exclusive_to_this_party": True, "can_send_a_substitute": True, "bears_financial_risk": True, "integrated_into_organisation": True, "duration_months": 0, "occupation_code": "ng-7412"}, "ends_on": "2026-09-01", "attesting_individual": "example"})
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/rail/engagements',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={"engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "starts_on": "2026-09-01", "attested_facts": {"controls_how_work_is_done": True, "controls_when_work_is_done": True, "provides_equipment": True, "exclusive_to_this_party": True, "can_send_a_substitute": True, "bears_financial_risk": True, "integrated_into_organisation": True, "duration_months": 0, "occupation_code": "ng-7412"}, "ends_on": "2026-09-01", "attesting_individual": "example"},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->railEngagementsCreate($idempotencyKey, json_decode('{"engaging_party_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","worker_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","starts_on":"2026-09-01","attested_facts":{"controls_how_work_is_done":true,"controls_when_work_is_done":true,"provides_equipment":true,"exclusive_to_this_party":true,"can_send_a_substitute":true,"bears_financial_risk":true,"integrated_into_organisation":true,"duration_months":0,"occupation_code":"ng-7412"},"ends_on":"2026-09-01","attesting_individual":"example"}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Content-Type: application/json',
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
  CURLOPT_POSTFIELDS => '{"engaging_party_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","worker_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","starts_on":"2026-09-01","attested_facts":{"controls_how_work_is_done":true,"controls_when_work_is_done":true,"provides_equipment":true,"exclusive_to_this_party":true,"can_send_a_substitute":true,"bears_financial_risk":true,"integrated_into_organisation":true,"duration_months":0,"occupation_code":"ng-7412"},"ends_on":"2026-09-01","attesting_individual":"example"}',
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

var result = api.railEngagementsCreate(idempotencyKey, body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", UUID.randomUUID().toString())
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
          "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
          "starts_on": "2026-09-01",
          "attested_facts": {
            "controls_how_work_is_done": true,
            "controls_when_work_is_done": true,
            "provides_equipment": true,
            "exclusive_to_this_party": true,
            "can_send_a_substitute": true,
            "bears_financial_risk": true,
            "integrated_into_organisation": true,
            "duration_months": 0,
            "occupation_code": "ng-7412"
          },
          "ends_on": "2026-09-01",
          "attesting_individual": "example"
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

var result = api.RailEngagementsCreate(idempotencyKey, body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
    {
      "engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "starts_on": "2026-09-01",
      "attested_facts": {
        "controls_how_work_is_done": true,
        "controls_when_work_is_done": true,
        "provides_equipment": true,
        "exclusive_to_this_party": true,
        "can_send_a_substitute": true,
        "bears_financial_risk": true,
        "integrated_into_organisation": true,
        "duration_months": 0,
        "occupation_code": "ng-7412"
      },
      "ends_on": "2026-09-01",
      "attesting_individual": "example"
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.RAILAPI.RailEngagementsCreate(ctx).IdempotencyKey(key).RailEngagementCreateRequest(body).Execute()
body := strings.NewReader(`{
  "engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "starts_on": "2026-09-01",
  "attested_facts": {
    "controls_how_work_is_done": true,
    "controls_when_work_is_done": true,
    "provides_equipment": true,
    "exclusive_to_this_party": true,
    "can_send_a_substitute": true,
    "bears_financial_risk": true,
    "integrated_into_organisation": true,
    "duration_months": 0,
    "occupation_code": "ng-7412"
  },
  "ends_on": "2026-09-01",
  "attesting_individual": "example"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "engagement",
  "livemode": true,
  "mocked": true,
  "status": "classified",
  "engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "classification": "employment",
  "classification_review_required": true,
  "instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "starts_on": "2026-09-01",
  "ends_on": "2026-09-01",
  "as_of": "2026-09-01",
  "created_at": "2026-09-01T09:00:00Z"
}
GET/v1/rail/reconcile#

Recover from missed lapse and termination events

rail.engagements.reconcile

The current standing of every engagement you hold an interest in, with a sequence number.

Call this if you dropped a webhook or your consumer was down. Otherwise a missed lapse leaves a worker taking offers on an engagement that is no longer lawful, and nothing tells you.

Query parameters

since_sequence integer optional

Only changes after this sequence number: pass the as_of_sequence from your last call to GET /v1/rail/reconcile. Leave it out to get the current standing of every engagement.

Returns

The current standing of every engagement you hold an interest in.

object always "engagement_reconciliation" required

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

The 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 optional

When this report was produced, as an RFC 3339 timestamp in UTC.

engagements array of object required

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

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

Transitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.

classifiedpaperedexecutedregisteredcurrentlapseddisputedterminated
sequence integer required

The 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 optional

When the engagement last changed standing, as an RFC 3339 timestamp in UTC.

Other responses

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

Errors it can return

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

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

// query parameters: since_sequence (optional)
const result = await api.railEngagementsReconcile({ sinceSequence: 0 });
// query parameters: since_sequence (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/reconcile?since_sequence=0', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

# query parameters: since_sequence (optional)
result = api.rail_engagements_reconcile(since_sequence=0)
import os

import requests

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

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

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

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

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

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

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

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

// query parameters: since_sequence (optional)
result, _, err := client.RAILAPI.RailEngagementsReconcile(ctx).SinceSequence(0).Execute()
// query parameters: since_sequence (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/reconcile?since_sequence=0", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "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"
}
GET/v1/rail/envelopes#

List obligation envelopes

rail.envelopes.list

What each of your engagements makes owed, for your payroll and remittance to read.

No envelope carries an amount. It carries the basis your payroll computes the amount from.

Query parameters

limit integer optional

How 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 optional

The 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 optional

Only the envelopes of one engagement, by its id, which starts with rail_engagement_ and comes from POST /v1/rail/engagements or GET /v1/rail/engagements. Leave it out to list envelopes across all your engagements.

Returns

A page of envelopes.

object always "list" required

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

data array of ObligationEnvelope required

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

10 fields of ObligationEnvelope
id string required

The 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" required

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

livemode boolean required

Which realm this record is in: false is the sandbox, true is live. Read it before you act on anything.

mocked boolean required

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

The 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 optional

Decided by the versioned pack. No model decides it and you can't assert one yourself.

employmentfixed_term_employmentindependent_contractingapprenticeshipcasualtask_based
obligations array of object required

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

The 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 optional

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

How an amount is worked out, not what it is. Your payroll computes the figure from it.

effective_from string · date required

The 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 optional

The last date the obligation applies, as a calendar date in YYYY-MM-DD. null while it has no end date.

frequency string required

How 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_engagement
threshold_note string optional

Why a threshold or exemption did or did not apply here.

rule_pack_version string optional

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

When this envelope was published for your payroll and remittance to read, as an RFC 3339 timestamp in UTC.

supersedes string · nullable optional

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

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: limit (optional), starting_after (optional), engagement_id (optional)
curl -X GET "https://sandbox.droomwork.io/v1/rail/envelopes?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

// query parameters: limit (optional), starting_after (optional), engagement_id (optional)
const result = await api.railEnvelopesList({ limit: 25, engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
// query parameters: limit (optional), starting_after (optional), engagement_id (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/envelopes?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

# query parameters: limit (optional), starting_after (optional), engagement_id (optional)
result = api.rail_envelopes_list(limit=25, engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

# query parameters: limit (optional), starting_after (optional), engagement_id (optional)
response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/rail/envelopes?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
    },
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

# query parameters: limit (optional), starting_after (optional), engagement_id (optional)
$result = $api->railEnvelopesList(limit: 25, engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
// query parameters: limit (optional), starting_after (optional), engagement_id (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/envelopes?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

// query parameters: limit (optional), starting_after (optional), engagement_id (optional)
var result = api.railEnvelopesList(25, null, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
// query parameters: limit (optional), starting_after (optional), engagement_id (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/envelopes?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

// query parameters: limit (optional), starting_after (optional), engagement_id (optional)
var result = api.RailEnvelopesList(limit: 25, engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
// query parameters: limit (optional), starting_after (optional), engagement_id (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/envelopes?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: limit (optional), starting_after (optional), engagement_id (optional)
result, _, err := client.RAILAPI.RailEnvelopesList(ctx).Limit(25).EngagementId("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
// query parameters: limit (optional), starting_after (optional), engagement_id (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/envelopes?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "list",
  "data": [
    {
      "id": "rail_envelope_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "obligation_envelope",
      "livemode": true,
      "mocked": true,
      "engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "obligations": [
        {
          "authority": "Rivers State Internal Revenue Service",
          "basis": "paye_graduated_bands",
          "effective_from": "2026-09-01",
          "frequency": "monthly",
          "authority_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
          "effective_to": "2026-09-01",
          "threshold_note": "example"
        }
      ],
      "published_at": "2026-09-01T09:00:00Z",
      "classification": "employment",
      "rule_pack_version": "2026.08.1",
      "supersedes": "example"
    }
  ],
  "has_more": true
}
GET/v1/rail/instruments#

List instruments

rail.instruments.list

Instruments across your engagements, including superseded versions. The chain is the record.

Query parameters

limit integer optional

How 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 optional

The 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 optional

Only 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 optional

Only 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_assentexecutedexpiredsuperseded

Returns

A page of instruments.

object always "list" required

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

data array of Instrument required

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

16 fields of Instrument
id string required

The 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" required

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

livemode boolean required

Which realm this record is in: false is the sandbox, true is live. Read it before you act on anything.

mocked boolean required

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

The 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 required
draftedawaiting_assentexecutedexpiredsuperseded
template_code string required

A template from the approved set. Not wording.

clauses array of object optional

The clauses used, by reference. Every one comes from the approved library, as written there.

2 fields
clause_code string required

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

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

The 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 optional

The 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 optional

The 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 optional

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

Sealor
supersedes string · nullable optional

The 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 optional

The 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 optional

An unexecuted instrument lapses after this, and the lapse is recorded.

executed_at string · date-time · nullable optional

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

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
curl -X GET "https://sandbox.droomwork.io/v1/rail/instruments?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=drafted" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

// query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
const result = await api.railInstrumentsList({ limit: 25, engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', status: 'drafted' });
// query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/instruments?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=drafted', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

# query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
result = api.rail_instruments_list(limit=25, engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', status='drafted')
import os

import requests

# query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/rail/instruments?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=drafted',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
    },
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

# query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
$result = $api->railInstrumentsList(limit: 25, engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', status: 'drafted');
<?php
// query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/instruments?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=drafted');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;
import com.droomwork.sdk.model.*;

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

// query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
var result = api.railInstrumentsList(25, null, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", RailInstrumentStatus.fromValue("drafted"));
// query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/instruments?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=drafted"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

// query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
var result = api.RailInstrumentsList(limit: 25, engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", status: RailInstrumentStatus.Drafted);
// query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/instruments?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=drafted");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
result, _, err := client.RAILAPI.RailInstrumentsList(ctx).Limit(25).EngagementId("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Status(droomwork.RailInstrumentStatus("drafted")).Execute()
// query parameters: limit (optional), starting_after (optional), engagement_id (optional), status (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/instruments?limit=25&engagement_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=drafted", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "list",
  "data": [
    {
      "id": "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "instrument",
      "livemode": true,
      "mocked": true,
      "engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "status": "drafted",
      "template_code": "no_payee_destination",
      "clause_library_version": "2026.08.1",
      "clauses": [
        {
          "clause_code": "no_payee_destination",
          "clause_version": "2026.08.1"
        }
      ],
      "rule_pack_version": "2026.08.1",
      "legal_approval_reference": "paye-2026-09-rivers",
      "seal": {
        "hash": "sha256:9f2c1e0043a1b8",
        "previous_hash": "sha256:9f2c1e0043a1b8",
        "sealed_at": "2026-09-01T09:00:00Z",
        "algorithm": "sha256"
      },
      "supersedes": "example",
      "superseded_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "expires_at": "2026-09-01T09:00:00Z",
      "executed_at": "2026-09-01T09:00:00Z"
    }
  ],
  "has_more": true
}
POST/v1/rail/instruments/verify#

Verify a sealed instrument independently

rail.instruments.verify

Checks a tamper evident hash against the chain and tells you whether the document you hold is the document that was executed.

You don't need to hold the engagement to call this.

Body

hash string required

The seal hash from the document you hold.

instrument_id string optional

The id of the instrument the document you hold claims to be, starting with rail_instrument_, from rail.instruments.create or rail.instruments.supersede. Pass it when you know it; the hash alone is enough to verify.

Returns

The verification result.

object always "instrument_verification" required

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

valid boolean required

Whether this hash matches an instrument that was executed.

chain_intact boolean required

Whether the chain from that instrument back to its origin is unbroken.

instrument_id string · nullable optional

The 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 optional

When the matched instrument was executed, as an RFC 3339 timestamp in UTC. null when nothing matched.

superseded_by string · nullable optional

Set when a later variation replaced the instrument you are holding.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/instruments/verify" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"hash":"sha256:9f2c1e0043a1b8","instrument_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}'
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railInstrumentsVerify({
  railInstrumentVerifyRequest: {"hash":"sha256:9f2c1e0043a1b8","instrumentId":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"},
});
const response = await fetch('https://sandbox.droomwork.io/v1/rail/instruments/verify', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "hash": "sha256:9f2c1e0043a1b8",
    "instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
  }),
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_instruments_verify(body={"hash": "sha256:9f2c1e0043a1b8", "instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"})
import os

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/rail/instruments/verify',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
    },
    json={"hash": "sha256:9f2c1e0043a1b8", "instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->railInstrumentsVerify(json_decode('{"hash":"sha256:9f2c1e0043a1b8","instrument_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/instruments/verify');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => '{"hash":"sha256:9f2c1e0043a1b8","instrument_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}',
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

var result = api.railInstrumentsVerify(body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/instruments/verify"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .header("Content-Type", "application/json")
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "hash": "sha256:9f2c1e0043a1b8",
          "instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

var result = api.RailInstrumentsVerify(body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/instruments/verify");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Content = new StringContent("""
    {
      "hash": "sha256:9f2c1e0043a1b8",
      "instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.RAILAPI.RailInstrumentsVerify(ctx).RailInstrumentVerifyRequest(body).Execute()
body := strings.NewReader(`{
  "hash": "sha256:9f2c1e0043a1b8",
  "instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/instruments/verify", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "instrument_verification",
  "valid": true,
  "chain_intact": true,
  "instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "executed_at": "2026-09-01T09:00:00Z",
  "superseded_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
GET/v1/rail/clause_library#

Read the approved clause library

rail.clause_library.retrieve

Every clause an instrument can be built from, with the legal approval reference behind it.

Read only. There is no endpoint that changes it, and nothing drafts, edits or paraphrases a clause for you.

Query parameters

version string optional

The library version to read, such as 2026.08.1: the clause_library_version on an instrument from GET /v1/rail/instruments, or the version on this response. Leave it out to get the version currently in force.

Returns

The clause library.

object always "clause_library" required

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

version string required

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

The 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 optional

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

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

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

The clause's version in this library. An instrument records the version it used, so a later change never alters what was executed.

heading string optional

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

Classifications this clause may appear in.

mandatory boolean optional

true 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 optional

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

The code you pass as template_code when drafting an instrument. It selects a template, never wording.

classification string required

Decided by the versioned pack. No model decides it and you can't assert one yourself.

employmentfixed_term_employmentindependent_contractingapprenticeshipcasualtask_based
clause_codes array of string optional

The 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

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

Errors it can return

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

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

// query parameters: version (optional)
const result = await api.railClauseLibraryRetrieve({ version: '2026.08.1' });
// query parameters: version (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/clause_library?version=2026.08.1', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

# query parameters: version (optional)
result = api.rail_clause_library_retrieve(version='2026.08.1')
import os

import requests

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

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

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

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

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

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

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

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

// query parameters: version (optional)
result, _, err := client.RAILAPI.RailClauseLibraryRetrieve(ctx).Version("2026.08.1").Execute()
// query parameters: version (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/clause_library?version=2026.08.1", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "clause_library",
  "version": "2026.08.1",
  "legal_approval_reference": "paye-2026-09-rivers",
  "clauses": [
    {
      "clause_code": "no_payee_destination",
      "clause_version": "2026.08.1",
      "applies_to": [
        "employment"
      ],
      "heading": "example",
      "mandatory": true
    }
  ],
  "effective_from": "2026-09-01",
  "templates": [
    {
      "template_code": "no_payee_destination",
      "classification": "employment",
      "clause_codes": [
        "no_payee_destination"
      ]
    }
  ]
}
GET/v1/rail/remediation_cases#

List remediation cases

rail.remediation_cases.list

Opened when a rule pack change makes one of your live engagements non compliant. Each case names the specific change and the action you need to take.

Query parameters

limit integer optional

How 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 optional

The 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 optional

Only 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_progressresolved

Returns

A page of cases.

object always "list" required

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

data array of RemediationCase required

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

10 fields of RemediationCase
id string required

The 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" required

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

engagement_id string required

The 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 required
openin_progressresolved
pack_change object required

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

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

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

What actually changed in the rules, in plain words.

why_non_compliant string optional

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

What 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 optional

Transitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.

classifiedpaperedexecutedregisteredcurrentlapseddisputedterminated
opened_at string · date-time optional

When the case was opened, as an RFC 3339 timestamp in UTC.

resolved_by string · nullable optional

Who closed the case: the actor that called resolve. null until the case is resolved.

has_more boolean required

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: limit (optional), starting_after (optional), status (optional)
curl -X GET "https://sandbox.droomwork.io/v1/rail/remediation_cases?limit=25&status=open" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

// query parameters: limit (optional), starting_after (optional), status (optional)
const result = await api.railRemediationCasesList({ limit: 25, status: 'open' });
// query parameters: limit (optional), starting_after (optional), status (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/remediation_cases?limit=25&status=open', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

# query parameters: limit (optional), starting_after (optional), status (optional)
result = api.rail_remediation_cases_list(limit=25, status='open')
import os

import requests

# query parameters: limit (optional), starting_after (optional), status (optional)
response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/rail/remediation_cases?limit=25&status=open',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
    },
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

# query parameters: limit (optional), starting_after (optional), status (optional)
$result = $api->railRemediationCasesList(limit: 25, status: 'open');
<?php
// query parameters: limit (optional), starting_after (optional), status (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/remediation_cases?limit=25&status=open');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;
import com.droomwork.sdk.model.*;

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

// query parameters: limit (optional), starting_after (optional), status (optional)
var result = api.railRemediationCasesList(25, null, RailRemediationStatus.fromValue("open"));
// query parameters: limit (optional), starting_after (optional), status (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/remediation_cases?limit=25&status=open"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

// query parameters: limit (optional), starting_after (optional), status (optional)
var result = api.RailRemediationCasesList(limit: 25, status: RailRemediationStatus.Open);
// query parameters: limit (optional), starting_after (optional), status (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/remediation_cases?limit=25&status=open");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: limit (optional), starting_after (optional), status (optional)
result, _, err := client.RAILAPI.RailRemediationCasesList(ctx).Limit(25).Status(droomwork.RailRemediationStatus("open")).Execute()
// query parameters: limit (optional), starting_after (optional), status (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/remediation_cases?limit=25&status=open", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "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
}
GET/v1/rail/remediation_cases/{remediation_case_id}#

Retrieve a remediation case

rail.remediation_cases.retrieve

What changed in the pack, what it means for this engagement, and what you need to do.

Path parameters

remediation_case_id string required

The case's id, as listed at GET /v1/rail/remediation_cases or carried in data on the remediation_case.opened event. It starts with rail_engagement_.

Returns

The case.

id string required

The 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" required

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

engagement_id string required

The 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 required
openin_progressresolved
pack_change object required

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

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

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

What actually changed in the rules, in plain words.

why_non_compliant string optional

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

What 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 optional

Transitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.

classifiedpaperedexecutedregisteredcurrentlapseddisputedterminated
opened_at string · date-time optional

When the case was opened, as an RFC 3339 timestamp in UTC.

resolved_by string · nullable optional

Who closed the case: the actor that called resolve. null until the case is resolved.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railRemediationCasesRetrieve({ remediationCaseId: '{remediation_case_id}' });
const response = await fetch('https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_remediation_cases_retrieve(remediation_case_id='{remediation_case_id}')
import os

import requests

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

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

$result = $api->railRemediationCasesRetrieve(remediation_case_id: '{remediation_case_id}');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

var result = api.railRemediationCasesRetrieve("{remediation_case_id}");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

var result = api.RailRemediationCasesRetrieve(remediationCaseId: "{remediation_case_id}");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.RAILAPI.RailRemediationCasesRetrieve(ctx, "{remediation_case_id}").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "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"
}
POST/v1/rail/remediation_cases/{remediation_case_id}/resolve#

Close a remediation case

rail.remediation_cases.resolve

Records what you did and who did it.

Path parameters

remediation_case_id string required

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

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

Body

action_taken string required

What 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 optional

The id of the instrument you drafted to remedy the case, starting with rail_instrument_, from rail.instruments.supersede or rail.instruments.create. Leave it out when no new instrument was needed.

Returns

The resolved case.

id string required

The 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" required

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

engagement_id string required

The 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 required
openin_progressresolved
pack_change object required

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

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

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

What actually changed in the rules, in plain words.

why_non_compliant string optional

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

What 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 optional

Transitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.

classifiedpaperedexecutedregisteredcurrentlapseddisputedterminated
opened_at string · date-time optional

When the case was opened, as an RFC 3339 timestamp in UTC.

resolved_by string · nullable optional

Who closed the case: the actor that called resolve. null until the case is resolved.

Other responses

400The request could not be read, or a value was refused.
401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.
409The record is not in a state that allows this.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D/resolve" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"action_taken":"example","new_instrument_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}'
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railRemediationCasesResolve({
  remediationCaseId: '{remediation_case_id}',
  idempotencyKey: crypto.randomUUID(),
  railRemediationResolveRequest: {"actionTaken":"example","newInstrumentId":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"},
});
const response = await fetch('https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D/resolve', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    "action_taken": "example",
    "new_instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
  }),
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_remediation_cases_resolve(remediation_case_id='{remediation_case_id}', body={"action_taken": "example", "new_instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"})
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D/resolve',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={"action_taken": "example", "new_instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->railRemediationCasesResolve($idempotencyKey, json_decode('{"action_taken":"example","new_instrument_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D/resolve');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Content-Type: application/json',
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
  CURLOPT_POSTFIELDS => '{"action_taken":"example","new_instrument_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}',
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

var result = api.railRemediationCasesResolve("{remediation_case_id}", idempotencyKey, body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D/resolve"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", UUID.randomUUID().toString())
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "action_taken": "example",
          "new_instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

var result = api.RailRemediationCasesResolve(remediationCaseId: "{remediation_case_id}", idempotencyKey, body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D/resolve");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
    {
      "action_taken": "example",
      "new_instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.RAILAPI.RailRemediationCasesResolve(ctx, "{remediation_case_id}").IdempotencyKey(key).RailRemediationResolveRequest(body).Execute()
body := strings.NewReader(`{
  "action_taken": "example",
  "new_instrument_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/remediation_cases/%7Bremediation_case_id%7D/resolve", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "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"
}
GET/v1/rail/gating_contract#

What downstream modules require of an engagement

rail.gating_contract.retrieve

What each module needs an engagement to be before it will act. Dispatch needs an executed engagement. Payroll needs one that is registered or current. Read this before you build rather than finding out when a run is refused.

Returns

The gating contract.

object always "gating_contract" required

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

version string required

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

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

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

matchrunremitroute
requires_status array of EngagementStatus required

The 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 optional

The rule in plain words, where there is more to say than the status list. Not present on every rule.

contract_row string required

The 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

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

Errors it can return

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

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

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

import droomwork

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

result = api.rail_gating_contract_retrieve()
import os

import requests

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

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

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

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

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

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

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

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

result, _, err := client.RAILAPI.RailGatingContractRetrieve(ctx).Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/gating_contract", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "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."
    }
  ]
}
GET/v1/rail/readiness#

What RAIL needs, what you have, and what is missing

rail.readiness.retrieve

Both parties need an anchored identity before an instrument can execute. This tells you whether that comes from ANCHOR or you supply it, and whether a clause library and classification pack are in force.

Returns

The readiness report.

object always "readiness_report" required

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

module string required

The 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).

anchorproofrailflowmatchrunremitroute
mode string required

integrated means a required fact comes from the Droomwork module that owns it; standalone means you supply it yourself under an attestation. The checks are the same in both, and what each fact is worth is recorded on its row as held.

integratedstandalone
ready boolean required

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

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

Named for what it is, not for who supplies it.

owner string required

The sibling module that owns this fact when running integrated.

contract_row string optional
required string required

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

attestedverified
held one of required
Assuranceor
source string · nullable optional
integratedstandalonenull
satisfied boolean required
missing_because string · nullable optional
not_suppliedassurance_too_lownull
always_enforced array of string optional

Gates 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 optional

No instrument can be drafted without one.

classification_pack_in_force string · nullable optional

The 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 optional

Counts both sides. An engaging party without an identity blocks execution too.

Other responses

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

Errors it can return

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

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

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

import droomwork

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

result = api.rail_readiness_retrieve()
import os

import requests

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

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

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

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

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

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

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

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

result, _, err := client.RAILAPI.RailReadinessRetrieve(ctx).Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/readiness", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "readiness_report",
  "module": "anchor",
  "mode": "integrated",
  "ready": true,
  "rows": [
    {
      "fact": "anchored_subject",
      "owner": "anchor",
      "required": "attested",
      "held": "attested",
      "satisfied": true,
      "contract_row": "6.1.13",
      "source": "integrated",
      "missing_because": "not_supplied"
    }
  ],
  "always_enforced": [
    "example"
  ],
  "clause_library_in_force": "example",
  "classification_pack_in_force": "example",
  "parties_without_identity": 1
}
GET/v1/rail/engagements/{engagement_id}#

Retrieve an engagement

rail.engagements.retrieve

The engagement in its current state.

Pass as_of to get what was in force on a given date.

Path parameters

engagement_id string required

The 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 optional

Answer as at this date rather than now, as a calendar date in YYYY-MM-DD: you get the engagement as it stood on that date. Leave it out to get it as it stands now.

Returns

The engagement.

id string required

The 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" required

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

livemode boolean required

Which realm this record is in: false is the sandbox, true is live. Read it before you act on anything.

mocked boolean required

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

Transitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.

classifiedpaperedexecutedregisteredcurrentlapseddisputedterminated
engaging_party_ref string required

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

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 engaging party.

classification one of optional

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

Classificationor
classification_review_required boolean optional

True when confidence was low or the result was contested. A person decides.

instrument_in_force_id string · nullable optional

The 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 optional

The 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 optional

The 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 optional

The date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.

as_of string · date · nullable optional

Present when this was answered as at a past date rather than now.

created_at string · date-time optional

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: as_of (optional)
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z?as_of=Tue%20Sep%2001%202026%2001%3A00%3A00%20GMT%2B0100%20(West%20Africa%20Time)" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

// query parameters: as_of (optional)
const result = await api.railEngagementsRetrieve({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', asOf: 'Tue Sep 01 2026 01:00:00 GMT+0100 (West Africa Time)' });
// query parameters: as_of (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z?as_of=Tue%20Sep%2001%202026%2001%3A00%3A00%20GMT%2B0100%20(West%20Africa%20Time)', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

# query parameters: as_of (optional)
result = api.rail_engagements_retrieve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', as_of='Tue Sep 01 2026 01:00:00 GMT+0100 (West Africa Time)')
import os

import requests

# query parameters: as_of (optional)
response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z?as_of=Tue%20Sep%2001%202026%2001%3A00%3A00%20GMT%2B0100%20(West%20Africa%20Time)',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
    },
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

# query parameters: as_of (optional)
$result = $api->railEngagementsRetrieve(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', as_of: 'Tue Sep 01 2026 01:00:00 GMT+0100 (West Africa Time)');
<?php
// query parameters: as_of (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z?as_of=Tue%20Sep%2001%202026%2001%3A00%3A00%20GMT%2B0100%20(West%20Africa%20Time)');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

// query parameters: as_of (optional)
var result = api.railEngagementsRetrieve("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "Tue Sep 01 2026 01:00:00 GMT+0100 (West Africa Time)");
// query parameters: as_of (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z?as_of=Tue%20Sep%2001%202026%2001%3A00%3A00%20GMT%2B0100%20(West%20Africa%20Time)"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

// query parameters: as_of (optional)
var result = api.RailEngagementsRetrieve(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", asOf: "Tue Sep 01 2026 01:00:00 GMT+0100 (West Africa Time)");
// query parameters: as_of (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z?as_of=Tue%20Sep%2001%202026%2001%3A00%3A00%20GMT%2B0100%20(West%20Africa%20Time)");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: as_of (optional)
result, _, err := client.RAILAPI.RailEngagementsRetrieve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").AsOf("Tue Sep 01 2026 01:00:00 GMT+0100 (West Africa Time)").Execute()
// query parameters: as_of (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z?as_of=Tue%20Sep%2001%202026%2001%3A00%3A00%20GMT%2B0100%20(West%20Africa%20Time)", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "engagement",
  "livemode": true,
  "mocked": true,
  "status": "classified",
  "engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "classification": "employment",
  "classification_review_required": true,
  "instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "starts_on": "2026-09-01",
  "ends_on": "2026-09-01",
  "as_of": "2026-09-01",
  "created_at": "2026-09-01T09:00:00Z"
}
POST/v1/rail/engagements/{engagement_id}/classify#

Classify the engagement

rail.engagements.classify

Gives you a classification from the versioned pack. No model decides it and you can't assert one yourself.

A low confidence or contested result is held for review and the engagement waits. You can't draft on it until the review is done.

Classification on its own confers no rights. An unexecuted instrument binds no one.

Path parameters

engagement_id string required

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

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

Returns

The engagement, classified or awaiting review.

id string required

The 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" required

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

livemode boolean required

Which realm this record is in: false is the sandbox, true is live. Read it before you act on anything.

mocked boolean required

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

Transitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.

classifiedpaperedexecutedregisteredcurrentlapseddisputedterminated
engaging_party_ref string required

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

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 engaging party.

classification one of optional

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

Classificationor
classification_review_required boolean optional

True when confidence was low or the result was contested. A person decides.

instrument_in_force_id string · nullable optional

The 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 optional

The 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 optional

The 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 optional

The date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.

as_of string · date · nullable optional

Present when this was answered as at a past date rather than now.

created_at string · date-time optional

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

Other responses

401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.
409The record is not in a state that allows this.
422A required fact is missing. Call the readiness endpoint to see what.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classify" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railEngagementsClassify({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classify', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_engagements_classify(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

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

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

$result = $api->railEngagementsClassify(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classify');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

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

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

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

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

result, _, err := client.RAILAPI.RailEngagementsClassify(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classify", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "engagement",
  "livemode": true,
  "mocked": true,
  "status": "classified",
  "engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "classification": "employment",
  "classification_review_required": true,
  "instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "starts_on": "2026-09-01",
  "ends_on": "2026-09-01",
  "as_of": "2026-09-01",
  "created_at": "2026-09-01T09:00:00Z"
}
GET/v1/rail/engagements/{engagement_id}/classification#

Retrieve the classification

rail.classifications.retrieve

The classification, the confidence, and the pack that decided it.

Path parameters

engagement_id string required

The engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.

Returns

The classification.

object always "classification" required

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

engagement_id string required

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

Decided by the versioned pack. No model decides it and you can't assert one yourself.

employmentfixed_term_employmentindependent_contractingapprenticeshipcasualtask_based
confidence string required

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

highmediumlowcontested
rule_pack object required

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

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

The 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 optional

The 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" required

Always 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 optional

The 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 optional

When the pack decided the classification, as an RFC 3339 timestamp in UTC.

Other responses

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

Errors it can return

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

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

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

import droomwork

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

result = api.rail_classifications_retrieve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

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

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

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

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

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

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

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

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

result, _, err := client.RAILAPI.RailClassificationsRetrieve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "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"
}
GET/v1/rail/engagements/{engagement_id}/classification/rationale#

Retrieve the full classification rationale

rail.classifications.retrieve_rationale

Every indicator considered, whether it fired, its weight, the pack version and the facts you attested to.

This is what an inspector asks for and what an appeal argues with.

Path parameters

engagement_id string required

The engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.

Returns

The rationale.

object always "classification_rationale" required

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

engagement_id string required

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

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

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

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

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

Decided by the versioned pack. No model decides it and you can't assert one yourself.

employmentfixed_term_employmentindependent_contractingapprenticeshipcasualtask_based
source_fact string optional

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

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

The 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 optional

true 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 optional

true when the engaging party sets the hours or days the work is done, rather than the worker choosing them.

provides_equipment boolean optional

true 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 optional

true when the worker works for this engaging party alone and takes no engagements from anyone else.

can_send_a_substitute boolean optional

true 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 optional

true 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 optional

true 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 optional

How long the engagement is expected to run, in whole months, 0 or more.

occupation_code string optional

The 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 optional

The 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 optional

When the facts were attested to, as an RFC 3339 timestamp in UTC.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification/rationale" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railClassificationsRetrieveRationale({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification/rationale', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_classifications_retrieve_rationale(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

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

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

$result = $api->railClassificationsRetrieveRationale(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification/rationale');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

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

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

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

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

result, _, err := client.RAILAPI.RailClassificationsRetrieveRationale(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/classification/rationale", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "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"
}
POST/v1/rail/engagements/{engagement_id}/terminate#

Terminate an engagement

rail.engagements.terminate

Refused while the engagement is disputed. A dispute suspends termination until it resolves, so a disagreement can't be ended by ending the relationship.

Path parameters

engagement_id string required

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

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

Body

reason string required

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

The date the engagement ends, as YYYY-MM-DD. Name the day the relationship ends, not the day you make the call.

Returns

The terminated engagement.

id string required

The 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" required

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

livemode boolean required

Which realm this record is in: false is the sandbox, true is live. Read it before you act on anything.

mocked boolean required

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

Transitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.

classifiedpaperedexecutedregisteredcurrentlapseddisputedterminated
engaging_party_ref string required

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

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 engaging party.

classification one of optional

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

Classificationor
classification_review_required boolean optional

True when confidence was low or the result was contested. A person decides.

instrument_in_force_id string · nullable optional

The 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 optional

The 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 optional

The 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 optional

The date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.

as_of string · date · nullable optional

Present when this was answered as at a past date rather than now.

created_at string · date-time optional

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/terminate" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"reason":"The requester confirmed the work in person.","effective_on":"2026-09-01"}'
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railEngagementsTerminate({
  engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
  idempotencyKey: crypto.randomUUID(),
  railTerminateRequest: {"reason":"The requester confirmed the work in person.","effectiveOn":"2026-09-01"},
});
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/terminate', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    "reason": "The requester confirmed the work in person.",
    "effective_on": "2026-09-01"
  }),
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_engagements_terminate(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"reason": "The requester confirmed the work in person.", "effective_on": "2026-09-01"})
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/terminate',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={"reason": "The requester confirmed the work in person.", "effective_on": "2026-09-01"},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->railEngagementsTerminate($idempotencyKey, json_decode('{"reason":"The requester confirmed the work in person.","effective_on":"2026-09-01"}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/terminate');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Content-Type: application/json',
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
  CURLOPT_POSTFIELDS => '{"reason":"The requester confirmed the work in person.","effective_on":"2026-09-01"}',
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

var result = api.railEngagementsTerminate("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/terminate"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", UUID.randomUUID().toString())
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "reason": "The requester confirmed the work in person.",
          "effective_on": "2026-09-01"
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

var result = api.RailEngagementsTerminate(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/terminate");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
    {
      "reason": "The requester confirmed the work in person.",
      "effective_on": "2026-09-01"
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.RAILAPI.RailEngagementsTerminate(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).RailTerminateRequest(body).Execute()
body := strings.NewReader(`{
  "reason": "The requester confirmed the work in person.",
  "effective_on": "2026-09-01"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/terminate", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "engagement",
  "livemode": true,
  "mocked": true,
  "status": "classified",
  "engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "classification": "employment",
  "classification_review_required": true,
  "instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "starts_on": "2026-09-01",
  "ends_on": "2026-09-01",
  "as_of": "2026-09-01",
  "created_at": "2026-09-01T09:00:00Z"
}
POST/v1/rail/engagements/{engagement_id}/attestations#

Attest to the facts relied upon

rail.attestations.create

State the facts the classification will read. The attesting individual, the timestamp and the version of the attestation wording are recorded permanently.

If no other Droomwork module supplies the facts, this is where you send them. The wording is versioned and effective dated, so the record pins the version you attested under.

Path parameters

engagement_id string required

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

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

Body

facts AttestedFacts required

The 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 optional

true 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 optional

true when the engaging party sets the hours or days the work is done, rather than the worker choosing them.

provides_equipment boolean optional

true 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 optional

true when the worker works for this engaging party alone and takes no engagements from anyone else.

can_send_a_substitute boolean optional

true 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 optional

true 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 optional

true 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 optional

How long the engagement is expected to run, in whole months, 0 or more.

occupation_code string optional

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

The named person attesting to these facts, not a system account. Recorded permanently with the facts and the timestamp.

wording_version string required

The version of the attestation wording the person accepted, such as 2026.08.1. The record pins this version, so what they accepted is answered from the record, never from the current page.

Returns

The attestation.

id string required

The 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" required

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

engagement_id string required

The 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 optional

The 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 optional

true 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 optional

true when the engaging party sets the hours or days the work is done, rather than the worker choosing them.

provides_equipment boolean optional

true 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 optional

true when the worker works for this engaging party alone and takes no engagements from anyone else.

can_send_a_substitute boolean optional

true 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 optional

true 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 optional

true 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 optional

How long the engagement is expected to run, in whole months, 0 or more.

occupation_code string optional

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

The named person who attested to these facts. Recorded permanently; a person, not a system account.

wording_version string required

Versioned and effective dated. What someone accepted is answered from the record rather than from the current page.

attested_at string · date-time required

When the facts were attested, as an RFC 3339 timestamp in UTC. Recorded permanently beside the individual and the wording version.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"facts":{"controls_how_work_is_done":true,"controls_when_work_is_done":true,"provides_equipment":true,"exclusive_to_this_party":true,"can_send_a_substitute":true,"bears_financial_risk":true,"integrated_into_organisation":true,"duration_months":0,"occupation_code":"ng-7412"},"attesting_individual":"example","wording_version":"2026.08.1"}'
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railAttestationsCreate({
  engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
  idempotencyKey: crypto.randomUUID(),
  railAttestationCreateRequest: {"facts":{"controlsHowWorkIsDone":true,"controlsWhenWorkIsDone":true,"providesEquipment":true,"exclusiveToThisParty":true,"canSendASubstitute":true,"bearsFinancialRisk":true,"integratedIntoOrganisation":true,"durationMonths":0,"occupationCode":"ng-7412"},"attestingIndividual":"example","wordingVersion":"2026.08.1"},
});
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    "facts": {
      "controls_how_work_is_done": true,
      "controls_when_work_is_done": true,
      "provides_equipment": true,
      "exclusive_to_this_party": true,
      "can_send_a_substitute": true,
      "bears_financial_risk": true,
      "integrated_into_organisation": true,
      "duration_months": 0,
      "occupation_code": "ng-7412"
    },
    "attesting_individual": "example",
    "wording_version": "2026.08.1"
  }),
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_attestations_create(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"facts": {"controls_how_work_is_done": True, "controls_when_work_is_done": True, "provides_equipment": True, "exclusive_to_this_party": True, "can_send_a_substitute": True, "bears_financial_risk": True, "integrated_into_organisation": True, "duration_months": 0, "occupation_code": "ng-7412"}, "attesting_individual": "example", "wording_version": "2026.08.1"})
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={"facts": {"controls_how_work_is_done": True, "controls_when_work_is_done": True, "provides_equipment": True, "exclusive_to_this_party": True, "can_send_a_substitute": True, "bears_financial_risk": True, "integrated_into_organisation": True, "duration_months": 0, "occupation_code": "ng-7412"}, "attesting_individual": "example", "wording_version": "2026.08.1"},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->railAttestationsCreate($idempotencyKey, json_decode('{"facts":{"controls_how_work_is_done":true,"controls_when_work_is_done":true,"provides_equipment":true,"exclusive_to_this_party":true,"can_send_a_substitute":true,"bears_financial_risk":true,"integrated_into_organisation":true,"duration_months":0,"occupation_code":"ng-7412"},"attesting_individual":"example","wording_version":"2026.08.1"}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Content-Type: application/json',
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
  CURLOPT_POSTFIELDS => '{"facts":{"controls_how_work_is_done":true,"controls_when_work_is_done":true,"provides_equipment":true,"exclusive_to_this_party":true,"can_send_a_substitute":true,"bears_financial_risk":true,"integrated_into_organisation":true,"duration_months":0,"occupation_code":"ng-7412"},"attesting_individual":"example","wording_version":"2026.08.1"}',
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

var result = api.railAttestationsCreate("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", UUID.randomUUID().toString())
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "facts": {
            "controls_how_work_is_done": true,
            "controls_when_work_is_done": true,
            "provides_equipment": true,
            "exclusive_to_this_party": true,
            "can_send_a_substitute": true,
            "bears_financial_risk": true,
            "integrated_into_organisation": true,
            "duration_months": 0,
            "occupation_code": "ng-7412"
          },
          "attesting_individual": "example",
          "wording_version": "2026.08.1"
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

var result = api.RailAttestationsCreate(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
    {
      "facts": {
        "controls_how_work_is_done": true,
        "controls_when_work_is_done": true,
        "provides_equipment": true,
        "exclusive_to_this_party": true,
        "can_send_a_substitute": true,
        "bears_financial_risk": true,
        "integrated_into_organisation": true,
        "duration_months": 0,
        "occupation_code": "ng-7412"
      },
      "attesting_individual": "example",
      "wording_version": "2026.08.1"
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.RAILAPI.RailAttestationsCreate(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).RailAttestationCreateRequest(body).Execute()
body := strings.NewReader(`{
  "facts": {
    "controls_how_work_is_done": true,
    "controls_when_work_is_done": true,
    "provides_equipment": true,
    "exclusive_to_this_party": true,
    "can_send_a_substitute": true,
    "bears_financial_risk": true,
    "integrated_into_organisation": true,
    "duration_months": 0,
    "occupation_code": "ng-7412"
  },
  "attesting_individual": "example",
  "wording_version": "2026.08.1"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "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"
  }
}
GET/v1/rail/engagements/{engagement_id}/attestations/{attestation_id}#

Retrieve an attestation

rail.attestations.retrieve

What was attested, by whom, when, and under which wording version.

Path parameters

engagement_id string required

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

The attestation's identifier: the id returned when you attested at POST /v1/rail/engagements/{engagement_id}/attestations.

Returns

The attestation.

id string required

The 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" required

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

engagement_id string required

The 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 optional

The 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 optional

true 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 optional

true when the engaging party sets the hours or days the work is done, rather than the worker choosing them.

provides_equipment boolean optional

true 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 optional

true when the worker works for this engaging party alone and takes no engagements from anyone else.

can_send_a_substitute boolean optional

true 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 optional

true 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 optional

true 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 optional

How long the engagement is expected to run, in whole months, 0 or more.

occupation_code string optional

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

The named person who attested to these facts. Recorded permanently; a person, not a system account.

wording_version string required

Versioned and effective dated. What someone accepted is answered from the record rather than from the current page.

attested_at string · date-time required

When the facts were attested, as an RFC 3339 timestamp in UTC. Recorded permanently beside the individual and the wording version.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations/%7Battestation_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railAttestationsRetrieve({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', attestationId: '{attestation_id}' });
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations/%7Battestation_id%7D', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_attestations_retrieve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', attestation_id='{attestation_id}')
import os

import requests

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

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

$result = $api->railAttestationsRetrieve(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', attestation_id: '{attestation_id}');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations/%7Battestation_id%7D');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

var result = api.railAttestationsRetrieve("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{attestation_id}");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations/%7Battestation_id%7D"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

var result = api.RailAttestationsRetrieve(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", attestationId: "{attestation_id}");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations/%7Battestation_id%7D");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.RAILAPI.RailAttestationsRetrieve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{attestation_id}").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/attestations/%7Battestation_id%7D", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "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"
  }
}
POST/v1/rail/engagements/{engagement_id}/instruments#

Draft an instrument from the clause library

rail.instruments.create

Assembles a document from approved clauses. You choose a template, not wording; there is no field here that accepts prose.

A contractor agreement over facts classified as employment is refused. So is drafting before classification has completed.

The clause library version, the rule pack version and the legal approval reference are recorded on what you get back.

Path parameters

engagement_id string required

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

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

Body

template_code string required

The 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 optional

Defaults to the version currently in force.

variables object optional

Values 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 optional

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

The 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" required

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

livemode boolean required

Which realm this record is in: false is the sandbox, true is live. Read it before you act on anything.

mocked boolean required

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

The 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 required
draftedawaiting_assentexecutedexpiredsuperseded
template_code string required

A template from the approved set. Not wording.

clauses array of object optional

The clauses used, by reference. Every one comes from the approved library, as written there.

2 fields
clause_code string required

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

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

The 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 optional

The 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 optional

The 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 optional

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

Sealor
supersedes string · nullable optional

The 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 optional

The 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 optional

An unexecuted instrument lapses after this, and the lapse is recorded.

executed_at string · date-time · nullable optional

When 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

400The request could not be read, or a value was refused.
401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.
409The record is not in a state that allows this.
422A required fact is missing. Call the readiness endpoint to see what.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"template_code":"no_payee_destination","clause_library_version":"2026.08.1","variables":{},"expires_in_days":1}'
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railInstrumentsCreate({
  engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
  idempotencyKey: crypto.randomUUID(),
  railInstrumentCreateRequest: {"templateCode":"no_payee_destination","clauseLibraryVersion":"2026.08.1","variables":{},"expiresInDays":1},
});
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    "template_code": "no_payee_destination",
    "clause_library_version": "2026.08.1",
    "variables": {},
    "expires_in_days": 1
  }),
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_instruments_create(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"template_code": "no_payee_destination", "clause_library_version": "2026.08.1", "variables": {}, "expires_in_days": 1})
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={"template_code": "no_payee_destination", "clause_library_version": "2026.08.1", "variables": {}, "expires_in_days": 1},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->railInstrumentsCreate($idempotencyKey, json_decode('{"template_code":"no_payee_destination","clause_library_version":"2026.08.1","variables":{},"expires_in_days":1}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Content-Type: application/json',
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
  CURLOPT_POSTFIELDS => '{"template_code":"no_payee_destination","clause_library_version":"2026.08.1","variables":{},"expires_in_days":1}',
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

var result = api.railInstrumentsCreate("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", UUID.randomUUID().toString())
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "template_code": "no_payee_destination",
          "clause_library_version": "2026.08.1",
          "variables": {},
          "expires_in_days": 1
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

var result = api.RailInstrumentsCreate(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
    {
      "template_code": "no_payee_destination",
      "clause_library_version": "2026.08.1",
      "variables": {},
      "expires_in_days": 1
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.RAILAPI.RailInstrumentsCreate(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).RailInstrumentCreateRequest(body).Execute()
body := strings.NewReader(`{
  "template_code": "no_payee_destination",
  "clause_library_version": "2026.08.1",
  "variables": {},
  "expires_in_days": 1
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "instrument",
  "livemode": true,
  "mocked": true,
  "engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "status": "drafted",
  "template_code": "no_payee_destination",
  "clause_library_version": "2026.08.1",
  "clauses": [
    {
      "clause_code": "no_payee_destination",
      "clause_version": "2026.08.1"
    }
  ],
  "rule_pack_version": "2026.08.1",
  "legal_approval_reference": "paye-2026-09-rivers",
  "seal": {
    "hash": "sha256:9f2c1e0043a1b8",
    "previous_hash": "sha256:9f2c1e0043a1b8",
    "sealed_at": "2026-09-01T09:00:00Z",
    "algorithm": "sha256"
  },
  "supersedes": "example",
  "superseded_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "expires_at": "2026-09-01T09:00:00Z",
  "executed_at": "2026-09-01T09:00:00Z"
}
GET/v1/rail/engagements/{engagement_id}/instruments/{instrument_id}#

Retrieve an instrument

rail.instruments.retrieve

The instrument, the clauses it was built from, and its seal once executed.

Path parameters

engagement_id string required

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

The instrument's identifier: the id returned when you drafted it at POST /v1/rail/engagements/{engagement_id}/instruments, or listed at GET /v1/rail/instruments. It starts with rail_instrument_.

Returns

The instrument.

id string required

The 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" required

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

livemode boolean required

Which realm this record is in: false is the sandbox, true is live. Read it before you act on anything.

mocked boolean required

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

The 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 required
draftedawaiting_assentexecutedexpiredsuperseded
template_code string required

A template from the approved set. Not wording.

clauses array of object optional

The clauses used, by reference. Every one comes from the approved library, as written there.

2 fields
clause_code string required

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

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

The 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 optional

The 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 optional

The 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 optional

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

Sealor
supersedes string · nullable optional

The 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 optional

The 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 optional

An unexecuted instrument lapses after this, and the lapse is recorded.

executed_at string · date-time · nullable optional

When 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

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railInstrumentsRetrieve({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrumentId: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_instruments_retrieve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id='rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

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

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

$result = $api->railInstrumentsRetrieve(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

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

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

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

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

result, _, err := client.RAILAPI.RailInstrumentsRetrieve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "instrument",
  "livemode": true,
  "mocked": true,
  "engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "status": "drafted",
  "template_code": "no_payee_destination",
  "clause_library_version": "2026.08.1",
  "clauses": [
    {
      "clause_code": "no_payee_destination",
      "clause_version": "2026.08.1"
    }
  ],
  "rule_pack_version": "2026.08.1",
  "legal_approval_reference": "paye-2026-09-rivers",
  "seal": {
    "hash": "sha256:9f2c1e0043a1b8",
    "previous_hash": "sha256:9f2c1e0043a1b8",
    "sealed_at": "2026-09-01T09:00:00Z",
    "algorithm": "sha256"
  },
  "supersedes": "example",
  "superseded_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "expires_at": "2026-09-01T09:00:00Z",
  "executed_at": "2026-09-01T09:00:00Z"
}
POST/v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/execute#

Execute an instrument once both parties have assented

rail.instruments.execute

Refused unless both parties hold a live, unrevoked Passport. That includes the engaging party, so check your own side first.

Execution seals the document with a tamper evident hash chained to earlier records. No manual legal step is needed to get here.

Path parameters

engagement_id string required

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

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

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

Returns

The executed and sealed instrument.

id string required

The 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" required

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

livemode boolean required

Which realm this record is in: false is the sandbox, true is live. Read it before you act on anything.

mocked boolean required

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

The 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 required
draftedawaiting_assentexecutedexpiredsuperseded
template_code string required

A template from the approved set. Not wording.

clauses array of object optional

The clauses used, by reference. Every one comes from the approved library, as written there.

2 fields
clause_code string required

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

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

The 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 optional

The 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 optional

The 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 optional

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

Sealor
supersedes string · nullable optional

The 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 optional

The 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 optional

An unexecuted instrument lapses after this, and the lapse is recorded.

executed_at string · date-time · nullable optional

When 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

401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.
409The record is not in a state that allows this.
422A required fact is missing. Call the readiness endpoint to see what.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railInstrumentsExecute({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrumentId: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_instruments_execute(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id='rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

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

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

$result = $api->railInstrumentsExecute(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

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

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

var result = api.RailInstrumentsExecute(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", instrumentId: "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.RAILAPI.RailInstrumentsExecute(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "instrument",
  "livemode": true,
  "mocked": true,
  "engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "status": "drafted",
  "template_code": "no_payee_destination",
  "clause_library_version": "2026.08.1",
  "clauses": [
    {
      "clause_code": "no_payee_destination",
      "clause_version": "2026.08.1"
    }
  ],
  "rule_pack_version": "2026.08.1",
  "legal_approval_reference": "paye-2026-09-rivers",
  "seal": {
    "hash": "sha256:9f2c1e0043a1b8",
    "previous_hash": "sha256:9f2c1e0043a1b8",
    "sealed_at": "2026-09-01T09:00:00Z",
    "algorithm": "sha256"
  },
  "supersedes": "example",
  "superseded_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "expires_at": "2026-09-01T09:00:00Z",
  "executed_at": "2026-09-01T09:00:00Z"
}
POST/v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/supersede#

Amend by drafting a variation

rail.instruments.supersede

Drafts a new instrument from the one in force and points the chain forward. The original is never edited and never destroyed.

A reclassification takes this path too. History is never restated.

Path parameters

engagement_id string required

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

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

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

Body

reason string required

Why you're varying the instrument in force, in your own words. Required on every variation, a reclassification included.

template_code string optional

Defaults to the template of the instrument being superseded.

variables object optional

Values the variation's template needs, such as dates and party names, each as a string. Values only: a variable cannot introduce a clause and cannot change one.

Returns

The variation, drafted and awaiting assent.

id string required

The 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" required

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

livemode boolean required

Which realm this record is in: false is the sandbox, true is live. Read it before you act on anything.

mocked boolean required

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

The 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 required
draftedawaiting_assentexecutedexpiredsuperseded
template_code string required

A template from the approved set. Not wording.

clauses array of object optional

The clauses used, by reference. Every one comes from the approved library, as written there.

2 fields
clause_code string required

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

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

The 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 optional

The 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 optional

The 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 optional

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

Sealor
supersedes string · nullable optional

The 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 optional

The 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 optional

An unexecuted instrument lapses after this, and the lapse is recorded.

executed_at string · date-time · nullable optional

When 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

400The request could not be read, or a value was refused.
401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.
409The record is not in a state that allows this.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/supersede" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"reason":"The requester confirmed the work in person.","template_code":"no_payee_destination","variables":{}}'
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railInstrumentsSupersede({
  engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
  instrumentId: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
  idempotencyKey: crypto.randomUUID(),
  railSupersedeRequest: {"reason":"The requester confirmed the work in person.","templateCode":"no_payee_destination","variables":{}},
});
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/supersede', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    "reason": "The requester confirmed the work in person.",
    "template_code": "no_payee_destination",
    "variables": {}
  }),
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_instruments_supersede(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id='rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"reason": "The requester confirmed the work in person.", "template_code": "no_payee_destination", "variables": {}})
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/supersede',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={"reason": "The requester confirmed the work in person.", "template_code": "no_payee_destination", "variables": {}},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->railInstrumentsSupersede($idempotencyKey, json_decode('{"reason":"The requester confirmed the work in person.","template_code":"no_payee_destination","variables":{}}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/supersede');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Content-Type: application/json',
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
  CURLOPT_POSTFIELDS => '{"reason":"The requester confirmed the work in person.","template_code":"no_payee_destination","variables":{}}',
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

var result = api.railInstrumentsSupersede("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/supersede"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", UUID.randomUUID().toString())
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "reason": "The requester confirmed the work in person.",
          "template_code": "no_payee_destination",
          "variables": {}
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

var result = api.RailInstrumentsSupersede(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", instrumentId: "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/supersede");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
    {
      "reason": "The requester confirmed the work in person.",
      "template_code": "no_payee_destination",
      "variables": {}
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.RAILAPI.RailInstrumentsSupersede(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).RailSupersedeRequest(body).Execute()
body := strings.NewReader(`{
  "reason": "The requester confirmed the work in person.",
  "template_code": "no_payee_destination",
  "variables": {}
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/supersede", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "instrument",
  "livemode": true,
  "mocked": true,
  "engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "status": "drafted",
  "template_code": "no_payee_destination",
  "clause_library_version": "2026.08.1",
  "clauses": [
    {
      "clause_code": "no_payee_destination",
      "clause_version": "2026.08.1"
    }
  ],
  "rule_pack_version": "2026.08.1",
  "legal_approval_reference": "paye-2026-09-rivers",
  "seal": {
    "hash": "sha256:9f2c1e0043a1b8",
    "previous_hash": "sha256:9f2c1e0043a1b8",
    "sealed_at": "2026-09-01T09:00:00Z",
    "algorithm": "sha256"
  },
  "supersedes": "example",
  "superseded_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "expires_at": "2026-09-01T09:00:00Z",
  "executed_at": "2026-09-01T09:00:00Z"
}
POST/v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/assents#

Record a party assenting to an instrument

rail.assents.create

Binds a signature to a live Passport and to the hash of the exact instrument version the party was shown.

Send the hash of what they saw. It's what makes the assent provably about this document rather than one with the same name.

Path parameters

engagement_id string required

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

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

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

Body

actor_ref string required

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

The 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 required
click_throughdrawn_signatureone_time_codein_person_witnessed
channel string required

Where 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_person
device string optional

The device the party gave assent on, as free text. Recorded as evidence beside the mode and channel.

instrument_hash string required

The hash of the exact instrument version the party was shown, prefixed with the algorithm, such as sha256:. Send the hash of what they saw; it's what makes the assent provably about this document.

Returns

The assent record.

id string required

The 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" required

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

instrument_id string required

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

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

The 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 optional

Which side of the engagement assented: engaging_party (the party engaging the worker) or worker. Both must assent before the instrument can execute.

engaging_partyworker
mode string required
click_throughdrawn_signatureone_time_codein_person_witnessed
channel string required

Where 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_person
device string optional

The device the assent was given on, recorded as evidence.

instrument_hash string required

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

When the assent was given, as an RFC 3339 timestamp in UTC. Part of the evidence, beside the mode, channel and device.

Other responses

400The request could not be read, or a value was refused.
401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.
422A required fact is missing. Call the readiness endpoint to see what.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"actor_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","identity_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","mode":"click_through","channel":"web","instrument_hash":"sha256:9f2c1e0043a1b8","device":"example"}'
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railAssentsCreate({
  engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
  instrumentId: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
  idempotencyKey: crypto.randomUUID(),
  railAssentCreateRequest: {"actorRef":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","identityRef":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","mode":"click_through","channel":"web","instrumentHash":"sha256:9f2c1e0043a1b8","device":"example"},
});
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    "actor_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
    "identity_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
    "mode": "click_through",
    "channel": "web",
    "instrument_hash": "sha256:9f2c1e0043a1b8",
    "device": "example"
  }),
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_assents_create(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id='rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"actor_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "identity_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "mode": "click_through", "channel": "web", "instrument_hash": "sha256:9f2c1e0043a1b8", "device": "example"})
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={"actor_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "identity_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "mode": "click_through", "channel": "web", "instrument_hash": "sha256:9f2c1e0043a1b8", "device": "example"},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->railAssentsCreate($idempotencyKey, json_decode('{"actor_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","identity_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","mode":"click_through","channel":"web","instrument_hash":"sha256:9f2c1e0043a1b8","device":"example"}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Content-Type: application/json',
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
  CURLOPT_POSTFIELDS => '{"actor_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","identity_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","mode":"click_through","channel":"web","instrument_hash":"sha256:9f2c1e0043a1b8","device":"example"}',
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

var result = api.railAssentsCreate("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", UUID.randomUUID().toString())
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "actor_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
          "identity_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
          "mode": "click_through",
          "channel": "web",
          "instrument_hash": "sha256:9f2c1e0043a1b8",
          "device": "example"
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

var result = api.RailAssentsCreate(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", instrumentId: "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
    {
      "actor_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "identity_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "mode": "click_through",
      "channel": "web",
      "instrument_hash": "sha256:9f2c1e0043a1b8",
      "device": "example"
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.RAILAPI.RailAssentsCreate(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).RailAssentCreateRequest(body).Execute()
body := strings.NewReader(`{
  "actor_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "identity_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "mode": "click_through",
  "channel": "web",
  "instrument_hash": "sha256:9f2c1e0043a1b8",
  "device": "example"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "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"
}
GET/v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/assents/{assent_id}#

Retrieve an assent record

rail.assents.retrieve

The actor, the identity reference, the mode, the timestamp, the channel, the device, and the hash of the exact version presented.

Path parameters

engagement_id string required

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

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

The assent record's identifier: the id returned when you recorded it at POST /v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/assents.

Returns

The assent record.

id string required

The 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" required

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

instrument_id string required

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

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

The 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 optional

Which side of the engagement assented: engaging_party (the party engaging the worker) or worker. Both must assent before the instrument can execute.

engaging_partyworker
mode string required
click_throughdrawn_signatureone_time_codein_person_witnessed
channel string required

Where 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_person
device string optional

The device the assent was given on, recorded as evidence.

instrument_hash string required

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

When the assent was given, as an RFC 3339 timestamp in UTC. Part of the evidence, beside the mode, channel and device.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents/%7Bassent_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railAssentsRetrieve({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrumentId: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', assentId: '{assent_id}' });
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents/%7Bassent_id%7D', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_assents_retrieve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id='rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', assent_id='{assent_id}')
import os

import requests

response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents/%7Bassent_id%7D',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
    },
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->railAssentsRetrieve(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', assent_id: '{assent_id}');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents/%7Bassent_id%7D');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

var result = api.railAssentsRetrieve("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{assent_id}");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents/%7Bassent_id%7D"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

var result = api.RailAssentsRetrieve(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", instrumentId: "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", assentId: "{assent_id}");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents/%7Bassent_id%7D");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.RAILAPI.RailAssentsRetrieve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{assent_id}").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/assents/%7Bassent_id%7D", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "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"
}
GET/v1/rail/engagements/{engagement_id}/instruments/{instrument_id}/copy#

Retrieve your executed copy

rail.executed_copies.retrieve

Each party gets a copy on the channel they used. The worker can retrieve theirs here without going through the engaging party.

Path parameters

engagement_id string required

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

The instrument's identifier: the id returned when you drafted it at POST /v1/rail/engagements/{engagement_id}/instruments, or listed at GET /v1/rail/instruments. It starts with rail_instrument_.

Returns

The executed copy.

object always "executed_copy" required

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

instrument_id string required

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

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

Tamper evident and chained to prior records.

4 fields of Seal
hash string required

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

The 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 optional

The hash algorithm the seal uses, such as sha256. The hash and previous_hash values are prefixed with it.

sealed_at string · date-time required

When 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 optional

The 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_person

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/copy" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railExecutedCopiesRetrieve({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrumentId: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/copy', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_executed_copies_retrieve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id='rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

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

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

$result = $api->railExecutedCopiesRetrieve(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', instrument_id: 'rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/copy');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

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

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

var result = api.RailExecutedCopiesRetrieve(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", instrumentId: "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/copy");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.RAILAPI.RailExecutedCopiesRetrieve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruments/rail_instrument_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/copy", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "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"
}
GET/v1/rail/engagements/{engagement_id}/envelope#

Retrieve the obligation envelope

rail.envelopes.retrieve

What is owed, to which authority, on what basis, from what date and how often.

The basis, never the figure. Your payroll computes the amount from this and the statutory rules. An obligation that does not attach, because a threshold or exemption applies, is absent rather than present and zero.

Path parameters

engagement_id string required

The engagement's id, as returned by POST /v1/rail/engagements when you opened it or by GET /v1/rail/engagements. It starts with rail_engagement_.

Returns

The obligation envelope.

id string required

The 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" required

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

livemode boolean required

Which realm this record is in: false is the sandbox, true is live. Read it before you act on anything.

mocked boolean required

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

The 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 optional

Decided by the versioned pack. No model decides it and you can't assert one yourself.

employmentfixed_term_employmentindependent_contractingapprenticeshipcasualtask_based
obligations array of object required

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

The 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 optional

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

How an amount is worked out, not what it is. Your payroll computes the figure from it.

effective_from string · date required

The 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 optional

The last date the obligation applies, as a calendar date in YYYY-MM-DD. null while it has no end date.

frequency string required

How 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_engagement
threshold_note string optional

Why a threshold or exemption did or did not apply here.

rule_pack_version string optional

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

When this envelope was published for your payroll and remittance to read, as an RFC 3339 timestamp in UTC.

supersedes string · nullable optional

The 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

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

Errors it can return

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

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

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

import droomwork

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

result = api.rail_envelopes_retrieve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

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

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

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

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

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

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

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

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

result, _, err := client.RAILAPI.RailEnvelopesRetrieve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/envelope", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "rail_envelope_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "obligation_envelope",
  "livemode": true,
  "mocked": true,
  "engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "obligations": [
    {
      "authority": "Rivers State Internal Revenue Service",
      "basis": "paye_graduated_bands",
      "effective_from": "2026-09-01",
      "frequency": "monthly",
      "authority_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "effective_to": "2026-09-01",
      "threshold_note": "example"
    }
  ],
  "published_at": "2026-09-01T09:00:00Z",
  "classification": "employment",
  "rule_pack_version": "2026.08.1",
  "supersedes": "example"
}
GET/v1/rail/engagements/{engagement_id}/discharges#

List discharge confirmations against this engagement

rail.discharges.list

Payment and filing references that came back from payroll, remittance and disbursement, as evidence that each obligation was met.

RAIL records that an obligation was discharged. It never moves the money that discharged it.

Path parameters

engagement_id string required

The 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 optional

How many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.

Returns

A page of discharges.

object always "list" required

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

data array of Discharge required

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

8 fields of Discharge
id string required

The 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" required

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

engagement_id string required

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

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

The period the obligation was discharged for, as a calendar month in YYYY-MM, such as 2026-09.

reference string required

The receipt or filing reference from the module that discharged it.

discharged_by string required

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

runremitroute
confirmed_at string · date-time optional

When the obligation was confirmed as met, as an RFC 3339 timestamp in UTC.

has_more boolean required

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: limit (optional)
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/discharges?limit=25" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

// query parameters: limit (optional)
const result = await api.railDischargesList({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', limit: 25 });
// query parameters: limit (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/discharges?limit=25', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

# query parameters: limit (optional)
result = api.rail_discharges_list(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', limit=25)
import os

import requests

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

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

# query parameters: limit (optional)
$result = $api->railDischargesList(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', limit: 25);
<?php
// query parameters: limit (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/discharges?limit=25');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

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

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

// query parameters: limit (optional)
var result = api.RailDischargesList(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", limit: 25);
// query parameters: limit (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/discharges?limit=25");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: limit (optional)
result, _, err := client.RAILAPI.RailDischargesList(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Limit(25).Execute()
// query parameters: limit (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/discharges?limit=25", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "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
}
POST/v1/rail/engagements/{engagement_id}/disputes#

Raise a dispute

rail.disputes.create

Freezes the evidence bundle as it stands and suspends termination until the dispute resolves. Either party may raise one.

Path parameters

engagement_id string required

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

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

Body

raised_by string required

Which party is raising the dispute: engaging_party (the party engaging the worker) or worker. Either may raise one.

engaging_partyworker
grounds string required

Why the dispute is raised, in your own words, such as the classification not reflecting how the work is controlled. Recorded on the dispute.

Returns

The dispute, with the evidence frozen.

id string required

The 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" required

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

engagement_id string required

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

Which 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_partyworker
grounds string optional

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

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

openresolvedwithdrawn
evidence_frozen_at string · date-time required

The bundle is fixed from this moment and cannot change while the dispute runs.

termination_suspended boolean optional

true 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 optional

How 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 optional

When the outcome was recorded, as an RFC 3339 timestamp in UTC. null while the dispute is still open.

Other responses

400The request could not be read, or a value was refused.
401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.
409The record is not in a state that allows this.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"raised_by":"engaging_party","grounds":"The classification does not reflect how the work is actually controlled."}'
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railDisputesCreate({
  engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
  idempotencyKey: crypto.randomUUID(),
  railDisputeCreateRequest: {"raisedBy":"engaging_party","grounds":"The classification does not reflect how the work is actually controlled."},
});
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    "raised_by": "engaging_party",
    "grounds": "The classification does not reflect how the work is actually controlled."
  }),
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_disputes_create(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"raised_by": "engaging_party", "grounds": "The classification does not reflect how the work is actually controlled."})
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={"raised_by": "engaging_party", "grounds": "The classification does not reflect how the work is actually controlled."},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->railDisputesCreate($idempotencyKey, json_decode('{"raised_by":"engaging_party","grounds":"The classification does not reflect how the work is actually controlled."}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Content-Type: application/json',
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
  CURLOPT_POSTFIELDS => '{"raised_by":"engaging_party","grounds":"The classification does not reflect how the work is actually controlled."}',
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

var result = api.railDisputesCreate("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", UUID.randomUUID().toString())
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "raised_by": "engaging_party",
          "grounds": "The classification does not reflect how the work is actually controlled."
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

var result = api.RailDisputesCreate(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
    {
      "raised_by": "engaging_party",
      "grounds": "The classification does not reflect how the work is actually controlled."
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.RAILAPI.RailDisputesCreate(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).RailDisputeCreateRequest(body).Execute()
body := strings.NewReader(`{
  "raised_by": "engaging_party",
  "grounds": "The classification does not reflect how the work is actually controlled."
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "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"
}
POST/v1/rail/engagements/{engagement_id}/disputes/{dispute_id}/resolve#

Resolve a dispute

rail.disputes.resolve

Records the outcome and lifts the suspension on termination.

Path parameters

engagement_id string required

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

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

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

Body

outcome string required

How 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 optional

Anything worth recording beside the outcome, such as who agreed it and where. Optional, and it isn't shown on the dispute you get back.

Returns

The resolved dispute.

id string required

The 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" required

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

engagement_id string required

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

Which 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_partyworker
grounds string optional

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

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

openresolvedwithdrawn
evidence_frozen_at string · date-time required

The bundle is fixed from this moment and cannot change while the dispute runs.

termination_suspended boolean optional

true 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 optional

How 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 optional

When the outcome was recorded, as an RFC 3339 timestamp in UTC. null while the dispute is still open.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes/%7Bdispute_id%7D/resolve" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"outcome":"example","note":"example"}'
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railDisputesResolve({
  engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
  disputeId: '{dispute_id}',
  idempotencyKey: crypto.randomUUID(),
  railDisputeResolveRequest: {"outcome":"example","note":"example"},
});
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes/%7Bdispute_id%7D/resolve', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    "outcome": "example",
    "note": "example"
  }),
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_disputes_resolve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', dispute_id='{dispute_id}', body={"outcome": "example", "note": "example"})
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes/%7Bdispute_id%7D/resolve',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={"outcome": "example", "note": "example"},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->railDisputesResolve($idempotencyKey, json_decode('{"outcome":"example","note":"example"}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes/%7Bdispute_id%7D/resolve');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Content-Type: application/json',
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
  CURLOPT_POSTFIELDS => '{"outcome":"example","note":"example"}',
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

var result = api.railDisputesResolve("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{dispute_id}", idempotencyKey, body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes/%7Bdispute_id%7D/resolve"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", UUID.randomUUID().toString())
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "outcome": "example",
          "note": "example"
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

var result = api.RailDisputesResolve(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", disputeId: "{dispute_id}", idempotencyKey, body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes/%7Bdispute_id%7D/resolve");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
    {
      "outcome": "example",
      "note": "example"
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.RAILAPI.RailDisputesResolve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{dispute_id}").IdempotencyKey(key).RailDisputeResolveRequest(body).Execute()
body := strings.NewReader(`{
  "outcome": "example",
  "note": "example"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/disputes/%7Bdispute_id%7D/resolve", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "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"
}
POST/v1/rail/engagements/{engagement_id}/audit_bundles#

Export an audit bundle for an engagement

rail.audit_bundles.create

Everything on the record for this engagement, with integrity proofs a recipient can verify without trusting us.

The record is write once and append only. There is no endpoint that edits or removes anything in it.

Path parameters

engagement_id string required

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

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

Returns

The audit bundle.

id string required

The 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" required

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

engagement_id string required

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

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

generatingreadyfailed
download_url string · uri · nullable optional

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

You can verify it without trusting us.

3 fields
root_hash string optional

The 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 optional

The hash algorithm behind root_hash, such as sha256. Use the same one when you check the bundle yourself.

verification_instructions_url string · uri optional

A 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 optional

Which 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 optional

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railAuditBundlesCreate({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_audit_bundles_create(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

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

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

$result = $api->railAuditBundlesCreate(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

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

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

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

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

result, _, err := client.RAILAPI.RailAuditBundlesCreate(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "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"
}
GET/v1/rail/engagements/{engagement_id}/audit_bundles/{audit_bundle_id}#

Retrieve an audit bundle

rail.audit_bundles.retrieve

The bundle, with instructions for verifying its integrity proofs.

Path parameters

engagement_id string required

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

The id of the bundle you want, from the response to POST /v1/rail/engagements/{engagement_id}/audit_bundles (rail.audit_bundles.create) when you exported it. No endpoint lists bundles, so keep it from that response.

Returns

The audit bundle.

id string required

The 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" required

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

engagement_id string required

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

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

generatingreadyfailed
download_url string · uri · nullable optional

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

You can verify it without trusting us.

3 fields
root_hash string optional

The 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 optional

The hash algorithm behind root_hash, such as sha256. Use the same one when you check the bundle yourself.

verification_instructions_url string · uri optional

A 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 optional

Which 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 optional

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles/%7Baudit_bundle_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railAuditBundlesRetrieve({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', auditBundleId: '{audit_bundle_id}' });
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles/%7Baudit_bundle_id%7D', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_audit_bundles_retrieve(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', audit_bundle_id='{audit_bundle_id}')
import os

import requests

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

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

$result = $api->railAuditBundlesRetrieve(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', audit_bundle_id: '{audit_bundle_id}');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles/%7Baudit_bundle_id%7D');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

var result = api.railAuditBundlesRetrieve("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{audit_bundle_id}");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles/%7Baudit_bundle_id%7D"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

var result = api.RailAuditBundlesRetrieve(engagementId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", auditBundleId: "{audit_bundle_id}");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles/%7Baudit_bundle_id%7D");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.RAILAPI.RailAuditBundlesRetrieve(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{audit_bundle_id}").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/audit_bundles/%7Baudit_bundle_id%7D", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "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"
}
POST/v1/rail/engagements/{engagement_id}/paper#

Paper an engagement

rail.engagements.paper

Builds the instrument from the clause library for the classification the engagement carries. Refused while a classification review is outstanding: you can't paper a classification that isn't settled.

Path parameters

engagement_id string required

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

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

Returns

The engagement in its new state.

id string required

The 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" required

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

livemode boolean required

Which realm this record is in: false is the sandbox, true is live. Read it before you act on anything.

mocked boolean required

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

Transitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.

classifiedpaperedexecutedregisteredcurrentlapseddisputedterminated
engaging_party_ref string required

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

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 engaging party.

classification one of optional

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

Classificationor
classification_review_required boolean optional

True when confidence was low or the result was contested. A person decides.

instrument_in_force_id string · nullable optional

The 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 optional

The 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 optional

The 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 optional

The date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.

as_of string · date · nullable optional

Present when this was answered as at a past date rather than now.

created_at string · date-time optional

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

Other responses

400The request could not be read, or a value was refused.
401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.
409The record is not in a state that allows this.
422A required fact is missing. Call the readiness endpoint to see what.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/paper" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railEngagementsPaper({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/paper', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_engagements_paper(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

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

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

$result = $api->railEngagementsPaper(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/paper');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

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

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

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

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

result, _, err := client.RAILAPI.RailEngagementsPaper(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/paper", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "engagement",
  "livemode": true,
  "mocked": true,
  "status": "classified",
  "engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "classification": "employment",
  "classification_review_required": true,
  "instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "starts_on": "2026-09-01",
  "ends_on": "2026-09-01",
  "as_of": "2026-09-01",
  "created_at": "2026-09-01T09:00:00Z"
}
POST/v1/rail/engagements/{engagement_id}/execute#

Execute an engagement

rail.engagements.execute

Both sides have assented and the instrument is sealed. This is the point the agreement binds, and the earliest point at which anybody may be paid under it.

Path parameters

engagement_id string required

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

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

Returns

The engagement in its new state.

id string required

The 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" required

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

livemode boolean required

Which realm this record is in: false is the sandbox, true is live. Read it before you act on anything.

mocked boolean required

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

Transitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.

classifiedpaperedexecutedregisteredcurrentlapseddisputedterminated
engaging_party_ref string required

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

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 engaging party.

classification one of optional

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

Classificationor
classification_review_required boolean optional

True when confidence was low or the result was contested. A person decides.

instrument_in_force_id string · nullable optional

The 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 optional

The 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 optional

The 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 optional

The date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.

as_of string · date · nullable optional

Present when this was answered as at a past date rather than now.

created_at string · date-time optional

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

Other responses

400The request could not be read, or a value was refused.
401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.
409The record is not in a state that allows this.
422A required fact is missing. Call the readiness endpoint to see what.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railEngagementsExecute({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_engagements_execute(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

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

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

$result = $api->railEngagementsExecute(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

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

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

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

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

result, _, err := client.RAILAPI.RailEngagementsExecute(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "engagement",
  "livemode": true,
  "mocked": true,
  "status": "classified",
  "engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "classification": "employment",
  "classification_review_required": true,
  "instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "starts_on": "2026-09-01",
  "ends_on": "2026-09-01",
  "as_of": "2026-09-01",
  "created_at": "2026-09-01T09:00:00Z"
}
POST/v1/rail/engagements/{engagement_id}/register#

Register an engagement

rail.engagements.register

Files the engagement where the jurisdiction requires it to be registered. A separate step from execution: the two happen at different times, and one can fail without the other.

Path parameters

engagement_id string required

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

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

Returns

The engagement in its new state.

id string required

The 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" required

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

livemode boolean required

Which realm this record is in: false is the sandbox, true is live. Read it before you act on anything.

mocked boolean required

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

Transitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.

classifiedpaperedexecutedregisteredcurrentlapseddisputedterminated
engaging_party_ref string required

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

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 engaging party.

classification one of optional

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

Classificationor
classification_review_required boolean optional

True when confidence was low or the result was contested. A person decides.

instrument_in_force_id string · nullable optional

The 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 optional

The 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 optional

The 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 optional

The date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.

as_of string · date · nullable optional

Present when this was answered as at a past date rather than now.

created_at string · date-time optional

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

Other responses

400The request could not be read, or a value was refused.
401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.
409The record is not in a state that allows this.
422A required fact is missing. Call the readiness endpoint to see what.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/register" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railEngagementsRegister({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/register', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_engagements_register(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

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

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

$result = $api->railEngagementsRegister(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/register');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

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

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

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

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

result, _, err := client.RAILAPI.RailEngagementsRegister(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/register", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "engagement",
  "livemode": true,
  "mocked": true,
  "status": "classified",
  "engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "classification": "employment",
  "classification_review_required": true,
  "instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "starts_on": "2026-09-01",
  "ends_on": "2026-09-01",
  "as_of": "2026-09-01",
  "created_at": "2026-09-01T09:00:00Z"
}
POST/v1/rail/engagements/{engagement_id}/activate#

Activate an engagement

rail.engagements.activate

Marks the engagement current, so work can be allocated against it. MATCH and RUN both wait for this status.

Path parameters

engagement_id string required

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

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

Returns

The engagement in its new state.

id string required

The 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" required

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

livemode boolean required

Which realm this record is in: false is the sandbox, true is live. Read it before you act on anything.

mocked boolean required

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

Transitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.

classifiedpaperedexecutedregisteredcurrentlapseddisputedterminated
engaging_party_ref string required

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

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 engaging party.

classification one of optional

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

Classificationor
classification_review_required boolean optional

True when confidence was low or the result was contested. A person decides.

instrument_in_force_id string · nullable optional

The 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 optional

The 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 optional

The 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 optional

The date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.

as_of string · date · nullable optional

Present when this was answered as at a past date rather than now.

created_at string · date-time optional

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

Other responses

400The request could not be read, or a value was refused.
401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.
409The record is not in a state that allows this.
422A required fact is missing. Call the readiness endpoint to see what.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/activate" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railEngagementsActivate({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/activate', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_engagements_activate(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

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

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

$result = $api->railEngagementsActivate(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/activate');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

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

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

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

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

result, _, err := client.RAILAPI.RailEngagementsActivate(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/activate", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "engagement",
  "livemode": true,
  "mocked": true,
  "status": "classified",
  "engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "classification": "employment",
  "classification_review_required": true,
  "instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "starts_on": "2026-09-01",
  "ends_on": "2026-09-01",
  "as_of": "2026-09-01",
  "created_at": "2026-09-01T09:00:00Z"
}
POST/v1/rail/engagements/{engagement_id}/lapse#

Lapse an engagement

rail.engagements.lapse

The term ended and nothing renewed it. A lapsed engagement is not terminated and is not disputed: it stopped, and the record says so.

Path parameters

engagement_id string required

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

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

Returns

The engagement in its new state.

id string required

The 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" required

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

livemode boolean required

Which realm this record is in: false is the sandbox, true is live. Read it before you act on anything.

mocked boolean required

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

Transitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.

classifiedpaperedexecutedregisteredcurrentlapseddisputedterminated
engaging_party_ref string required

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

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 engaging party.

classification one of optional

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

Classificationor
classification_review_required boolean optional

True when confidence was low or the result was contested. A person decides.

instrument_in_force_id string · nullable optional

The 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 optional

The 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 optional

The 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 optional

The date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.

as_of string · date · nullable optional

Present when this was answered as at a past date rather than now.

created_at string · date-time optional

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

Other responses

400The request could not be read, or a value was refused.
401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.
409The record is not in a state that allows this.
422A required fact is missing. Call the readiness endpoint to see what.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/lapse" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railEngagementsLapse({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/lapse', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_engagements_lapse(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

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

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

$result = $api->railEngagementsLapse(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/lapse');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

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

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

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

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

result, _, err := client.RAILAPI.RailEngagementsLapse(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/lapse", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "engagement",
  "livemode": true,
  "mocked": true,
  "status": "classified",
  "engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "classification": "employment",
  "classification_review_required": true,
  "instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "starts_on": "2026-09-01",
  "ends_on": "2026-09-01",
  "as_of": "2026-09-01",
  "created_at": "2026-09-01T09:00:00Z"
}
POST/v1/rail/engagements/{engagement_id}/dispute#

Dispute an engagement

rail.engagements.dispute

One side says the agreement is not what was agreed. Work carries on or stops by the terms of the instrument. This records that the disagreement exists; it doesn't decide it.

Path parameters

engagement_id string required

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

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

Returns

The engagement in its new state.

id string required

The 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" required

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

livemode boolean required

Which realm this record is in: false is the sandbox, true is live. Read it before you act on anything.

mocked boolean required

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

Transitions are enforced, and every one is recorded. Classification alone confers no rights and an unexecuted instrument binds no one.

classifiedpaperedexecutedregisteredcurrentlapseddisputedterminated
engaging_party_ref string required

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

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 engaging party.

classification one of optional

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

Classificationor
classification_review_required boolean optional

True when confidence was low or the result was contested. A person decides.

instrument_in_force_id string · nullable optional

The 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 optional

The 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 optional

The 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 optional

The date the engagement ends, as a calendar date in YYYY-MM-DD. null when no end date was set.

as_of string · date · nullable optional

Present when this was answered as at a past date rather than now.

created_at string · date-time optional

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

Other responses

400The request could not be read, or a value was refused.
401No valid credential was presented.
403The credential does not carry the required scope.
404No record with that identifier.
409The record is not in a state that allows this.
422A required fact is missing. Call the readiness endpoint to see what.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/dispute" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railEngagementsDispute({ engagementId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/dispute', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_engagements_dispute(engagement_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

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

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

$result = $api->railEngagementsDispute(engagement_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/dispute');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

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

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

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

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

result, _, err := client.RAILAPI.RailEngagementsDispute(ctx, "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/rail/engagements/rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/dispute", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "engagement",
  "livemode": true,
  "mocked": true,
  "status": "classified",
  "engaging_party_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "worker_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "classification": "employment",
  "classification_review_required": true,
  "instrument_in_force_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "envelope_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "starts_on": "2026-09-01",
  "ends_on": "2026-09-01",
  "as_of": "2026-09-01",
  "created_at": "2026-09-01T09:00:00Z"
}
GET/v1/rail/events#

List events

rail.events.list

The append only record of everything RAIL is the authority for, oldest first. Every event here is one the webhook catalogue declares, so the code that handles a delivery handles a replay too.

Two ways to read it. Pass stream with after to replay one stream from the sequence you last handled; it's exact and needs no cursor, because the sequence counts per organisation and per stream. Without stream, you get events across streams in the order they were recorded, paged with starting_after.

Query parameters

stream string optional

The 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 optional

Replay 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 optional

How 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 optional

The id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.

Returns

A page of events.

object always "list" required

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

data array of StoredEvent required

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

13 fields of StoredEvent
id string required

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

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

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

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

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

When the event happened, as an RFC 3339 timestamp in UTC. Not when it was delivered: a redelivery carries the original value.

request_id string optional

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

Which realm the event happened in. False is the sandbox.

mocked boolean required

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

Which part of Droomwork is the authority for this fact: anchor (identity), proof (credentials), rail (engagements), flow (sourcing), match (allocation), run (payroll), remit (remittance), route (payouts), gateway (the API's front door), iam (accounts and API keys), ledger (the books), registry (rule packs), delivery (webhooks and messages), documents (rendered payslips and instruments) or intelligence (AI decisions). Read the fact from there when it matters; your own copy is never the authority.

anchorproofrailflowmatchrunremitroutegatewayiamledgerregistrydeliverydocumentsintelligence
object always "event" required

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

stream string required

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

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

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
curl -X GET "https://sandbox.droomwork.io/v1/rail/events?stream=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
const result = await api.railEventsList({ stream: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', after: 0, limit: 25 });
// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/events?stream=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

# query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
result = api.rail_events_list(stream='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', after=0, limit=25)
import os

import requests

# query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/rail/events?stream=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
    },
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

# query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
$result = $api->railEventsList(stream: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', after: 0, limit: 25);
<?php
// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/events?stream=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
var result = api.railEventsList("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", 0, 25, null);
// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/events?stream=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
var result = api.RailEventsList(stream: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", after: 0, limit: 25);
// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/events?stream=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
result, _, err := client.RAILAPI.RailEventsList(ctx).Stream("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").After(0).Limit(25).Execute()
// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/events?stream=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "list",
  "data": [
    {
      "id": "evt_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "type": "run.payslip.calculated",
      "schema_version": 1,
      "org_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "sequence": 0,
      "occurred_at": "2026-09-01T09:00:00Z",
      "livemode": true,
      "mocked": true,
      "source": "anchor",
      "request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "event",
      "stream": "example",
      "data": {}
    }
  ],
  "has_more": true
}
GET/v1/rail/events/{event_id}#

Retrieve an event

rail.events.retrieve

Returns one event. An identifier belonging to another organisation is not found rather than refused, so nothing confirms it exists.

Path parameters

event_id string required

The id of the event you want, from an entry on GET /v1/rail/events (rail.events.list) or the id on an event delivered to your webhook. It starts with evt_.

Returns

The event.

id string required

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

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

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

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

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

When the event happened, as an RFC 3339 timestamp in UTC. Not when it was delivered: a redelivery carries the original value.

request_id string optional

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

Which realm the event happened in. False is the sandbox.

mocked boolean required

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

Which part of Droomwork is the authority for this fact: anchor (identity), proof (credentials), rail (engagements), flow (sourcing), match (allocation), run (payroll), remit (remittance), route (payouts), gateway (the API's front door), iam (accounts and API keys), ledger (the books), registry (rule packs), delivery (webhooks and messages), documents (rendered payslips and instruments) or intelligence (AI decisions). Read the fact from there when it matters; your own copy is never the authority.

anchorproofrailflowmatchrunremitroutegatewayiamledgerregistrydeliverydocumentsintelligence
object always "event" required

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

stream string required

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

The 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

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/rail/events/%7Bevent_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railEventsRetrieve({ eventId: '{event_id}' });
const response = await fetch('https://sandbox.droomwork.io/v1/rail/events/%7Bevent_id%7D', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_events_retrieve(event_id='{event_id}')
import os

import requests

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

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

$result = $api->railEventsRetrieve(event_id: '{event_id}');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/events/%7Bevent_id%7D');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

var result = api.railEventsRetrieve("{event_id}");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/events/%7Bevent_id%7D"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

var result = api.RailEventsRetrieve(eventId: "{event_id}");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/events/%7Bevent_id%7D");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.RAILAPI.RailEventsRetrieve(ctx, "{event_id}").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/events/%7Bevent_id%7D", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "evt_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "type": "run.payslip.calculated",
  "schema_version": 1,
  "org_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "sequence": 0,
  "occurred_at": "2026-09-01T09:00:00Z",
  "livemode": true,
  "mocked": true,
  "source": "anchor",
  "request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "event",
  "stream": "example",
  "data": {}
}
GET/v1/rail/audit_entries#

List audit entries

rail.audit_entries.list

Who did what, newest first. One row per attempt rather than per success, so refusals are here too.

A read that succeeded is not recorded.

Query parameters

action string optional

Only 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 optional

Only 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 optional

Only 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 optional

Only 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 optional

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

succeededrefusedfailed
recorded_after string optional

Only 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 optional

Only 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 optional

How 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 optional

The id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.

Returns

A page of audit entries.

object always "list" required

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

data array of AuditEntry required

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

13 fields of AuditEntry
id string required

The entry's identifier, as it appears on GET /v1/rail/audit_entries; it starts with audit_entry_ and never changes. Pass it as audit_entry_id to GET /v1/rail/audit_entries/{audit_entry_id}, or as starting_after to page on from it.

object always "audit_entry" required

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

livemode boolean required

Which realm the action happened in. False is the sandbox.

mocked boolean required

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

When the attempt was made, as an RFC 3339 timestamp in UTC. The list is ordered by it, newest first.

request_id string required

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

What kind of caller made the attempt: client for one of your own API keys or OAuth clients, staff for a Droomwork staff member acting under a support grant, service when Droomwork acted for you.

actor_id string required

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

What was attempted, as the method and the route pattern.

resource string · nullable optional

The 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 optional

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

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

succeededrefusedfailed
status integer required

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

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
curl -X GET "https://sandbox.droomwork.io/v1/rail/audit_entries?action=POST%20%2Fv1%2Frail%2Fengagements&actor_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=engagements&resource_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
const result = await api.railAuditEntriesList({ action: 'POST /v1/rail/engagements', actorId: 'sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', resource: 'engagements', resourceId: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', outcome: 'succeeded', recordedAfter: '2026-01-01T00:00:00Z', recordedBefore: '2027-01-01T00:00:00Z', limit: 25 });
// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/rail/audit_entries?action=POST%20%2Fv1%2Frail%2Fengagements&actor_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=engagements&resource_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

# query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
result = api.rail_audit_entries_list(action='POST /v1/rail/engagements', actor_id='sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', resource='engagements', resource_id='rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', outcome='succeeded', recorded_after='2026-01-01T00:00:00Z', recorded_before='2027-01-01T00:00:00Z', limit=25)
import os

import requests

# query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/rail/audit_entries?action=POST%20%2Fv1%2Frail%2Fengagements&actor_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=engagements&resource_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
    },
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

# query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
$result = $api->railAuditEntriesList(action: 'POST /v1/rail/engagements', actor_id: 'sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', resource: 'engagements', resource_id: 'rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', outcome: 'succeeded', recorded_after: '2026-01-01T00:00:00Z', recorded_before: '2027-01-01T00:00:00Z', limit: 25);
<?php
// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/audit_entries?action=POST%20%2Fv1%2Frail%2Fengagements&actor_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=engagements&resource_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
var result = api.railAuditEntriesList("POST /v1/rail/engagements", "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "engagements", "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "succeeded", "2026-01-01T00:00:00Z", "2027-01-01T00:00:00Z", 25, null);
// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/audit_entries?action=POST%20%2Fv1%2Frail%2Fengagements&actor_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=engagements&resource_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
var result = api.RailAuditEntriesList(action: "POST /v1/rail/engagements", actorId: "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", resource: "engagements", resourceId: "rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", outcome: "succeeded", recordedAfter: "2026-01-01T00:00:00Z", recordedBefore: "2027-01-01T00:00:00Z", limit: 25);
// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/audit_entries?action=POST%20%2Fv1%2Frail%2Fengagements&actor_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=engagements&resource_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
result, _, err := client.RAILAPI.RailAuditEntriesList(ctx).Action("POST /v1/rail/engagements").ActorId("sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Resource("engagements").ResourceId("rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Outcome("succeeded").RecordedAfter("2026-01-01T00:00:00Z").RecordedBefore("2027-01-01T00:00:00Z").Limit(25).Execute()
// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/audit_entries?action=POST%20%2Fv1%2Frail%2Fengagements&actor_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=engagements&resource_id=rail_engagement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "list",
  "data": [
    {
      "id": "audit_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "audit_entry",
      "livemode": true,
      "mocked": true,
      "at": "2026-09-01T09:00:00Z",
      "request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "actor_type": "example",
      "actor_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "action": "example",
      "outcome": "succeeded",
      "status": 1,
      "resource": "example",
      "resource_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
    }
  ],
  "has_more": true
}
GET/v1/rail/audit_entries/{audit_entry_id}#

Retrieve an audit entry

rail.audit_entries.retrieve

Returns one entry. An identifier belonging to another organisation is not found rather than refused, so nothing confirms it exists.

Path parameters

audit_entry_id string required

The id of the entry you want, from an entry on GET /v1/rail/audit_entries (rail.audit_entries.list). It starts with audit_entry_.

Returns

The audit entry.

id string required

The entry's identifier, as it appears on GET /v1/rail/audit_entries; it starts with audit_entry_ and never changes. Pass it as audit_entry_id to GET /v1/rail/audit_entries/{audit_entry_id}, or as starting_after to page on from it.

object always "audit_entry" required

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

livemode boolean required

Which realm the action happened in. False is the sandbox.

mocked boolean required

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

When the attempt was made, as an RFC 3339 timestamp in UTC. The list is ordered by it, newest first.

request_id string required

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

What kind of caller made the attempt: client for one of your own API keys or OAuth clients, staff for a Droomwork staff member acting under a support grant, service when Droomwork acted for you.

actor_id string required

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

What was attempted, as the method and the route pattern.

resource string · nullable optional

The 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 optional

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

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

succeededrefusedfailed
status integer required

The 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

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/rail/audit_entries/%7Baudit_entry_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RAILApi } from '@droomwork/sdk';

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

const result = await api.railAuditEntriesRetrieve({ auditEntryId: '{audit_entry_id}' });
const response = await fetch('https://sandbox.droomwork.io/v1/rail/audit_entries/%7Baudit_entry_id%7D', {
  method: 'GET',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
  },
});
const result = await response.json();
import os

import droomwork

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

result = api.rail_audit_entries_retrieve(audit_entry_id='{audit_entry_id}')
import os

import requests

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

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

$result = $api->railAuditEntriesRetrieve(audit_entry_id: '{audit_entry_id}');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/rail/audit_entries/%7Baudit_entry_id%7D');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RailApi;

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

var result = api.railAuditEntriesRetrieve("{audit_entry_id}");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/rail/audit_entries/%7Baudit_entry_id%7D"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

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

var result = api.RailAuditEntriesRetrieve(auditEntryId: "{audit_entry_id}");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/rail/audit_entries/%7Baudit_entry_id%7D");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.RAILAPI.RailAuditEntriesRetrieve(ctx, "{audit_entry_id}").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/rail/audit_entries/%7Baudit_entry_id%7D", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "audit_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "audit_entry",
  "livemode": true,
  "mocked": true,
  "at": "2026-09-01T09:00:00Z",
  "request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "actor_type": "example",
  "actor_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "action": "example",
  "outcome": "succeeded",
  "status": 1,
  "resource": "example",
  "resource_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}