Version 1.0.0
Heros RUN ENTERPRISE
Gross to net payroll calculation for Nigeria.
https://sandbox.droomwork.ioDroomwork-Api-Key: dw_test_… or Authorization: Bearer dw_test_…RUN computes your payroll. Give it a roster and a set of pay profiles, open a run for the period, and you get payslips that carry the arithmetic behind every figure on them.
What you should know before you start
Money is whole minor units. An amount is an integer count of the currency's smallest unit plus the currency code. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. Send a fractional amount and it's refused, not rounded.
Every figure can be defended later. A payslip line comes with the arithmetic that produced it and the rule pack version it was produced under. So when somebody questions a deduction months afterwards you answer from the record, and you don't recompute it from memory and hope the two agree. Keep what you are given.
A run moves through states, and you move it with endpoints. Create it, simulate it to see the figures, approve it, then execute it to write the payslips. You can't execute a run that was never approved, and once a run has executed you can't cancel it.
The sandbox answers with stand-in figures. Every record it returns says so in mocked. Build against the shapes and the states, which are what you will still be integrating with when the figures become real, and don't reconcile the amounts.
Getting started
Get a sandbox key from the developer portal. A run needs somebody to pay, so create a pay profile and a roster entry first. Then open a run for the period, simulate it, and read its totals.
Every operation below has two samples: Direct HTTP, with nothing but your language's own client, and Client library, with ours. Same request, same answer; the switch above each sample picks one and remembers it. Which should I use?
/v1/pay_profiles#Follow a worker's pay terms over time
payroll.pay_profiles.list
This gives you your pay profiles, superseded versions included. So when somebody questions a payslip from six months ago, your product can show the terms in force then and not whatever is current.
Send nothing to get them all, or pass subject_id for one person's. Each row is one version, newest first, with the salary, allowances and pay frequency it pays. Profiles come from POST /v1/pay_profiles.
A version gains effective_to only when a later version replaces it, the day before that one starts.
The version in force is the one no later version has replaced. Follow version and supersedes to order a person's versions. It is the only version you may supersede at PATCH /v1/pay_profiles/{pay_profile_id} or attach at POST /v1/payroll/roster_entries, and both refuse a replaced one.
Query parameters
limit
integer
optionalHow many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.
starting_after
string
optionalThe id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.
subject_id
string
optionalReturn only this person's pay profiles, superseded versions included: pass their subject identifier, starting with sub_, the subject_id you sent at POST /v1/pay_profiles. Leave it out to get every profile.
Returns
A page of pay profiles.
object
always "list"
requiredAlways list. Tells you which kind of record you are looking at, so one handler can read any response.
data
array of PayProfile
requiredThe records on this page, in the order the list promises. Empty when nothing matched.
12 fields of PayProfile
id
string
requiredThe identifier of this profile version. It starts with run_enterprise_pay_profile_, is assigned at POST /v1/pay_profiles or PATCH /v1/pay_profiles/{pay_profile_id} and never changes; pass it as pay_profile_id to name this version.
object
always "pay_profile"
requiredAlways pay_profile. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
subject_id
string
requiredThe worker this profile pays: their subject identifier, starting with sub_, as you sent it at POST /v1/pay_profiles. Every version of the profile carries the same one.
base_salary
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
pay_frequency
string
optionalHow often the worker is paid: monthly (once a month), semi_monthly (twice a month) or weekly (once a week). It's what you set at POST /v1/pay_profiles; a new version keeps it.
monthlysemi_monthlyweeklyallowances
array of object
optionalFixed allowances on top of base salary, each as a code and an amount. Empty or absent when the worker has none.
2 fields
code
string
requiredYour own code for this allowance, as you sent it on the create or supersede call. Keep it the same on every version so you can follow one allowance over time.
amount
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
effective_from
string · date
requiredThe first day this version applies, as a calendar date in YYYY-MM-DD. A payslip for an earlier period computes from the version in force then.
effective_to
string · date · nullable
optionalThe last day this version applies, as a calendar date in YYYY-MM-DD, or null while it's current. A leaver is an end date, not a deletion.
version
integer · minimum 1
requiredWhich version of the worker's profile this is, counting from 1. Superseding it creates the next; earlier versions stay listed so you can explain an old payslip.
supersedes
string · nullable
optionalThe id of the version this one replaced, or null for the first version.
has_more
boolean
requiredtrue when there are more records after this page. Pass the last record's id as starting_after to get the next page.
Other responses
Errors it can return
# query parameters: limit (optional), starting_after (optional), subject_id (optional)
curl -X GET "https://sandbox.droomwork.io/v1/pay_profiles?limit=25&subject_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
// query parameters: limit (optional), starting_after (optional), subject_id (optional)
const result = await api.payrollPayProfilesList({ limit: 25, subjectId: 'sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });// query parameters: limit (optional), starting_after (optional), subject_id (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/pay_profiles?limit=25&subject_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
# query parameters: limit (optional), starting_after (optional), subject_id (optional)
result = api.payroll_pay_profiles_list(limit=25, subject_id='sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import requests
# query parameters: limit (optional), starting_after (optional), subject_id (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/pay_profiles?limit=25&subject_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
# query parameters: limit (optional), starting_after (optional), subject_id (optional)
$result = $api->payrollPayProfilesList(limit: 25, subject_id: 'sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
// query parameters: limit (optional), starting_after (optional), subject_id (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/pay_profiles?limit=25&subject_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
// query parameters: limit (optional), starting_after (optional), subject_id (optional)
var result = api.payrollPayProfilesList(25, null, "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");// query parameters: limit (optional), starting_after (optional), subject_id (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/pay_profiles?limit=25&subject_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
// query parameters: limit (optional), starting_after (optional), subject_id (optional)
var result = api.PayrollPayProfilesList(limit: 25, subjectId: "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");// query parameters: limit (optional), starting_after (optional), subject_id (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/pay_profiles?limit=25&subject_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: limit (optional), starting_after (optional), subject_id (optional)
result, _, err := client.RUNAPI.PayrollPayProfilesList(ctx).Limit(25).SubjectId("sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()// query parameters: limit (optional), starting_after (optional), subject_id (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/pay_profiles?limit=25&subject_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"object": "list",
"data": [
{
"id": "run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "pay_profile",
"livemode": false,
"mocked": true,
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"base_salary": {
"amount": 45000000,
"currency": "NGN"
},
"effective_from": "2026-09-01",
"version": 1,
"pay_frequency": "monthly",
"allowances": [
{
"code": "pension_employee",
"amount": {
"amount": 7500000,
"currency": "NGN"
}
}
],
"effective_to": "2026-09-01",
"supersedes": "example"
}
],
"has_more": true
}
/v1/pay_profiles#Set out what somebody is paid
payroll.pay_profiles.create
This sets out what somebody is paid. So your product records their terms once, and when a roster entry names the profile, every payroll run from then on computes their pay from it.
Send the person, their salary and allowances, how often they are paid and the date those terms start. You get the profile back with its id. They need an anchored identity, from POST /v1/identity/verifications.
Set pay_frequency yourself: leave it out and the profile pays monthly.
A later change is a new version and not an edit, so a payslip already issued still computes from what was true then. A new version cannot move the frequency or the currency: a change to either one is a second profile.
That id is what a roster entry names at POST /v1/payroll/roster_entries. A profile on its own pays nobody: a run pays the people its entries name.
Headers
Idempotency-Key
string
requiredA key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.
Body
subject_id
string
requiredThe worker this profile pays: their subject identifier, starting with sub_, the subject_ref you chose for them at POST /v1/identity/verifications. They need an anchored identity, or the call is refused with subject_not_anchored.
base_salary
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
pay_frequency
string
optionalHow often this person is paid: monthly (once a month), semi_monthly (twice a month) or weekly (once a week). A new version made at PATCH /v1/pay_profiles/{pay_profile_id} keeps whatever you set here.
monthlysemi_monthlyweeklyallowances
array of object
optionalThe allowances paid on top of base_salary, each as a code you choose and an amount in whole minor units. Leave it out when there are none.
2 fields
code
string
requiredYour code for this allowance, such as housing or transport. Use the same code on every version of the profile so you can tell one allowance from another over time.
amount
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
effective_from
string · date
requiredThe first date these terms apply, as a calendar date in YYYY-MM-DD. A payslip for an earlier period computes from whatever was true then, never from this profile.
Returns
The pay profile.
id
string
requiredThe identifier of this profile version. It starts with run_enterprise_pay_profile_, is assigned at POST /v1/pay_profiles or PATCH /v1/pay_profiles/{pay_profile_id} and never changes; pass it as pay_profile_id to name this version.
object
always "pay_profile"
requiredAlways pay_profile. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
subject_id
string
requiredThe worker this profile pays: their subject identifier, starting with sub_, as you sent it at POST /v1/pay_profiles. Every version of the profile carries the same one.
base_salary
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
pay_frequency
string
optionalHow often the worker is paid: monthly (once a month), semi_monthly (twice a month) or weekly (once a week). It's what you set at POST /v1/pay_profiles; a new version keeps it.
monthlysemi_monthlyweeklyallowances
array of object
optionalFixed allowances on top of base salary, each as a code and an amount. Empty or absent when the worker has none.
2 fields
code
string
requiredYour own code for this allowance, as you sent it on the create or supersede call. Keep it the same on every version so you can follow one allowance over time.
amount
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
effective_from
string · date
requiredThe first day this version applies, as a calendar date in YYYY-MM-DD. A payslip for an earlier period computes from the version in force then.
effective_to
string · date · nullable
optionalThe last day this version applies, as a calendar date in YYYY-MM-DD, or null while it's current. A leaver is an end date, not a deletion.
version
integer · minimum 1
requiredWhich version of the worker's profile this is, counting from 1. Superseding it creates the next; earlier versions stay listed so you can explain an old payslip.
supersedes
string · nullable
optionalThe id of the version this one replaced, or null for the first version.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/pay_profiles" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"subject_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","base_salary":{"amount":45000000,"currency":"NGN"},"effective_from":"2026-09-01","pay_frequency":"monthly","allowances":[{"code":"housing","amount":{"amount":7500000,"currency":"NGN"}}]}'import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollPayProfilesCreate({
idempotencyKey: crypto.randomUUID(),
runPayProfileCreateRequest: {"subjectId":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","baseSalary":{"amount":45000000,"currency":"NGN"},"effectiveFrom":"2026-09-01","payFrequency":"monthly","allowances":[{"code":"housing","amount":{"amount":7500000,"currency":"NGN"}}]},
});const response = await fetch('https://sandbox.droomwork.io/v1/pay_profiles', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"base_salary": {
"amount": 45000000,
"currency": "NGN"
},
"effective_from": "2026-09-01",
"pay_frequency": "monthly",
"allowances": [
{
"code": "housing",
"amount": {
"amount": 7500000,
"currency": "NGN"
}
}
]
}),
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_pay_profiles_create(body={"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "base_salary": {"amount": 45000000, "currency": "NGN"}, "effective_from": "2026-09-01", "pay_frequency": "monthly", "allowances": [{"code": "housing", "amount": {"amount": 7500000, "currency": "NGN"}}]})import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/pay_profiles',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "base_salary": {"amount": 45000000, "currency": "NGN"}, "effective_from": "2026-09-01", "pay_frequency": "monthly", "allowances": [{"code": "housing", "amount": {"amount": 7500000, "currency": "NGN"}}]},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollPayProfilesCreate($idempotencyKey, json_decode('{"subject_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","base_salary":{"amount":45000000,"currency":"NGN"},"effective_from":"2026-09-01","pay_frequency":"monthly","allowances":[{"code":"housing","amount":{"amount":7500000,"currency":"NGN"}}]}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/pay_profiles');
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 => '{"subject_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","base_salary":{"amount":45000000,"currency":"NGN"},"effective_from":"2026-09-01","pay_frequency":"monthly","allowances":[{"code":"housing","amount":{"amount":7500000,"currency":"NGN"}}]}',
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollPayProfilesCreate(idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/pay_profiles"))
.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("""
{
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"base_salary": {
"amount": 45000000,
"currency": "NGN"
},
"effective_from": "2026-09-01",
"pay_frequency": "monthly",
"allowances": [
{
"code": "housing",
"amount": {
"amount": 7500000,
"currency": "NGN"
}
}
]
}
"""))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollPayProfilesCreate(idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/pay_profiles");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"base_salary": {
"amount": 45000000,
"currency": "NGN"
},
"effective_from": "2026-09-01",
"pay_frequency": "monthly",
"allowances": [
{
"code": "housing",
"amount": {
"amount": 7500000,
"currency": "NGN"
}
}
]
}
""", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollPayProfilesCreate(ctx).IdempotencyKey(key).RunPayProfileCreateRequest(body).Execute()body := strings.NewReader(`{
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"base_salary": {
"amount": 45000000,
"currency": "NGN"
},
"effective_from": "2026-09-01",
"pay_frequency": "monthly",
"allowances": [
{
"code": "housing",
"amount": {
"amount": 7500000,
"currency": "NGN"
}
}
]
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/pay_profiles", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "pay_profile",
"livemode": false,
"mocked": true,
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"base_salary": {
"amount": 45000000,
"currency": "NGN"
},
"effective_from": "2026-09-01",
"version": 1,
"pay_frequency": "monthly",
"allowances": [
{
"code": "pension_employee",
"amount": {
"amount": 7500000,
"currency": "NGN"
}
}
],
"effective_to": "2026-09-01",
"supersedes": "example"
}
/v1/payroll/imports#See every upload and its report
payroll.imports.list
This gives you every import record this organisation holds, each with the report that came back. So anybody in the organisation can open an uploads page in your product and see everything sent for checking.
Send nothing required. You get the records oldest first, each carrying the kind of file it was, its status, when it was created and its report.
A record does not name the file it came from, so pair its id with the file name you hold.
The rows a report names are a stand-in. Read mocked before you put one in front of anybody.
Read one back at GET /v1/payroll/imports/{import_id}; the list already carries each one whole. Send a corrected file at POST /v1/payroll/imports. Checking a file is not adding anybody: people go on the roster at POST /v1/payroll/roster_entries, and their pay profiles at POST /v1/pay_profiles.
Query parameters
limit
integer
optionalHow many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.
starting_after
string
optionalThe id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.
Returns
A page of imports, roster and pay profile alike, told apart by kind and nothing else.
object
always "list"
requiredAlways list. Tells you which kind of record you are looking at, so one handler can read any response.
data
array of Import
requiredThe records on this page, in the order the list promises. Empty when nothing matched.
8 fields of Import
id
string
requiredThe import's identifier, from POST /v1/payroll/imports. It starts with run_enterprise_import_ and never changes; pass it as import_id at GET /v1/payroll/imports/{import_id} to read the report again.
object
always "payroll_import"
requiredAlways payroll_import. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
kind
string
requiredWhat the file holds, as you sent it as kind at POST /v1/payroll/imports: roster, the people you pay, or pay_profiles, how each of them is paid. It decides how every row is checked and what an accepted import creates.
rosterpay_profilesstatus
string
requiredvalidating while rows are checked, pending_acceptance once the report is ready for you to read, accepted once the rows are committed, rejected when they won't be. Nothing is committed before accepted.
validatingpending_acceptanceacceptedrejectedvalidation_report
ValidationReport
requiredThe result of checking a submission before anything is committed. Names every failing entry rather than stopping at the first, so one submission tells you everything to fix. The same shape wherever the platform ingests, validates and reports, which is every bulk intake in every module.
4 fields of ValidationReport
entry_count
integer · minimum 0
requiredHow many entries the submission held: every row in the file or line in the instruction set, whether it passed or not.
error_count
integer · minimum 0
requiredHow many entries failed with an error. Each is named in entries with its position and why, so one submission tells you everything to fix.
warning_count
integer · minimum 0
requiredHow many entries passed with a warning, each named in entries. A warning does not fail the entry; it points at something to confirm before you go ahead.
entries
array of object
optionalOne item per error or warning found, with the row or line it sits on, its severity, a code and what was wrong. Empty or absent when every entry passed clean.
4 fields
index
integer · minimum 1
requiredThe row in a file, or the line in an instruction set.
severity
string
requirederror means the entry failed and must be fixed. warning means it passed, but points at something you should confirm before you go ahead.
errorwarningcode
string
requiredWhy the entry was flagged, as a code you can branch on. detail says the same in words and may change; the code does not.
detail
string
requiredWhat was wrong with the entry, in plain words. Show it beside the row; it may change, so branch on code.
created_at
string · date-time
optionalWhen the record was created, as an RFC 3339 timestamp in UTC.
has_more
boolean
requiredtrue when there are more records after this page. Pass the last record's id as starting_after to get the next page.
Other responses
Errors it can return
# query parameters: limit (optional), starting_after (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payroll/imports?limit=25" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
// query parameters: limit (optional), starting_after (optional)
const result = await api.payrollImportsList({ limit: 25 });// query parameters: limit (optional), starting_after (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/payroll/imports?limit=25', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
# query parameters: limit (optional), starting_after (optional)
result = api.payroll_imports_list(limit=25)import os
import requests
# query parameters: limit (optional), starting_after (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/imports?limit=25',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
# query parameters: limit (optional), starting_after (optional)
$result = $api->payrollImportsList(limit: 25);<?php
// query parameters: limit (optional), starting_after (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/imports?limit=25');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
// query parameters: limit (optional), starting_after (optional)
var result = api.payrollImportsList(25, null);// query parameters: limit (optional), starting_after (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/imports?limit=25"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
// query parameters: limit (optional), starting_after (optional)
var result = api.PayrollImportsList(limit: 25);// query parameters: limit (optional), starting_after (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/imports?limit=25");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: limit (optional), starting_after (optional)
result, _, err := client.RUNAPI.PayrollImportsList(ctx).Limit(25).Execute()// query parameters: limit (optional), starting_after (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/imports?limit=25", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"object": "list",
"data": [
{
"id": "run_enterprise_import_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "payroll_import",
"livemode": false,
"mocked": true,
"kind": "roster",
"status": "validating",
"validation_report": {
"entry_count": 0,
"error_count": 0,
"warning_count": 0,
"entries": [
{
"index": 1,
"severity": "error",
"code": "pension_employee",
"detail": "The payee has no verified destination, so this line cannot be paid."
}
]
},
"created_at": "2026-09-01T09:00:00Z"
}
],
"has_more": true
}
/v1/payroll/imports#Check a roster or pay profile file
payroll.imports.create
This submits a roster, or a set of pay profiles, as one file to be checked. So your product can take a payroll's intake as one upload, and fix the rows it flags before creating a single record.
Send the file as the file part of a multipart/form-data body, with kind naming which of the two it holds. You get the import back with its id and its validation report. Nothing has to exist first.
Checking a file adds nobody and creates no pay profile. Nothing in it reaches your roster until you create each record yourself.
Read the import back at GET /v1/payroll/imports/{import_id}, and see every one at GET /v1/payroll/imports. People go on the roster at POST /v1/payroll/roster_entries, and a pay profile at POST /v1/pay_profiles, one call per row.
Headers
Idempotency-Key
string
requiredA key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.
Returns
The import with its report, every flagged row named by its index in the file you sent.
id
string
requiredThe import's identifier, from POST /v1/payroll/imports. It starts with run_enterprise_import_ and never changes; pass it as import_id at GET /v1/payroll/imports/{import_id} to read the report again.
object
always "payroll_import"
requiredAlways payroll_import. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
kind
string
requiredWhat the file holds, as you sent it as kind at POST /v1/payroll/imports: roster, the people you pay, or pay_profiles, how each of them is paid. It decides how every row is checked and what an accepted import creates.
rosterpay_profilesstatus
string
requiredvalidating while rows are checked, pending_acceptance once the report is ready for you to read, accepted once the rows are committed, rejected when they won't be. Nothing is committed before accepted.
validatingpending_acceptanceacceptedrejectedvalidation_report
ValidationReport
requiredThe result of checking a submission before anything is committed. Names every failing entry rather than stopping at the first, so one submission tells you everything to fix. The same shape wherever the platform ingests, validates and reports, which is every bulk intake in every module.
4 fields of ValidationReport
entry_count
integer · minimum 0
requiredHow many entries the submission held: every row in the file or line in the instruction set, whether it passed or not.
error_count
integer · minimum 0
requiredHow many entries failed with an error. Each is named in entries with its position and why, so one submission tells you everything to fix.
warning_count
integer · minimum 0
requiredHow many entries passed with a warning, each named in entries. A warning does not fail the entry; it points at something to confirm before you go ahead.
entries
array of object
optionalOne item per error or warning found, with the row or line it sits on, its severity, a code and what was wrong. Empty or absent when every entry passed clean.
4 fields
index
integer · minimum 1
requiredThe row in a file, or the line in an instruction set.
severity
string
requirederror means the entry failed and must be fixed. warning means it passed, but points at something you should confirm before you go ahead.
errorwarningcode
string
requiredWhy the entry was flagged, as a code you can branch on. detail says the same in words and may change; the code does not.
detail
string
requiredWhat was wrong with the entry, in plain words. Show it beside the row; it may change, so branch on code.
created_at
string · date-time
optionalWhen the record was created, as an RFC 3339 timestamp in UTC.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/payroll/imports" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollImportsCreate({});const response = await fetch('https://sandbox.droomwork.io/v1/payroll/imports', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Idempotency-Key': crypto.randomUUID(),
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_imports_create()import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/payroll/imports',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollImportsCreate();<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/imports');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollImportsCreate();var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/imports"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.header("Idempotency-Key", UUID.randomUUID().toString())
.method("POST", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollImportsCreate();using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payroll/imports");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollImportsCreate(ctx).Execute()req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payroll/imports", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_import_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "payroll_import",
"livemode": false,
"mocked": true,
"kind": "roster",
"status": "validating",
"validation_report": {
"entry_count": 0,
"error_count": 0,
"warning_count": 0,
"entries": [
{
"index": 1,
"severity": "error",
"code": "pension_employee",
"detail": "The payee has no verified destination, so this line cannot be paid."
}
]
},
"created_at": "2026-09-01T09:00:00Z"
}
/v1/payroll/readiness#Check what is still missing before a run
payroll.readiness.retrieve
This answers what your organisation still has to do before RUN will pay anybody, so your product can show a setup screen with the work that is left.
Send nothing; this takes no parameters. Each row carries the assurance its fact needs and the assurance you hold: attested when someone stated it, verified when a source confirmed it. The identity row needs verified and an attestation does not satisfy it.
The answer is about your whole roster. One person short holds a row back, so a row is satisfied only once everybody clears it, and an empty roster holds nothing yet.
Clear an identity at POST /v1/identity/verifications. An engagement becomes payable at POST /v1/rail/engagements/{engagement_id}/register, and its row reads not_supplied until it does.
A ready of true is not a promise a run will go through: the gates in always_enforced apply whatever it says.
Returns
The readiness report.
object
always "readiness_report"
requiredAlways readiness_report. Tells you which kind of record you are looking at, so one handler can read any response.
module
string
requiredThe module whose readiness endpoint you called, such as GET /v1/payroll/readiness: anchor (identity), proof (credentials), rail (engagements), flow (sourcing), match (allocation), run (payroll), remit (remittance) or route (payouts).
anchorproofrailflowmatchrunremitroutemode
string
requiredintegrated means a required fact comes from the Heros module that owns it; standalone means you supply it yourself under an attestation. The checks are the same in both, and what each fact is worth is recorded on its row as held.
integratedstandaloneready
boolean
requiredtrue when every row is satisfied and the module has what it needs from you. false when a required fact is missing or held at too low an assurance; rows says which.
rows
array of ReadinessRow
requiredOne row per fact the module requires: who owns it, the assurance it needs, what you hold and whether that satisfies it. The rows that are not satisfied are what to bring.
8 fields of ReadinessRow
fact
string
requiredNamed for what it is, not for who supplies it.
owner
string
requiredThe sibling module that owns this fact when running integrated.
contract_row
string
optionalrequired
string
requiredHow a fact was established. Recorded on the fact rather than in configuration, so an attested identity and a verified one stay distinguishable a year later, which is the distinction that matters when something is disputed.
attestedverifiedheld
one of
requiredAssuranceorsource
string · nullable
optionalintegratedstandalonenullsatisfied
boolean
requiredmissing_because
string · nullable
optionalnot_suppliedassurance_too_lownullalways_enforced
array of string
optionalGates that hold in every mode and cannot be attested away. Consent before any source is queried, bank account validation, the tax identifiers returns are filed under, and duplicate detection within the organisation's own population.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/payroll/readiness" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollReadinessRetrieve({});const response = await fetch('https://sandbox.droomwork.io/v1/payroll/readiness', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_readiness_retrieve()import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/readiness',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollReadinessRetrieve();<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/readiness');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollReadinessRetrieve();var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/readiness"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollReadinessRetrieve();using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/readiness");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollReadinessRetrieve(ctx).Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/readiness", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"object": "readiness_report",
"module": "run",
"mode": "integrated",
"ready": true,
"rows": [
{
"fact": "anchored_subject",
"owner": "anchor",
"contract_row": "6.1.13",
"required": "verified",
"held": "verified",
"source": "integrated",
"satisfied": true,
"missing_because": null
},
{
"fact": "obligation_envelope",
"owner": "rail",
"contract_row": "6.6.14",
"required": "attested",
"held": "attested",
"source": "integrated",
"satisfied": true,
"missing_because": null
}
],
"always_enforced": [
"consent_token",
"bank_account_validation",
"tax_identifier_validation",
"duplicate_detection"
]
}
/v1/payroll/settings#Read what your organisation has answered
payroll.settings.retrieve
This gives you what your organisation has answered about itself: what a run reads before it computes, and the figure an approval is checked against. So your product can show a settings screen that explains why a deduction is being taken.
Send nothing. Each answer comes back under applicable, keyed by the condition it answers, with any pension rate you set above the pack's own rate and updated_at for the last change.
Nothing has to exist first, and an organisation that has answered nothing reads empty settings. A run that meets an unanswered condition is refused, and the refusal names the condition. Answer it at PATCH /v1/payroll/settings.
A key you no longer recognise is one somebody answered once: a condition no pack names is kept and never read.
The maker-checker threshold is here too. Above that gross, whoever prepared a run cannot also approve it.
Returns
Your settings.
object
always "payroll_settings"
requiredAlways payroll_settings. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm these settings are for. false is the sandbox and true is live; each realm keeps its own answers, so what you set in the sandbox changes nothing about your live payroll.
applicable
object
requiredWhat your organisation answers about the conditions a rule pack names, keyed by the condition. organisation_enrolled is the national health insurance scheme; employer_with_five_or_more_employees_or_turnover_at_or_above_5000000000 is the industrial training levy. Empty until you answer, and an element whose condition is unanswered stops a run with prerequisite_not_met rather than being left out quietly.
pension_employee_rate
Rate
optionalAn exact fraction of two whole numbers. Never a decimal, and it carries no currency.
2 fields of Rate
numerator
integer · int64
requiredThe top of the fraction, a whole number. 8 over 100 is eight percent.
denominator
integer · int64 · minimum 1
requiredThe bottom of the fraction, a whole number of 1 or more. Never zero, so every rate can be evaluated.
pension_employer_rate
Rate
optionalAn exact fraction of two whole numbers. Never a decimal, and it carries no currency.
2 fields of Rate
numerator
integer · int64
requiredThe top of the fraction, a whole number. 8 over 100 is eight percent.
denominator
integer · int64 · minimum 1
requiredThe bottom of the fraction, a whole number of 1 or more. Never zero, so every rate can be evaluated.
maker_checker_threshold
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
updated_at
string · date-time
optionalWhen these settings last changed, from the database's own clock.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/payroll/settings" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollSettingsRetrieve({});const response = await fetch('https://sandbox.droomwork.io/v1/payroll/settings', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_settings_retrieve()import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/settings',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollSettingsRetrieve();<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/settings');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollSettingsRetrieve();var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/settings"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollSettingsRetrieve();using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/settings");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollSettingsRetrieve(ctx).Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/settings", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"object": "payroll_settings",
"livemode": false,
"applicable": {},
"pension_employee_rate": {
"numerator": 1,
"denominator": 1
},
"pension_employer_rate": {
"numerator": 1,
"denominator": 1
},
"maker_checker_threshold": {
"amount": 50000000,
"currency": "NGN"
},
"updated_at": "2026-09-01T09:00:00Z"
}
/v1/payroll/settings#Change what your organisation answers
payroll.settings.update
This records what your organisation answers about its own payroll. So your product can carry one settings screen, and every run computes on that organisation's own figures.
Send only what you are changing and your settings come back whole. What you leave out keeps its answer, and no settings have to exist first.
A pension rate you declare is checked against the rule pack in force. Below the floor that pack carries it is refused with invalid_request, and nothing is stored.
You can raise or lower maker_checker_threshold later, and you cannot remove it once it is set. Set it to zero and nobody can approve a run they prepared unless it pays nothing.
A run that meets a condition you have not answered is refused and names it. Answer it here and simulate again at POST /v1/payroll/runs/{run_id}/simulate.
Headers
Idempotency-Key
string
requiredA key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.
Body
applicable
object
optionalThe conditions you are answering, keyed by the name the rule pack uses. Merged into what you have already answered rather than replacing it, so answering one condition does not unanswer another. A condition no pack names is accepted and never read.
pension_employee_rate
Rate
optionalAn exact fraction of two whole numbers. Never a decimal, and it carries no currency.
2 fields of Rate
numerator
integer · int64
requiredThe top of the fraction, a whole number. 8 over 100 is eight percent.
denominator
integer · int64 · minimum 1
requiredThe bottom of the fraction, a whole number of 1 or more. Never zero, so every rate can be evaluated.
pension_employer_rate
Rate
optionalAn exact fraction of two whole numbers. Never a decimal, and it carries no currency.
2 fields of Rate
numerator
integer · int64
requiredThe top of the fraction, a whole number. 8 over 100 is eight percent.
denominator
integer · int64 · minimum 1
requiredThe bottom of the fraction, a whole number of 1 or more. Never zero, so every rate can be evaluated.
maker_checker_threshold
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
Returns
The settings, carrying updated_at once there is one, so you can record the change without reading them back.
object
always "payroll_settings"
requiredAlways payroll_settings. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm these settings are for. false is the sandbox and true is live; each realm keeps its own answers, so what you set in the sandbox changes nothing about your live payroll.
applicable
object
requiredWhat your organisation answers about the conditions a rule pack names, keyed by the condition. organisation_enrolled is the national health insurance scheme; employer_with_five_or_more_employees_or_turnover_at_or_above_5000000000 is the industrial training levy. Empty until you answer, and an element whose condition is unanswered stops a run with prerequisite_not_met rather than being left out quietly.
pension_employee_rate
Rate
optionalAn exact fraction of two whole numbers. Never a decimal, and it carries no currency.
2 fields of Rate
numerator
integer · int64
requiredThe top of the fraction, a whole number. 8 over 100 is eight percent.
denominator
integer · int64 · minimum 1
requiredThe bottom of the fraction, a whole number of 1 or more. Never zero, so every rate can be evaluated.
pension_employer_rate
Rate
optionalAn exact fraction of two whole numbers. Never a decimal, and it carries no currency.
2 fields of Rate
numerator
integer · int64
requiredThe top of the fraction, a whole number. 8 over 100 is eight percent.
denominator
integer · int64 · minimum 1
requiredThe bottom of the fraction, a whole number of 1 or more. Never zero, so every rate can be evaluated.
maker_checker_threshold
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
updated_at
string · date-time
optionalWhen these settings last changed, from the database's own clock.
Other responses
Errors it can return
curl -X PATCH "https://sandbox.droomwork.io/v1/payroll/settings" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"applicable":{},"pension_employee_rate":{"numerator":1,"denominator":1},"pension_employer_rate":{"numerator":1,"denominator":1},"maker_checker_threshold":{"amount":50000000,"currency":"NGN"}}'import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollSettingsUpdate({
idempotencyKey: crypto.randomUUID(),
runPayrollSettingsUpdateRequest: {"applicable":{},"pensionEmployeeRate":{"numerator":1,"denominator":1},"pensionEmployerRate":{"numerator":1,"denominator":1},"makerCheckerThreshold":{"amount":50000000,"currency":"NGN"}},
});const response = await fetch('https://sandbox.droomwork.io/v1/payroll/settings', {
method: 'PATCH',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"applicable": {},
"pension_employee_rate": {
"numerator": 1,
"denominator": 1
},
"pension_employer_rate": {
"numerator": 1,
"denominator": 1
},
"maker_checker_threshold": {
"amount": 50000000,
"currency": "NGN"
}
}),
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_settings_update(body={"applicable": {}, "pension_employee_rate": {"numerator": 1, "denominator": 1}, "pension_employer_rate": {"numerator": 1, "denominator": 1}, "maker_checker_threshold": {"amount": 50000000, "currency": "NGN"}})import os
import uuid
import requests
response = requests.request(
'PATCH',
'https://sandbox.droomwork.io/v1/payroll/settings',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"applicable": {}, "pension_employee_rate": {"numerator": 1, "denominator": 1}, "pension_employer_rate": {"numerator": 1, "denominator": 1}, "maker_checker_threshold": {"amount": 50000000, "currency": "NGN"}},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollSettingsUpdate($idempotencyKey, json_decode('{"applicable":{},"pension_employee_rate":{"numerator":1,"denominator":1},"pension_employer_rate":{"numerator":1,"denominator":1},"maker_checker_threshold":{"amount":50000000,"currency":"NGN"}}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/settings');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
'Content-Type: application/json',
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
],
CURLOPT_POSTFIELDS => '{"applicable":{},"pension_employee_rate":{"numerator":1,"denominator":1},"pension_employer_rate":{"numerator":1,"denominator":1},"maker_checker_threshold":{"amount":50000000,"currency":"NGN"}}',
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollSettingsUpdate(idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/settings"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.header("Content-Type", "application/json")
.header("Idempotency-Key", UUID.randomUUID().toString())
.method("PATCH", HttpRequest.BodyPublishers.ofString("""
{
"applicable": {},
"pension_employee_rate": {
"numerator": 1,
"denominator": 1
},
"pension_employer_rate": {
"numerator": 1,
"denominator": 1
},
"maker_checker_threshold": {
"amount": 50000000,
"currency": "NGN"
}
}
"""))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollSettingsUpdate(idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Patch, "https://sandbox.droomwork.io/v1/payroll/settings");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"applicable": {},
"pension_employee_rate": {
"numerator": 1,
"denominator": 1
},
"pension_employer_rate": {
"numerator": 1,
"denominator": 1
},
"maker_checker_threshold": {
"amount": 50000000,
"currency": "NGN"
}
}
""", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollSettingsUpdate(ctx).IdempotencyKey(key).RunPayrollSettingsUpdateRequest(body).Execute()body := strings.NewReader(`{
"applicable": {},
"pension_employee_rate": {
"numerator": 1,
"denominator": 1
},
"pension_employer_rate": {
"numerator": 1,
"denominator": 1
},
"maker_checker_threshold": {
"amount": 50000000,
"currency": "NGN"
}
}`)
req, _ := http.NewRequest("PATCH", "https://sandbox.droomwork.io/v1/payroll/settings", 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)
{
"object": "payroll_settings",
"livemode": false,
"applicable": {},
"pension_employee_rate": {
"numerator": 1,
"denominator": 1
},
"pension_employer_rate": {
"numerator": 1,
"denominator": 1
},
"maker_checker_threshold": {
"amount": 50000000,
"currency": "NGN"
},
"updated_at": "2026-09-01T09:00:00Z"
}
/v1/pay_profiles/{pay_profile_id}#Check one version of a pay profile
payroll.pay_profiles.retrieve
A pay profile version is one person's pay terms fixed from a date: base salary, allowances and how often they are paid. This reads one version back, so a page in your product can show what somebody is paid now, and a superseded version what they were paid before.
Send one version's id in the path, from POST /v1/pay_profiles or the pay_profile_id on a roster entry. You get that version and the dates it applies between.
The id names one version, not the whole arrangement. Walk back with supersedes, which names the version this one replaced; version 1 has nothing before it.
The version with no effective_to is the latest one, and the only one PATCH /v1/pay_profiles/{pay_profile_id} will supersede; any other is refused.
Pass the subject_id to GET /v1/pay_profiles for every profile and version this person has. Another organisation's id answers not_found, not forbidden.
Path parameters
pay_profile_id
string
requiredThe pay profile's identifier: the id from POST /v1/pay_profiles or GET /v1/pay_profiles. It starts with run_enterprise_pay_profile_ and names one version of the profile.
Returns
The profile version, and nothing on it says when you recorded it: effective_from is a date you chose.
id
string
requiredThe identifier of this profile version. It starts with run_enterprise_pay_profile_, is assigned at POST /v1/pay_profiles or PATCH /v1/pay_profiles/{pay_profile_id} and never changes; pass it as pay_profile_id to name this version.
object
always "pay_profile"
requiredAlways pay_profile. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
subject_id
string
requiredThe worker this profile pays: their subject identifier, starting with sub_, as you sent it at POST /v1/pay_profiles. Every version of the profile carries the same one.
base_salary
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
pay_frequency
string
optionalHow often the worker is paid: monthly (once a month), semi_monthly (twice a month) or weekly (once a week). It's what you set at POST /v1/pay_profiles; a new version keeps it.
monthlysemi_monthlyweeklyallowances
array of object
optionalFixed allowances on top of base salary, each as a code and an amount. Empty or absent when the worker has none.
2 fields
code
string
requiredYour own code for this allowance, as you sent it on the create or supersede call. Keep it the same on every version so you can follow one allowance over time.
amount
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
effective_from
string · date
requiredThe first day this version applies, as a calendar date in YYYY-MM-DD. A payslip for an earlier period computes from the version in force then.
effective_to
string · date · nullable
optionalThe last day this version applies, as a calendar date in YYYY-MM-DD, or null while it's current. A leaver is an end date, not a deletion.
version
integer · minimum 1
requiredWhich version of the worker's profile this is, counting from 1. Superseding it creates the next; earlier versions stay listed so you can explain an old payslip.
supersedes
string · nullable
optionalThe id of the version this one replaced, or null for the first version.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/pay_profiles/run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollPayProfilesRetrieve({ payProfileId: 'run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/pay_profiles/run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_pay_profiles_retrieve(pay_profile_id='run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/pay_profiles/run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollPayProfilesRetrieve(pay_profile_id: 'run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/pay_profiles/run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollPayProfilesRetrieve("run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/pay_profiles/run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollPayProfilesRetrieve(payProfileId: "run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/pay_profiles/run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollPayProfilesRetrieve(ctx, "run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/pay_profiles/run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "pay_profile",
"livemode": false,
"mocked": true,
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"base_salary": {
"amount": 45000000,
"currency": "NGN"
},
"effective_from": "2026-09-01",
"version": 1,
"pay_frequency": "monthly",
"allowances": [
{
"code": "pension_employee",
"amount": {
"amount": 7500000,
"currency": "NGN"
}
}
],
"effective_to": "2026-09-01",
"supersedes": "example"
}
/v1/pay_profiles/{pay_profile_id}#Supersede a pay profile with a new version
payroll.pay_profiles.update
This records a pay change as a new version of somebody's pay profile. So your product can give a raise or change an allowance without disturbing anything already paid.
Send the profile's id in the path with the new terms and the date they start, after the current version's first day. A profile has to exist, from POST /v1/pay_profiles.
Leave base_salary out to keep the current salary. Send allowances as the whole list to pay, including the ones that did not change; an empty list pays none.
The version you supersede is closed the day before yours, so a payslip computed under it still explains itself. Somebody leaving is an end date on their roster entry, at PATCH /v1/payroll/roster_entries/{roster_entry_id}. It pays them to that date and does not remove the entry.
You get the new version back under its own id. A version already superseded is refused with conflict.
Path parameters
pay_profile_id
string
requiredThe pay profile's identifier: the id from POST /v1/pay_profiles or GET /v1/pay_profiles. It starts with run_enterprise_pay_profile_ and names one version of the profile.
Headers
Idempotency-Key
string
requiredA key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.
Body
base_salary
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
allowances
array of object
optionalThe allowances the new version pays on top of base_salary, each as a code and an amount in whole minor units. Send the whole list the new version should pay, not only the ones that changed.
2 fields
code
string
requiredYour code for this allowance, such as housing or transport. Keep the code the earlier version used so the allowance reads as the same one across versions.
amount
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
effective_from
string · date
requiredWhen the new version takes effect.
Returns
The new version, which keeps the pay_frequency the earlier one paid: this call cannot change it.
id
string
requiredThe identifier of this profile version. It starts with run_enterprise_pay_profile_, is assigned at POST /v1/pay_profiles or PATCH /v1/pay_profiles/{pay_profile_id} and never changes; pass it as pay_profile_id to name this version.
object
always "pay_profile"
requiredAlways pay_profile. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
subject_id
string
requiredThe worker this profile pays: their subject identifier, starting with sub_, as you sent it at POST /v1/pay_profiles. Every version of the profile carries the same one.
base_salary
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
pay_frequency
string
optionalHow often the worker is paid: monthly (once a month), semi_monthly (twice a month) or weekly (once a week). It's what you set at POST /v1/pay_profiles; a new version keeps it.
monthlysemi_monthlyweeklyallowances
array of object
optionalFixed allowances on top of base salary, each as a code and an amount. Empty or absent when the worker has none.
2 fields
code
string
requiredYour own code for this allowance, as you sent it on the create or supersede call. Keep it the same on every version so you can follow one allowance over time.
amount
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
effective_from
string · date
requiredThe first day this version applies, as a calendar date in YYYY-MM-DD. A payslip for an earlier period computes from the version in force then.
effective_to
string · date · nullable
optionalThe last day this version applies, as a calendar date in YYYY-MM-DD, or null while it's current. A leaver is an end date, not a deletion.
version
integer · minimum 1
requiredWhich version of the worker's profile this is, counting from 1. Superseding it creates the next; earlier versions stay listed so you can explain an old payslip.
supersedes
string · nullable
optionalThe id of the version this one replaced, or null for the first version.
Other responses
Errors it can return
curl -X PATCH "https://sandbox.droomwork.io/v1/pay_profiles/run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"effective_from":"2026-09-01","base_salary":{"amount":45000000,"currency":"NGN"},"allowances":[{"code":"housing","amount":{"amount":7500000,"currency":"NGN"}}]}'import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollPayProfilesUpdate({
payProfileId: 'run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
idempotencyKey: crypto.randomUUID(),
runPayProfileUpdateRequest: {"effectiveFrom":"2026-09-01","baseSalary":{"amount":45000000,"currency":"NGN"},"allowances":[{"code":"housing","amount":{"amount":7500000,"currency":"NGN"}}]},
});const response = await fetch('https://sandbox.droomwork.io/v1/pay_profiles/run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', {
method: 'PATCH',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"effective_from": "2026-09-01",
"base_salary": {
"amount": 45000000,
"currency": "NGN"
},
"allowances": [
{
"code": "housing",
"amount": {
"amount": 7500000,
"currency": "NGN"
}
}
]
}),
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_pay_profiles_update(pay_profile_id='run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"effective_from": "2026-09-01", "base_salary": {"amount": 45000000, "currency": "NGN"}, "allowances": [{"code": "housing", "amount": {"amount": 7500000, "currency": "NGN"}}]})import os
import uuid
import requests
response = requests.request(
'PATCH',
'https://sandbox.droomwork.io/v1/pay_profiles/run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"effective_from": "2026-09-01", "base_salary": {"amount": 45000000, "currency": "NGN"}, "allowances": [{"code": "housing", "amount": {"amount": 7500000, "currency": "NGN"}}]},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollPayProfilesUpdate($idempotencyKey, json_decode('{"effective_from":"2026-09-01","base_salary":{"amount":45000000,"currency":"NGN"},"allowances":[{"code":"housing","amount":{"amount":7500000,"currency":"NGN"}}]}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/pay_profiles/run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
'Content-Type: application/json',
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
],
CURLOPT_POSTFIELDS => '{"effective_from":"2026-09-01","base_salary":{"amount":45000000,"currency":"NGN"},"allowances":[{"code":"housing","amount":{"amount":7500000,"currency":"NGN"}}]}',
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollPayProfilesUpdate("run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/pay_profiles/run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.header("Content-Type", "application/json")
.header("Idempotency-Key", UUID.randomUUID().toString())
.method("PATCH", HttpRequest.BodyPublishers.ofString("""
{
"effective_from": "2026-09-01",
"base_salary": {
"amount": 45000000,
"currency": "NGN"
},
"allowances": [
{
"code": "housing",
"amount": {
"amount": 7500000,
"currency": "NGN"
}
}
]
}
"""))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollPayProfilesUpdate(payProfileId: "run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Patch, "https://sandbox.droomwork.io/v1/pay_profiles/run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"effective_from": "2026-09-01",
"base_salary": {
"amount": 45000000,
"currency": "NGN"
},
"allowances": [
{
"code": "housing",
"amount": {
"amount": 7500000,
"currency": "NGN"
}
}
]
}
""", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollPayProfilesUpdate(ctx, "run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).RunPayProfileUpdateRequest(body).Execute()body := strings.NewReader(`{
"effective_from": "2026-09-01",
"base_salary": {
"amount": 45000000,
"currency": "NGN"
},
"allowances": [
{
"code": "housing",
"amount": {
"amount": 7500000,
"currency": "NGN"
}
}
]
}`)
req, _ := http.NewRequest("PATCH", "https://sandbox.droomwork.io/v1/pay_profiles/run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "pay_profile",
"livemode": false,
"mocked": true,
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"base_salary": {
"amount": 45000000,
"currency": "NGN"
},
"effective_from": "2026-09-01",
"version": 1,
"pay_frequency": "monthly",
"allowances": [
{
"code": "pension_employee",
"amount": {
"amount": 7500000,
"currency": "NGN"
}
}
],
"effective_to": "2026-09-01",
"supersedes": "example"
}
/v1/payroll/roster_entries#Add someone to the roster
payroll.roster_entries.create
This puts somebody on the roster for payroll. So your product can set a joiner up before any run is opened, and show what is still missing: an entry with no pay profile is not payable, as no_pay_profile.
Send the person as subject_id and their first day on the roster as effective_from, with pay_profile_id and engagement_id when you hold them. You get the entry back with its id, which POST /v1/payroll/runs takes in roster_entry_ids.
You don't need a pay profile yet. A pay_profile_id you name has to be a current version from POST /v1/pay_profiles: a superseded one is refused.
Being on the roster is not being paid. The engagement can only be set here: attach the one from POST /v1/rail/engagements. Without it, POST /v1/payroll/runs/{run_id}/execute is refused with prerequisite_not_met and nothing is paid.
Headers
Idempotency-Key
string
requiredA key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.
Body
subject_id
string
requiredThe person to add: the subject_ref (sub_…) you first sent at POST /v1/identity/consent_tokens, the same subject_id as on their pay profile. Being on the roster doesn't make them payable; read payable on the entry you get back.
engagement_id
string
optionalThe engagement this person is paid under: its id, starting with rail_engagement_, from POST /v1/rail/engagements. Optional now, but the payment gate needs a live one at execution; read blocked_by on the entry you get back.
pay_profile_id
string
optionalThe pay profile to pay this person from: its id, starting with run_enterprise_pay_profile_, from POST /v1/pay_profiles or GET /v1/pay_profiles. Leave it out to attach one later; until then the entry reports no_pay_profile.
effective_from
string · date
requiredThe first date this person is on the roster, as a calendar date in YYYY-MM-DD. It can't be changed afterwards; an update changes the pay profile or sets an end date.
Returns
The entry as created, whose subject_id is what ties this person to their pay profile and payslips.
id
string
requiredThe entry's identifier, from POST /v1/payroll/roster_entries. It starts with run_enterprise_roster_entry_ and never changes; pass it as roster_entry_id on this entry's calls and in roster_entry_ids at POST /v1/payroll/runs.
object
always "roster_entry"
requiredAlways roster_entry. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
subject_id
string
requiredThe person on this entry: their subject identifier, starting with sub_, as you sent it at POST /v1/payroll/roster_entries. Their pay profile and payslips carry the same subject_id; it can't change once the entry exists.
engagement_id
string · nullable
optionalThe engagement this person is paid under: its id, starting with rail_engagement_, from POST /v1/rail/engagements. null until you attach one; the payment gate needs a live one, and blocked_by then says no_live_engagement.
pay_profile_id
string · nullable
optionalThe pay profile this person is paid from: its id, starting with run_enterprise_pay_profile_, from POST /v1/pay_profiles. null until you attach one; without it the entry isn't payable and blocked_by carries no_pay_profile.
payable
boolean
requiredWhether this person currently satisfies the payment gate. False doesn't mean the record is wrong; something they need is missing, and blocked_by says what.
contact_email
string · email · nullable
optionalWhere this person's payslip is sent, or null if you have not given us one. We will not pay somebody we cannot reach: a roster entry without this is not payable and says so in blocked_by as no_contact_address. Set it when you add them, or later at PATCH /v1/payroll/roster_entries/{roster_entry_id}. It is the same person's address at every employer who uses it, so two employers paying the same person each hold their own.
blocked_by
array of string
optionalEmpty when payable is true.
effective_from
string · date
requiredThe first date this person is on the roster, as a calendar date in YYYY-MM-DD, as you set it when you added them.
effective_to
string · date · nullable
optionalThe last date this person is on the roster, as YYYY-MM-DD, or null while they have no end date. A leaver is an end date, set at PATCH /v1/payroll/roster_entries/{roster_entry_id}; there is no delete.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/payroll/roster_entries" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"subject_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","effective_from":"2026-09-01","engagement_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","pay_profile_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}'import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollRosterEntriesCreate({
idempotencyKey: crypto.randomUUID(),
runRosterEntryCreateRequest: {"subjectId":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","effectiveFrom":"2026-09-01","engagementId":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","payProfileId":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"},
});const response = await fetch('https://sandbox.droomwork.io/v1/payroll/roster_entries', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"effective_from": "2026-09-01",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"pay_profile_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}),
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_roster_entries_create(body={"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "effective_from": "2026-09-01", "engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "pay_profile_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"})import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/payroll/roster_entries',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "effective_from": "2026-09-01", "engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "pay_profile_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollRosterEntriesCreate($idempotencyKey, json_decode('{"subject_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","effective_from":"2026-09-01","engagement_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","pay_profile_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/roster_entries');
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 => '{"subject_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","effective_from":"2026-09-01","engagement_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","pay_profile_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}',
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollRosterEntriesCreate(idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/roster_entries"))
.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("""
{
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"effective_from": "2026-09-01",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"pay_profile_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
"""))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollRosterEntriesCreate(idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payroll/roster_entries");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"effective_from": "2026-09-01",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"pay_profile_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
""", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollRosterEntriesCreate(ctx).IdempotencyKey(key).RunRosterEntryCreateRequest(body).Execute()body := strings.NewReader(`{
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"effective_from": "2026-09-01",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"pay_profile_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payroll/roster_entries", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_roster_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "roster_entry",
"livemode": false,
"mocked": true,
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"payable": true,
"effective_from": "2026-09-01",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"pay_profile_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"contact_email": "example",
"blocked_by": [
"no_anchored_identity"
],
"effective_to": "2026-09-01"
}
/v1/payroll/imports/{import_id}#Read one import back with its report
payroll.imports.retrieve
This reads one import back with its report, so you can return to an upload after the person who sent it has gone. Your product can put the report in front of whoever has to fix the file, without uploading it again.
Send the import's id in the path. You get the kind of file it was, counts of entries, errors and warnings, and one entry per row that failed or raised a warning.
The report does not carry the rows themselves, so keep the file you sent.
An import and its report are not people on your roster. Add them at POST /v1/payroll/roster_entries. A corrected file goes to POST /v1/payroll/imports.
An id from another organisation answers not_found, not forbidden. See every one at GET /v1/payroll/imports.
Path parameters
import_id
string
requiredThe import's identifier: the id from POST /v1/payroll/imports or GET /v1/payroll/imports. It starts with run_enterprise_import_.
Returns
The import and its report, where code is what to branch on and detail is wording that may change.
id
string
requiredThe import's identifier, from POST /v1/payroll/imports. It starts with run_enterprise_import_ and never changes; pass it as import_id at GET /v1/payroll/imports/{import_id} to read the report again.
object
always "payroll_import"
requiredAlways payroll_import. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
kind
string
requiredWhat the file holds, as you sent it as kind at POST /v1/payroll/imports: roster, the people you pay, or pay_profiles, how each of them is paid. It decides how every row is checked and what an accepted import creates.
rosterpay_profilesstatus
string
requiredvalidating while rows are checked, pending_acceptance once the report is ready for you to read, accepted once the rows are committed, rejected when they won't be. Nothing is committed before accepted.
validatingpending_acceptanceacceptedrejectedvalidation_report
ValidationReport
requiredThe result of checking a submission before anything is committed. Names every failing entry rather than stopping at the first, so one submission tells you everything to fix. The same shape wherever the platform ingests, validates and reports, which is every bulk intake in every module.
4 fields of ValidationReport
entry_count
integer · minimum 0
requiredHow many entries the submission held: every row in the file or line in the instruction set, whether it passed or not.
error_count
integer · minimum 0
requiredHow many entries failed with an error. Each is named in entries with its position and why, so one submission tells you everything to fix.
warning_count
integer · minimum 0
requiredHow many entries passed with a warning, each named in entries. A warning does not fail the entry; it points at something to confirm before you go ahead.
entries
array of object
optionalOne item per error or warning found, with the row or line it sits on, its severity, a code and what was wrong. Empty or absent when every entry passed clean.
4 fields
index
integer · minimum 1
requiredThe row in a file, or the line in an instruction set.
severity
string
requirederror means the entry failed and must be fixed. warning means it passed, but points at something you should confirm before you go ahead.
errorwarningcode
string
requiredWhy the entry was flagged, as a code you can branch on. detail says the same in words and may change; the code does not.
detail
string
requiredWhat was wrong with the entry, in plain words. Show it beside the row; it may change, so branch on code.
created_at
string · date-time
optionalWhen the record was created, as an RFC 3339 timestamp in UTC.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/payroll/imports/%7Bimport_id%7D" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollImportsRetrieve({ importId: '{import_id}' });const response = await fetch('https://sandbox.droomwork.io/v1/payroll/imports/%7Bimport_id%7D', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_imports_retrieve(import_id='{import_id}')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/imports/%7Bimport_id%7D',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollImportsRetrieve(import_id: '{import_id}');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/imports/%7Bimport_id%7D');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollImportsRetrieve("{import_id}");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/imports/%7Bimport_id%7D"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollImportsRetrieve(importId: "{import_id}");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/imports/%7Bimport_id%7D");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollImportsRetrieve(ctx, "{import_id}").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/imports/%7Bimport_id%7D", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_import_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "payroll_import",
"livemode": false,
"mocked": true,
"kind": "roster",
"status": "validating",
"validation_report": {
"entry_count": 0,
"error_count": 0,
"warning_count": 0,
"entries": [
{
"index": 1,
"severity": "error",
"code": "pension_employee",
"detail": "The payee has no verified destination, so this line cannot be paid."
}
]
},
"created_at": "2026-09-01T09:00:00Z"
}
/v1/payroll/runs#Open a payroll run for a period
payroll.runs.create
A payroll run is the record one pay period's pay is produced under. The figures, the sign-off and every payslip hang off its identifier.
Send the period and the kind of run: regular payroll, a bonus, off-cycle or thirteenth-month payment, a correction to a period already paid, or a final settlement. You get the run back in draft.
Each payee needs a pay profile and a roster entry first, from POST /v1/pay_profiles and POST /v1/payroll/roster_entries. Neither is checked here: the run opens over whatever the roster holds, and a missing profile is refused with prerequisite_not_met when you simulate.
Name roster_entry_ids to pay only certain people, or rule_pack_version to pin a different pack.
Read payee_count: a run covering nobody is accepted here and refused with conflict at execute. Simulate at POST /v1/payroll/runs/{run_id}/simulate, passing the id as run_id.
Headers
Idempotency-Key
string
requiredA key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.
Body
period
string
requiredThe pay period the run covers, as year and month in YYYY-MM form, for example 2026-09. It decides which roster entries and which rule pack apply unless you name them.
run_type
string
requiredFixed at creation and immutable afterwards.
regularoff_cyclebonusthirteenth_monthcorrectionfinal_settlementrule_pack_version
string
optionalDefaults to the pack in force for the period, such as 2026.01.1. Name one only to compute against a pack that is not in force, which simulate allows and execute does not.
roster_entry_ids
array of string
optionalDefaults to every payable roster entry for the period. Each is a roster entry identifier from POST /v1/payroll/roster_entries, such as run_enterprise_roster_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z; naming the same one twice is refused, because a run pays a person once.
Returns
The new run, created_at included, so you can store when it was opened without listing your runs.
id
string
requiredThe run's identifier. It starts with run_enterprise_, is assigned when you create the run at POST /v1/payroll/runs and never changes; pass it as run_id wherever a call names this run.
object
always "payroll_run"
requiredAlways payroll_run. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
status
string
requiredYou move a run between states with endpoints, not by setting this field.
draftsimulatedpending_approvalapprovedexecutingcompletedpartially_settledfailedcancelledrun_type
string
requiredFixed at creation and immutable afterwards.
regularoff_cyclebonusthirteenth_monthcorrectionfinal_settlementperiod
string
requiredThe pay period this run covers, as year and month in YYYY-MM form, for example 2026-09.
payee_count
integer · minimum 0
optionalHow many payees are on this run, one for each roster entry it covers. A whole number, 0 or more.
rule_pack
RulePackReference
optional3 fields of RulePackReference
pack_id
string
requiredThe family of rules the pack belongs to, such as ng-payroll. It's pinned when you create the run at POST /v1/payroll/runs and, with version, names exactly which rates, bands and thresholds were applied.
version
string
requiredThe pack version that was applied, in the form 2026.08.1. A version never changes once active, so the same version always means the same rules.
content_hash
string
requiredA digest of the pack's content, prefixed sha256:. It proves which exact rules produced the figure, so an edited pack can never pass as the one you ran under.
totals
RunTotals
optionalEach figure is the sum of its payslip lines, never a percentage of an aggregate.
5 fields of RunTotals
gross
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
net
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
total_deductions
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
employer_contributions
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
payslip_count
integer · minimum 0
optionalHow many payslips these totals are summed from. On a simulation, how many the run would produce; nothing is written.
prepared_by
string · nullable
optionalWho prepared this run, as their user identifier. Above the configured maker-checker threshold the same person can't approve it, so their approve call is refused.
approved_by
string · nullable
optionalNever the same actor as prepared_by above the maker-checker threshold.
created_at
string · date-time
requiredWhen the record was created, as an RFC 3339 timestamp in UTC.
executed_at
string · date-time · nullable
optionalWhen the run was executed, as an RFC 3339 timestamp in UTC. null until it has been.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/payroll/runs" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"period":"2026-09","run_type":"regular","rule_pack_version":"2026.01.1","roster_entry_ids":["run_enterprise_roster_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"]}'import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollRunsCreate({
idempotencyKey: crypto.randomUUID(),
runRunCreateRequest: {"period":"2026-09","runType":"regular","rulePackVersion":"2026.01.1","rosterEntryIds":["run_enterprise_roster_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"]},
});const response = await fetch('https://sandbox.droomwork.io/v1/payroll/runs', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"period": "2026-09",
"run_type": "regular",
"rule_pack_version": "2026.01.1",
"roster_entry_ids": [
"run_enterprise_roster_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
]
}),
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_runs_create(body={"period": "2026-09", "run_type": "regular", "rule_pack_version": "2026.01.1", "roster_entry_ids": ["run_enterprise_roster_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"]})import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/payroll/runs',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"period": "2026-09", "run_type": "regular", "rule_pack_version": "2026.01.1", "roster_entry_ids": ["run_enterprise_roster_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"]},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollRunsCreate($idempotencyKey, json_decode('{"period":"2026-09","run_type":"regular","rule_pack_version":"2026.01.1","roster_entry_ids":["run_enterprise_roster_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"]}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/runs');
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 => '{"period":"2026-09","run_type":"regular","rule_pack_version":"2026.01.1","roster_entry_ids":["run_enterprise_roster_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"]}',
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollRunsCreate(idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/runs"))
.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("""
{
"period": "2026-09",
"run_type": "regular",
"rule_pack_version": "2026.01.1",
"roster_entry_ids": [
"run_enterprise_roster_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
]
}
"""))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollRunsCreate(idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payroll/runs");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"period": "2026-09",
"run_type": "regular",
"rule_pack_version": "2026.01.1",
"roster_entry_ids": [
"run_enterprise_roster_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
]
}
""", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollRunsCreate(ctx).IdempotencyKey(key).RunRunCreateRequest(body).Execute()body := strings.NewReader(`{
"period": "2026-09",
"run_type": "regular",
"rule_pack_version": "2026.01.1",
"roster_entry_ids": [
"run_enterprise_roster_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
]
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payroll/runs", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "payroll_run",
"livemode": false,
"mocked": true,
"status": "draft",
"run_type": "regular",
"period": "2026-09",
"created_at": "2026-09-01T09:00:00Z",
"payee_count": 0,
"rule_pack": {
"pack_id": "ng-payroll",
"version": "2026.08.1",
"content_hash": "sha256:9f2c1e00"
},
"totals": {
"gross": {
"amount": 52500000,
"currency": "NGN"
},
"net": {
"amount": 41250000,
"currency": "NGN"
},
"total_deductions": {
"amount": 11250000,
"currency": "NGN"
},
"employer_contributions": {
"amount": 5400000,
"currency": "NGN"
},
"payslip_count": 0
},
"prepared_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"approved_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"executed_at": "2026-09-01T09:00:00Z"
}
/v1/payroll/roster_entries#Who is on payroll, and who is ready
payroll.roster_entries.list
This gives you the people on your payroll roster, one row each. So your product can carry a people screen for payroll, showing who is set up to be paid and who is still missing something.
Send nothing to get everybody, or payable to narrow to one side of it. Each entry names the person, the dates they are on the roster, and anything standing in the way of paying them.
pay_profile_id names the profile they are paid under and is null until you attach one at PATCH /v1/payroll/roster_entries/{roster_entry_id}. Revising that profile does not move it, so read the terms in force at GET /v1/pay_profiles.
Nobody is deleted from the roster. Somebody who has left keeps their entry and gets an end date in effective_to, so read it before you treat them as current.
Read one at GET /v1/payroll/roster_entries/{roster_entry_id}, and carry each id into roster_entry_ids at POST /v1/payroll/runs.
Query parameters
limit
integer
optionalHow many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.
starting_after
string
optionalThe id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.
payable
boolean
optionalPass true to get only the entries that currently satisfy the payment gate. Leave it out to get every entry, payable or not.
Returns
A page of roster entries, where what stops a payment comes back as a code and not as prose.
object
always "list"
requiredAlways list. Tells you which kind of record you are looking at, so one handler can read any response.
data
array of RosterEntry
requiredThe records on this page, in the order the list promises. Empty when nothing matched.
12 fields of RosterEntry
id
string
requiredThe entry's identifier, from POST /v1/payroll/roster_entries. It starts with run_enterprise_roster_entry_ and never changes; pass it as roster_entry_id on this entry's calls and in roster_entry_ids at POST /v1/payroll/runs.
object
always "roster_entry"
requiredAlways roster_entry. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
subject_id
string
requiredThe person on this entry: their subject identifier, starting with sub_, as you sent it at POST /v1/payroll/roster_entries. Their pay profile and payslips carry the same subject_id; it can't change once the entry exists.
engagement_id
string · nullable
optionalThe engagement this person is paid under: its id, starting with rail_engagement_, from POST /v1/rail/engagements. null until you attach one; the payment gate needs a live one, and blocked_by then says no_live_engagement.
pay_profile_id
string · nullable
optionalThe pay profile this person is paid from: its id, starting with run_enterprise_pay_profile_, from POST /v1/pay_profiles. null until you attach one; without it the entry isn't payable and blocked_by carries no_pay_profile.
payable
boolean
requiredWhether this person currently satisfies the payment gate. False doesn't mean the record is wrong; something they need is missing, and blocked_by says what.
contact_email
string · email · nullable
optionalWhere this person's payslip is sent, or null if you have not given us one. We will not pay somebody we cannot reach: a roster entry without this is not payable and says so in blocked_by as no_contact_address. Set it when you add them, or later at PATCH /v1/payroll/roster_entries/{roster_entry_id}. It is the same person's address at every employer who uses it, so two employers paying the same person each hold their own.
blocked_by
array of string
optionalEmpty when payable is true.
effective_from
string · date
requiredThe first date this person is on the roster, as a calendar date in YYYY-MM-DD, as you set it when you added them.
effective_to
string · date · nullable
optionalThe last date this person is on the roster, as YYYY-MM-DD, or null while they have no end date. A leaver is an end date, set at PATCH /v1/payroll/roster_entries/{roster_entry_id}; there is no delete.
has_more
boolean
requiredtrue when there are more records after this page. Pass the last record's id as starting_after to get the next page.
Other responses
Errors it can return
# query parameters: limit (optional), starting_after (optional), payable (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payroll/roster_entries?limit=25&payable=true" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
// query parameters: limit (optional), starting_after (optional), payable (optional)
const result = await api.payrollRosterEntriesList({ limit: 25, payable: true });// query parameters: limit (optional), starting_after (optional), payable (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/payroll/roster_entries?limit=25&payable=true', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
# query parameters: limit (optional), starting_after (optional), payable (optional)
result = api.payroll_roster_entries_list(limit=25, payable=true)import os
import requests
# query parameters: limit (optional), starting_after (optional), payable (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/roster_entries?limit=25&payable=true',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
# query parameters: limit (optional), starting_after (optional), payable (optional)
$result = $api->payrollRosterEntriesList(limit: 25, payable: true);<?php
// query parameters: limit (optional), starting_after (optional), payable (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/roster_entries?limit=25&payable=true');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
// query parameters: limit (optional), starting_after (optional), payable (optional)
var result = api.payrollRosterEntriesList(25, null, true);// query parameters: limit (optional), starting_after (optional), payable (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/roster_entries?limit=25&payable=true"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
// query parameters: limit (optional), starting_after (optional), payable (optional)
var result = api.PayrollRosterEntriesList(limit: 25, payable: true);// query parameters: limit (optional), starting_after (optional), payable (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/roster_entries?limit=25&payable=true");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: limit (optional), starting_after (optional), payable (optional)
result, _, err := client.RUNAPI.PayrollRosterEntriesList(ctx).Limit(25).Payable(true).Execute()// query parameters: limit (optional), starting_after (optional), payable (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/roster_entries?limit=25&payable=true", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"object": "list",
"data": [
{
"id": "run_enterprise_roster_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "roster_entry",
"livemode": false,
"mocked": true,
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"payable": true,
"effective_from": "2026-09-01",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"pay_profile_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"contact_email": "example",
"blocked_by": [
"no_anchored_identity"
],
"effective_to": "2026-09-01"
}
],
"has_more": true
}
/v1/payroll/roster_entries/{roster_entry_id}#Check one roster entry
payroll.roster_entries.retrieve
A roster entry is one person's place on your payroll: the profile that pays them, the engagement they are paid under, and what the payment gate last found missing. So your product can show the person a run refused and the reason it recorded.
Send the entry's id in the path. You get the person's subject_id, the pay_profile_id their pay is computed from, the dates it runs between, and payable with its reasons. Read the pay itself at GET /v1/pay_profiles/{pay_profile_id}.
payable and blocked_by hold the gate's last answer, not one worked out as you read. Before a run executes they say only whether a profile is attached; the identity and engagement checks are made at execution and recorded here.
Pass this id in roster_entry_ids at POST /v1/payroll/runs. Change the profile or set an end date at PATCH /v1/payroll/roster_entries/{roster_entry_id}; there is no delete. Another organisation's id answers not_found.
Path parameters
roster_entry_id
string
requiredThe roster entry's identifier: the id from POST /v1/payroll/roster_entries or GET /v1/payroll/roster_entries. It starts with run_enterprise_roster_entry_.
Returns
The roster entry, including why it is or is not payable.
id
string
requiredThe entry's identifier, from POST /v1/payroll/roster_entries. It starts with run_enterprise_roster_entry_ and never changes; pass it as roster_entry_id on this entry's calls and in roster_entry_ids at POST /v1/payroll/runs.
object
always "roster_entry"
requiredAlways roster_entry. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
subject_id
string
requiredThe person on this entry: their subject identifier, starting with sub_, as you sent it at POST /v1/payroll/roster_entries. Their pay profile and payslips carry the same subject_id; it can't change once the entry exists.
engagement_id
string · nullable
optionalThe engagement this person is paid under: its id, starting with rail_engagement_, from POST /v1/rail/engagements. null until you attach one; the payment gate needs a live one, and blocked_by then says no_live_engagement.
pay_profile_id
string · nullable
optionalThe pay profile this person is paid from: its id, starting with run_enterprise_pay_profile_, from POST /v1/pay_profiles. null until you attach one; without it the entry isn't payable and blocked_by carries no_pay_profile.
payable
boolean
requiredWhether this person currently satisfies the payment gate. False doesn't mean the record is wrong; something they need is missing, and blocked_by says what.
contact_email
string · email · nullable
optionalWhere this person's payslip is sent, or null if you have not given us one. We will not pay somebody we cannot reach: a roster entry without this is not payable and says so in blocked_by as no_contact_address. Set it when you add them, or later at PATCH /v1/payroll/roster_entries/{roster_entry_id}. It is the same person's address at every employer who uses it, so two employers paying the same person each hold their own.
blocked_by
array of string
optionalEmpty when payable is true.
effective_from
string · date
requiredThe first date this person is on the roster, as a calendar date in YYYY-MM-DD, as you set it when you added them.
effective_to
string · date · nullable
optionalThe last date this person is on the roster, as YYYY-MM-DD, or null while they have no end date. A leaver is an end date, set at PATCH /v1/payroll/roster_entries/{roster_entry_id}; there is no delete.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/payroll/roster_entries/%7Broster_entry_id%7D" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollRosterEntriesRetrieve({ rosterEntryId: '{roster_entry_id}' });const response = await fetch('https://sandbox.droomwork.io/v1/payroll/roster_entries/%7Broster_entry_id%7D', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_roster_entries_retrieve(roster_entry_id='{roster_entry_id}')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/roster_entries/%7Broster_entry_id%7D',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollRosterEntriesRetrieve(roster_entry_id: '{roster_entry_id}');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/roster_entries/%7Broster_entry_id%7D');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollRosterEntriesRetrieve("{roster_entry_id}");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/roster_entries/%7Broster_entry_id%7D"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollRosterEntriesRetrieve(rosterEntryId: "{roster_entry_id}");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/roster_entries/%7Broster_entry_id%7D");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollRosterEntriesRetrieve(ctx, "{roster_entry_id}").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/roster_entries/%7Broster_entry_id%7D", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_roster_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "roster_entry",
"livemode": false,
"mocked": true,
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"payable": true,
"effective_from": "2026-09-01",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"pay_profile_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"contact_email": "example",
"blocked_by": [
"no_anchored_identity"
],
"effective_to": "2026-09-01"
}
/v1/payroll/roster_entries/{roster_entry_id}#Attach a pay profile, or record a leaver
payroll.roster_entries.update
This attaches the pay profile a roster entry is paid from, moves it to another, or closes it with an end date. So your product can finish setting somebody up, or record a leaver with past payslips untouched.
Send the entry's id in the path with pay_profile_id, effective_to or both, and the entry comes back carrying payable and blocked_by. An entry has to exist, from POST /v1/payroll/roster_entries.
Send pay_profile_id to clear no_pay_profile from an entry created without one. A raise is not this call: new terms are a new version at PATCH /v1/pay_profiles/{pay_profile_id}, and this entry needs no change.
An end date cannot be lifted once set, and payable stays as it was. Runs go by the dates.
An unknown or superseded profile answers invalid_request, so name the current version. Another organisation's entry answers not_found.
Path parameters
roster_entry_id
string
requiredThe roster entry's identifier: the id from POST /v1/payroll/roster_entries or GET /v1/payroll/roster_entries. It starts with run_enterprise_roster_entry_.
Headers
Idempotency-Key
string
requiredA key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.
Body
pay_profile_id
string
optionalThe pay profile to pay this person from now on: its id, starting with run_enterprise_pay_profile_, from POST /v1/pay_profiles. Send it to move them onto a different profile; leave it out when you're only setting an end date.
effective_to
string · date
optionalA leaver is an end date. There is no delete.
Returns
The entry after the change, with the same subject_id: a different person is a different entry.
id
string
requiredThe entry's identifier, from POST /v1/payroll/roster_entries. It starts with run_enterprise_roster_entry_ and never changes; pass it as roster_entry_id on this entry's calls and in roster_entry_ids at POST /v1/payroll/runs.
object
always "roster_entry"
requiredAlways roster_entry. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
subject_id
string
requiredThe person on this entry: their subject identifier, starting with sub_, as you sent it at POST /v1/payroll/roster_entries. Their pay profile and payslips carry the same subject_id; it can't change once the entry exists.
engagement_id
string · nullable
optionalThe engagement this person is paid under: its id, starting with rail_engagement_, from POST /v1/rail/engagements. null until you attach one; the payment gate needs a live one, and blocked_by then says no_live_engagement.
pay_profile_id
string · nullable
optionalThe pay profile this person is paid from: its id, starting with run_enterprise_pay_profile_, from POST /v1/pay_profiles. null until you attach one; without it the entry isn't payable and blocked_by carries no_pay_profile.
payable
boolean
requiredWhether this person currently satisfies the payment gate. False doesn't mean the record is wrong; something they need is missing, and blocked_by says what.
contact_email
string · email · nullable
optionalWhere this person's payslip is sent, or null if you have not given us one. We will not pay somebody we cannot reach: a roster entry without this is not payable and says so in blocked_by as no_contact_address. Set it when you add them, or later at PATCH /v1/payroll/roster_entries/{roster_entry_id}. It is the same person's address at every employer who uses it, so two employers paying the same person each hold their own.
blocked_by
array of string
optionalEmpty when payable is true.
effective_from
string · date
requiredThe first date this person is on the roster, as a calendar date in YYYY-MM-DD, as you set it when you added them.
effective_to
string · date · nullable
optionalThe last date this person is on the roster, as YYYY-MM-DD, or null while they have no end date. A leaver is an end date, set at PATCH /v1/payroll/roster_entries/{roster_entry_id}; there is no delete.
Other responses
Errors it can return
curl -X PATCH "https://sandbox.droomwork.io/v1/payroll/roster_entries/%7Broster_entry_id%7D" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"pay_profile_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","effective_to":"2026-09-01"}'import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollRosterEntriesUpdate({
rosterEntryId: '{roster_entry_id}',
idempotencyKey: crypto.randomUUID(),
runRosterEntryUpdateRequest: {"payProfileId":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","effectiveTo":"2026-09-01"},
});const response = await fetch('https://sandbox.droomwork.io/v1/payroll/roster_entries/%7Broster_entry_id%7D', {
method: 'PATCH',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"pay_profile_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"effective_to": "2026-09-01"
}),
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_roster_entries_update(roster_entry_id='{roster_entry_id}', body={"pay_profile_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "effective_to": "2026-09-01"})import os
import uuid
import requests
response = requests.request(
'PATCH',
'https://sandbox.droomwork.io/v1/payroll/roster_entries/%7Broster_entry_id%7D',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"pay_profile_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "effective_to": "2026-09-01"},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollRosterEntriesUpdate($idempotencyKey, json_decode('{"pay_profile_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","effective_to":"2026-09-01"}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/roster_entries/%7Broster_entry_id%7D');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
'Content-Type: application/json',
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
],
CURLOPT_POSTFIELDS => '{"pay_profile_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","effective_to":"2026-09-01"}',
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollRosterEntriesUpdate("{roster_entry_id}", idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/roster_entries/%7Broster_entry_id%7D"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.header("Content-Type", "application/json")
.header("Idempotency-Key", UUID.randomUUID().toString())
.method("PATCH", HttpRequest.BodyPublishers.ofString("""
{
"pay_profile_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"effective_to": "2026-09-01"
}
"""))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollRosterEntriesUpdate(rosterEntryId: "{roster_entry_id}", idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Patch, "https://sandbox.droomwork.io/v1/payroll/roster_entries/%7Broster_entry_id%7D");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"pay_profile_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"effective_to": "2026-09-01"
}
""", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollRosterEntriesUpdate(ctx, "{roster_entry_id}").IdempotencyKey(key).RunRosterEntryUpdateRequest(body).Execute()body := strings.NewReader(`{
"pay_profile_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"effective_to": "2026-09-01"
}`)
req, _ := http.NewRequest("PATCH", "https://sandbox.droomwork.io/v1/payroll/roster_entries/%7Broster_entry_id%7D", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_roster_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "roster_entry",
"livemode": false,
"mocked": true,
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"payable": true,
"effective_from": "2026-09-01",
"engagement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"pay_profile_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"contact_email": "example",
"blocked_by": [
"no_anchored_identity"
],
"effective_to": "2026-09-01"
}
/v1/payroll/runs#See every run and its state
payroll.runs.list
This gives you every payroll run your organisation has opened. So a screen in your product can carry a row for every run, and a reader sees where each has got to without opening it.
Send nothing to get them all, or filter by status for one state and by period for one month. You get them most recently opened first, each with its period and kind.
One period can hold more than one run, and a correction for the same month is a row of its own. Key your rows on id.
A row carries totals only after the run has been simulated. Before that the field is absent, not zero, so check it is there before reading a figure.
Carry a row's id as run_id to read the same record at GET /v1/payroll/runs/{run_id}.
Query parameters
limit
integer
optionalHow many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.
starting_after
string
optionalThe id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.
status
string
optionalReturn only runs in this state: draft, simulated, pending_approval, approved, executing, completed, partially_settled (some payouts failed to settle), failed or cancelled. Leave it out to get runs in every state.
draftsimulatedpending_approvalapprovedexecutingcompletedpartially_settledfailedcancelledperiod
string
optionalReturn only runs for this pay period, as year and month in YYYY-MM form, for example 2026-09. Leave it out to get runs for every period.
Returns
A page of runs, each carrying created_at, so you can show when one was opened, not just its order.
object
always "list"
requiredAlways list. Tells you which kind of record you are looking at, so one handler can read any response.
data
array of Run
requiredThe records on this page, in the order the list promises. Empty when nothing matched.
14 fields of Run
id
string
requiredThe run's identifier. It starts with run_enterprise_, is assigned when you create the run at POST /v1/payroll/runs and never changes; pass it as run_id wherever a call names this run.
object
always "payroll_run"
requiredAlways payroll_run. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
status
string
requiredYou move a run between states with endpoints, not by setting this field.
draftsimulatedpending_approvalapprovedexecutingcompletedpartially_settledfailedcancelledrun_type
string
requiredFixed at creation and immutable afterwards.
regularoff_cyclebonusthirteenth_monthcorrectionfinal_settlementperiod
string
requiredThe pay period this run covers, as year and month in YYYY-MM form, for example 2026-09.
payee_count
integer · minimum 0
optionalHow many payees are on this run, one for each roster entry it covers. A whole number, 0 or more.
rule_pack
RulePackReference
optional3 fields of RulePackReference
pack_id
string
requiredThe family of rules the pack belongs to, such as ng-payroll. It's pinned when you create the run at POST /v1/payroll/runs and, with version, names exactly which rates, bands and thresholds were applied.
version
string
requiredThe pack version that was applied, in the form 2026.08.1. A version never changes once active, so the same version always means the same rules.
content_hash
string
requiredA digest of the pack's content, prefixed sha256:. It proves which exact rules produced the figure, so an edited pack can never pass as the one you ran under.
totals
RunTotals
optionalEach figure is the sum of its payslip lines, never a percentage of an aggregate.
5 fields of RunTotals
gross
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
net
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
total_deductions
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
employer_contributions
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
payslip_count
integer · minimum 0
optionalHow many payslips these totals are summed from. On a simulation, how many the run would produce; nothing is written.
prepared_by
string · nullable
optionalWho prepared this run, as their user identifier. Above the configured maker-checker threshold the same person can't approve it, so their approve call is refused.
approved_by
string · nullable
optionalNever the same actor as prepared_by above the maker-checker threshold.
created_at
string · date-time
requiredWhen the record was created, as an RFC 3339 timestamp in UTC.
executed_at
string · date-time · nullable
optionalWhen the run was executed, as an RFC 3339 timestamp in UTC. null until it has been.
has_more
boolean
requiredtrue when there are more records after this page. Pass the last record's id as starting_after to get the next page.
Other responses
Errors it can return
# query parameters: limit (optional), starting_after (optional), status (optional), period (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payroll/runs?limit=25&status=draft&period=2026-09" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
// query parameters: limit (optional), starting_after (optional), status (optional), period (optional)
const result = await api.payrollRunsList({ limit: 25, status: 'draft', period: '2026-09' });// query parameters: limit (optional), starting_after (optional), status (optional), period (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/payroll/runs?limit=25&status=draft&period=2026-09', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
# query parameters: limit (optional), starting_after (optional), status (optional), period (optional)
result = api.payroll_runs_list(limit=25, status='draft', period='2026-09')import os
import requests
# query parameters: limit (optional), starting_after (optional), status (optional), period (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/runs?limit=25&status=draft&period=2026-09',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
# query parameters: limit (optional), starting_after (optional), status (optional), period (optional)
$result = $api->payrollRunsList(limit: 25, status: 'draft', period: '2026-09');<?php
// query parameters: limit (optional), starting_after (optional), status (optional), period (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/runs?limit=25&status=draft&period=2026-09');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
import com.useheros.sdk.model.*;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
// query parameters: limit (optional), starting_after (optional), status (optional), period (optional)
var result = api.payrollRunsList(25, null, RunRunStatus.fromValue("draft"), "2026-09");// query parameters: limit (optional), starting_after (optional), status (optional), period (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/runs?limit=25&status=draft&period=2026-09"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
// query parameters: limit (optional), starting_after (optional), status (optional), period (optional)
var result = api.PayrollRunsList(limit: 25, status: RunRunStatus.Draft, period: "2026-09");// query parameters: limit (optional), starting_after (optional), status (optional), period (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/runs?limit=25&status=draft&period=2026-09");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: limit (optional), starting_after (optional), status (optional), period (optional)
result, _, err := client.RUNAPI.PayrollRunsList(ctx).Limit(25).Status(heros.RunRunStatus("draft")).Period("2026-09").Execute()// query parameters: limit (optional), starting_after (optional), status (optional), period (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/runs?limit=25&status=draft&period=2026-09", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"object": "list",
"data": [
{
"id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "payroll_run",
"livemode": false,
"mocked": true,
"status": "draft",
"run_type": "regular",
"period": "2026-09",
"created_at": "2026-09-01T09:00:00Z",
"payee_count": 0,
"rule_pack": {
"pack_id": "ng-payroll",
"version": "2026.08.1",
"content_hash": "sha256:9f2c1e00"
},
"totals": {
"gross": {
"amount": 52500000,
"currency": "NGN"
},
"net": {
"amount": 41250000,
"currency": "NGN"
},
"total_deductions": {
"amount": 11250000,
"currency": "NGN"
},
"employer_contributions": {
"amount": 5400000,
"currency": "NGN"
},
"payslip_count": 0
},
"prepared_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"approved_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"executed_at": "2026-09-01T09:00:00Z"
}
],
"has_more": true
}
/v1/payroll/runs/{run_id}#Check one run as it stands now
payroll.runs.retrieve
This reads one payroll run back, which is one period's payroll for one set of payees. So a payroll screen in your product can refresh a run it saved earlier, instead of showing totals that a later simulation has replaced.
Send the run's id in the path. You get the run back: status for where it has got to, and totals for gross, net, total deductions and employer contributions.
rule_pack is the pack those figures are pinned to. Both it and totals are absent until there is something to put in them: no totals means the figures are not computed yet, never zero.
Read the payslips it wrote at GET /v1/payroll/payslips?run_id=, passing this id, once it has executed. See every one at GET /v1/payroll/runs. Another organisation's id answers not_found, not forbidden.
Path parameters
run_id
string
requiredThe run's identifier, from the id of the response to POST /v1/payroll/runs or of a run on GET /v1/payroll/runs. It starts with run_enterprise_.
Returns
One run in full, naming who prepared the figures and who signed them off.
id
string
requiredThe run's identifier. It starts with run_enterprise_, is assigned when you create the run at POST /v1/payroll/runs and never changes; pass it as run_id wherever a call names this run.
object
always "payroll_run"
requiredAlways payroll_run. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
status
string
requiredYou move a run between states with endpoints, not by setting this field.
draftsimulatedpending_approvalapprovedexecutingcompletedpartially_settledfailedcancelledrun_type
string
requiredFixed at creation and immutable afterwards.
regularoff_cyclebonusthirteenth_monthcorrectionfinal_settlementperiod
string
requiredThe pay period this run covers, as year and month in YYYY-MM form, for example 2026-09.
payee_count
integer · minimum 0
optionalHow many payees are on this run, one for each roster entry it covers. A whole number, 0 or more.
rule_pack
RulePackReference
optional3 fields of RulePackReference
pack_id
string
requiredThe family of rules the pack belongs to, such as ng-payroll. It's pinned when you create the run at POST /v1/payroll/runs and, with version, names exactly which rates, bands and thresholds were applied.
version
string
requiredThe pack version that was applied, in the form 2026.08.1. A version never changes once active, so the same version always means the same rules.
content_hash
string
requiredA digest of the pack's content, prefixed sha256:. It proves which exact rules produced the figure, so an edited pack can never pass as the one you ran under.
totals
RunTotals
optionalEach figure is the sum of its payslip lines, never a percentage of an aggregate.
5 fields of RunTotals
gross
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
net
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
total_deductions
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
employer_contributions
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
payslip_count
integer · minimum 0
optionalHow many payslips these totals are summed from. On a simulation, how many the run would produce; nothing is written.
prepared_by
string · nullable
optionalWho prepared this run, as their user identifier. Above the configured maker-checker threshold the same person can't approve it, so their approve call is refused.
approved_by
string · nullable
optionalNever the same actor as prepared_by above the maker-checker threshold.
created_at
string · date-time
requiredWhen the record was created, as an RFC 3339 timestamp in UTC.
executed_at
string · date-time · nullable
optionalWhen the run was executed, as an RFC 3339 timestamp in UTC. null until it has been.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollRunsRetrieve({ runId: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_runs_retrieve(run_id='run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollRunsRetrieve(run_id: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollRunsRetrieve("run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollRunsRetrieve(runId: "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollRunsRetrieve(ctx, "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "payroll_run",
"livemode": false,
"mocked": true,
"status": "draft",
"run_type": "regular",
"period": "2026-09",
"created_at": "2026-09-01T09:00:00Z",
"payee_count": 0,
"rule_pack": {
"pack_id": "ng-payroll",
"version": "2026.08.1",
"content_hash": "sha256:9f2c1e00"
},
"totals": {
"gross": {
"amount": 52500000,
"currency": "NGN"
},
"net": {
"amount": 41250000,
"currency": "NGN"
},
"total_deductions": {
"amount": 11250000,
"currency": "NGN"
},
"employer_contributions": {
"amount": 5400000,
"currency": "NGN"
},
"payslip_count": 0
},
"prepared_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"approved_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"executed_at": "2026-09-01T09:00:00Z"
}
/v1/payroll/runs/{run_id}#Change a draft run's payees or rule pack
payroll.runs.update
This corrects a draft payroll run: who it pays, and which rule pack version its figures come from. So your product can let somebody fix a wrong payee list without cancelling the run.
Send the run's id in the path, with the roster entries it pays, a rule pack version, or both. You get the updated run back.
roster_entry_ids replaces the whole list. Send every entry the run pays, naming each once; payee_count follows it, and an empty list leaves the run paying nobody.
A run has to exist, from POST /v1/payroll/runs, and be in draft. It leaves draft the moment you simulate or cancel it, nothing brings it back, and a later change is refused with conflict.
Simulate at POST /v1/payroll/runs/{run_id}/simulate to see the figures. A wrong period or run type cannot be changed here: cancel at POST /v1/payroll/runs/{run_id}/cancel before the run executes, and open another.
Path parameters
run_id
string
requiredThe run's identifier, from the id of the response to POST /v1/payroll/runs or of a run on GET /v1/payroll/runs. It starts with run_enterprise_.
Headers
Idempotency-Key
string
requiredA key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.
Body
roster_entry_ids
array of string
optionalWhich roster entries the run pays, by their id. You can change this only while the run is in draft; afterwards the call is refused with conflict.
rule_pack_version
string
optionalThe rule pack version to pin the run to in place of the one it has, for example 2026.08.1. You can change this only while the run is in draft.
Returns
The updated run, carrying the period it pays, so you can confirm you changed the right one.
id
string
requiredThe run's identifier. It starts with run_enterprise_, is assigned when you create the run at POST /v1/payroll/runs and never changes; pass it as run_id wherever a call names this run.
object
always "payroll_run"
requiredAlways payroll_run. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
status
string
requiredYou move a run between states with endpoints, not by setting this field.
draftsimulatedpending_approvalapprovedexecutingcompletedpartially_settledfailedcancelledrun_type
string
requiredFixed at creation and immutable afterwards.
regularoff_cyclebonusthirteenth_monthcorrectionfinal_settlementperiod
string
requiredThe pay period this run covers, as year and month in YYYY-MM form, for example 2026-09.
payee_count
integer · minimum 0
optionalHow many payees are on this run, one for each roster entry it covers. A whole number, 0 or more.
rule_pack
RulePackReference
optional3 fields of RulePackReference
pack_id
string
requiredThe family of rules the pack belongs to, such as ng-payroll. It's pinned when you create the run at POST /v1/payroll/runs and, with version, names exactly which rates, bands and thresholds were applied.
version
string
requiredThe pack version that was applied, in the form 2026.08.1. A version never changes once active, so the same version always means the same rules.
content_hash
string
requiredA digest of the pack's content, prefixed sha256:. It proves which exact rules produced the figure, so an edited pack can never pass as the one you ran under.
totals
RunTotals
optionalEach figure is the sum of its payslip lines, never a percentage of an aggregate.
5 fields of RunTotals
gross
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
net
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
total_deductions
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
employer_contributions
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
payslip_count
integer · minimum 0
optionalHow many payslips these totals are summed from. On a simulation, how many the run would produce; nothing is written.
prepared_by
string · nullable
optionalWho prepared this run, as their user identifier. Above the configured maker-checker threshold the same person can't approve it, so their approve call is refused.
approved_by
string · nullable
optionalNever the same actor as prepared_by above the maker-checker threshold.
created_at
string · date-time
requiredWhen the record was created, as an RFC 3339 timestamp in UTC.
executed_at
string · date-time · nullable
optionalWhen the run was executed, as an RFC 3339 timestamp in UTC. null until it has been.
Other responses
Errors it can return
curl -X PATCH "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"roster_entry_ids":["sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"],"rule_pack_version":"2026.08.1"}'import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollRunsUpdate({
runId: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
idempotencyKey: crypto.randomUUID(),
runRunUpdateRequest: {"rosterEntryIds":["sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"],"rulePackVersion":"2026.08.1"},
});const response = await fetch('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', {
method: 'PATCH',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"roster_entry_ids": [
"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
],
"rule_pack_version": "2026.08.1"
}),
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_runs_update(run_id='run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"roster_entry_ids": ["sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"], "rule_pack_version": "2026.08.1"})import os
import uuid
import requests
response = requests.request(
'PATCH',
'https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"roster_entry_ids": ["sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"], "rule_pack_version": "2026.08.1"},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollRunsUpdate($idempotencyKey, json_decode('{"roster_entry_ids":["sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"],"rule_pack_version":"2026.08.1"}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
'Content-Type: application/json',
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
],
CURLOPT_POSTFIELDS => '{"roster_entry_ids":["sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"],"rule_pack_version":"2026.08.1"}',
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollRunsUpdate("run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.header("Content-Type", "application/json")
.header("Idempotency-Key", UUID.randomUUID().toString())
.method("PATCH", HttpRequest.BodyPublishers.ofString("""
{
"roster_entry_ids": [
"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
],
"rule_pack_version": "2026.08.1"
}
"""))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollRunsUpdate(runId: "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Patch, "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"roster_entry_ids": [
"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
],
"rule_pack_version": "2026.08.1"
}
""", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollRunsUpdate(ctx, "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).RunRunUpdateRequest(body).Execute()body := strings.NewReader(`{
"roster_entry_ids": [
"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
],
"rule_pack_version": "2026.08.1"
}`)
req, _ := http.NewRequest("PATCH", "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "payroll_run",
"livemode": false,
"mocked": true,
"status": "draft",
"run_type": "regular",
"period": "2026-09",
"created_at": "2026-09-01T09:00:00Z",
"payee_count": 0,
"rule_pack": {
"pack_id": "ng-payroll",
"version": "2026.08.1",
"content_hash": "sha256:9f2c1e00"
},
"totals": {
"gross": {
"amount": 52500000,
"currency": "NGN"
},
"net": {
"amount": 41250000,
"currency": "NGN"
},
"total_deductions": {
"amount": 11250000,
"currency": "NGN"
},
"employer_contributions": {
"amount": 5400000,
"currency": "NGN"
},
"payslip_count": 0
},
"prepared_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"approved_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"executed_at": "2026-09-01T09:00:00Z"
}
/v1/payroll/runs/{run_id}/simulate#Work out what a run would pay
payroll.runs.simulate
This works out what a run would pay without writing a payslip, and screens the result. So a sign-off screen in your product shows a real total with the questions raised against it, and a serious one blocks approval.
Send the run's id in the path. You get the totals the run would produce and the rule_pack they were computed against; the run moves to simulated.
A run has to exist, from POST /v1/payroll/runs. Every roster entry on it must be payable and name a pay profile, or you are refused with prerequisite_not_met.
Simulate again while the run is in draft or simulated. Its roster and pinned pack are fixed once it leaves draft, so PATCH /v1/payroll/runs/{run_id} is refused; correct a pay profile instead.
Read the findings it raised at GET /v1/payroll/runs/{run_id}/findings, then approve at POST /v1/payroll/runs/{run_id}/approve. An approved or cancelled run is refused with conflict.
Path parameters
run_id
string
requiredThe run's identifier, from the id of the response to POST /v1/payroll/runs or of a run on GET /v1/payroll/runs. It starts with run_enterprise_.
Headers
Idempotency-Key
string
requiredA key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.
Body optional
draft_rule_pack_version
string
optionalSimulate against a pack that has not been activated. Naming it here does not activate it.
Returns
The simulated totals. The run moves to simulated.
object
always "run_simulation"
requiredAlways run_simulation. Tells you which kind of record you are looking at, so one handler can read any response.
run_id
string
requiredThe id of the run these totals were simulated for, starting with run_enterprise_: the run_id you sent in the path of POST /v1/payroll/runs/{run_id}/simulate. The run itself moves to simulated.
rule_pack
RulePackReference
optional3 fields of RulePackReference
pack_id
string
requiredThe family of rules the pack belongs to, such as ng-payroll. It's pinned when you create the run at POST /v1/payroll/runs and, with version, names exactly which rates, bands and thresholds were applied.
version
string
requiredThe pack version that was applied, in the form 2026.08.1. A version never changes once active, so the same version always means the same rules.
content_hash
string
requiredA digest of the pack's content, prefixed sha256:. It proves which exact rules produced the figure, so an edited pack can never pass as the one you ran under.
totals
RunTotals
requiredEach figure is the sum of its payslip lines, never a percentage of an aggregate.
5 fields of RunTotals
gross
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
net
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
total_deductions
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
employer_contributions
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
payslip_count
integer · minimum 0
optionalHow many payslips these totals are summed from. On a simulation, how many the run would produce; nothing is written.
wrote_records
always false
requiredAlways false. A simulation writes nothing and moves nothing.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/simulate" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"draft_rule_pack_version":"2026.08.1"}'import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollRunsSimulate({
runId: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
idempotencyKey: crypto.randomUUID(),
runRunSimulateRequest: {"draftRulePackVersion":"2026.08.1"},
});const response = await fetch('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/simulate', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"draft_rule_pack_version": "2026.08.1"
}),
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_runs_simulate(run_id='run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"draft_rule_pack_version": "2026.08.1"})import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/simulate',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"draft_rule_pack_version": "2026.08.1"},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollRunsSimulate($idempotencyKey, json_decode('{"draft_rule_pack_version":"2026.08.1"}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/simulate');
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 => '{"draft_rule_pack_version":"2026.08.1"}',
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollRunsSimulate("run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/simulate"))
.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("""
{
"draft_rule_pack_version": "2026.08.1"
}
"""))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollRunsSimulate(runId: "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/simulate");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"draft_rule_pack_version": "2026.08.1"
}
""", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollRunsSimulate(ctx, "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).RunRunSimulateRequest(body).Execute()body := strings.NewReader(`{
"draft_rule_pack_version": "2026.08.1"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/simulate", 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)
{
"object": "run_simulation",
"run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"totals": {
"gross": {
"amount": 52500000,
"currency": "NGN"
},
"net": {
"amount": 41250000,
"currency": "NGN"
},
"total_deductions": {
"amount": 11250000,
"currency": "NGN"
},
"employer_contributions": {
"amount": 5400000,
"currency": "NGN"
},
"payslip_count": 0
},
"wrote_records": false,
"rule_pack": {
"pack_id": "ng-payroll",
"version": "2026.08.1",
"content_hash": "sha256:9f2c1e00"
}
}
/v1/payroll/runs/{run_id}/cancel#Cancel a run before execution
payroll.runs.cancel
This ends a run you have decided not to pay. So an approval queue in your product can turn a period down, and it closes with no payslips and no money owed.
Send the run's id in the path and no request body. You get the run back in the cancelled state.
A run has to exist, from POST /v1/payroll/runs, and must not have executed or already been cancelled. Either is refused with conflict.
It is also how you get out of a run that cannot go forward. A run's period and run_type are fixed when you create it and PATCH /v1/payroll/runs/{run_id} changes neither. You can still cancel a run that approving or executing has refused.
Cancelling is the end of the run: nothing moves it afterwards, and no event is published when it happens. Open the run you do mean to pay at POST /v1/payroll/runs.
Path parameters
run_id
string
requiredThe run's identifier, from the id of the response to POST /v1/payroll/runs or of a run on GET /v1/payroll/runs. It starts with run_enterprise_.
Headers
Idempotency-Key
string
requiredA key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.
Returns
The run record, with no field for when this call happened; the audit entry carries that time.
id
string
requiredThe run's identifier. It starts with run_enterprise_, is assigned when you create the run at POST /v1/payroll/runs and never changes; pass it as run_id wherever a call names this run.
object
always "payroll_run"
requiredAlways payroll_run. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
status
string
requiredYou move a run between states with endpoints, not by setting this field.
draftsimulatedpending_approvalapprovedexecutingcompletedpartially_settledfailedcancelledrun_type
string
requiredFixed at creation and immutable afterwards.
regularoff_cyclebonusthirteenth_monthcorrectionfinal_settlementperiod
string
requiredThe pay period this run covers, as year and month in YYYY-MM form, for example 2026-09.
payee_count
integer · minimum 0
optionalHow many payees are on this run, one for each roster entry it covers. A whole number, 0 or more.
rule_pack
RulePackReference
optional3 fields of RulePackReference
pack_id
string
requiredThe family of rules the pack belongs to, such as ng-payroll. It's pinned when you create the run at POST /v1/payroll/runs and, with version, names exactly which rates, bands and thresholds were applied.
version
string
requiredThe pack version that was applied, in the form 2026.08.1. A version never changes once active, so the same version always means the same rules.
content_hash
string
requiredA digest of the pack's content, prefixed sha256:. It proves which exact rules produced the figure, so an edited pack can never pass as the one you ran under.
totals
RunTotals
optionalEach figure is the sum of its payslip lines, never a percentage of an aggregate.
5 fields of RunTotals
gross
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
net
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
total_deductions
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
employer_contributions
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
payslip_count
integer · minimum 0
optionalHow many payslips these totals are summed from. On a simulation, how many the run would produce; nothing is written.
prepared_by
string · nullable
optionalWho prepared this run, as their user identifier. Above the configured maker-checker threshold the same person can't approve it, so their approve call is refused.
approved_by
string · nullable
optionalNever the same actor as prepared_by above the maker-checker threshold.
created_at
string · date-time
requiredWhen the record was created, as an RFC 3339 timestamp in UTC.
executed_at
string · date-time · nullable
optionalWhen the run was executed, as an RFC 3339 timestamp in UTC. null until it has been.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/cancel" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollRunsCancel({ runId: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/cancel', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Idempotency-Key': crypto.randomUUID(),
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_runs_cancel(run_id='run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/cancel',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollRunsCancel(run_id: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/cancel');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollRunsCancel("run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/cancel"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.header("Idempotency-Key", UUID.randomUUID().toString())
.method("POST", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollRunsCancel(runId: "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/cancel");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollRunsCancel(ctx, "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/cancel", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "payroll_run",
"livemode": false,
"mocked": true,
"status": "cancelled",
"run_type": "regular",
"period": "2026-09",
"created_at": "2026-09-01T09:00:00Z",
"payee_count": 0,
"rule_pack": {
"pack_id": "ng-payroll",
"version": "2026.08.1",
"content_hash": "sha256:9f2c1e00"
},
"totals": {
"gross": {
"amount": 52500000,
"currency": "NGN"
},
"net": {
"amount": 41250000,
"currency": "NGN"
},
"total_deductions": {
"amount": 11250000,
"currency": "NGN"
},
"employer_contributions": {
"amount": 5400000,
"currency": "NGN"
},
"payslip_count": 0
},
"prepared_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"approved_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"executed_at": "2026-09-01T09:00:00Z"
}
/v1/payroll/audit_entries#Who acted on your payroll, and when
payroll.audit_entries.list
This is who did what in RUN. So your product can answer who approved a period, or who cancelled one and when, without a second log of its own.
Send nothing, or narrow by operation, actor, record, outcome or a window of time. You get entries newest first, naming who acted, what they did and whether it was allowed.
Spell action as an entry stores it: the method, then the route with colons for path parameters, as in POST /v1/payroll/runs/:run_id/approve. Braces match nothing, and you get an empty page.
Your own successful reads leave no row here. A Heros staff member's read is recorded, so you can see what we opened. Nothing refused with unauthenticated is recorded, so a revoked credential's calls are not here.
Read one at GET /v1/payroll/audit_entries/{audit_entry_id}.
Query parameters
action
string
optionalOnly attempts at one operation, matched to action on each entry: the method and route pattern, such as POST /v1/payroll/runs. Leave it out to get attempts at every operation.
actor_id
string
optionalOnly attempts by one actor, matched to actor_id on each entry: an API key's id (key_…, from POST /v1/api_keys), an OAuth client_id, a signed-in person's id (usr_…) or a staff member's. Leave it out to get every actor.
resource
string
optionalOnly attempts on one kind of record, matched to resource on each entry: the collection segment of the route, such as runs or payslips. Pair it with resource_id to narrow to one record; leave it out to get every kind.
resource_id
string
optionalOnly attempts on one record, matched to resource_id on each entry: the record's id as it appeared in the route, such as a run's (run_enterprise_…, from POST /v1/payroll/runs). Pair it with resource; leave it out for every record.
outcome
string
optionalOnly attempts that ended one way, as outcome reads on each entry: succeeded (a status below 400), refused (a 4xx, the request was turned down) or failed (a 5xx, it went wrong on our side). Leave it out to get all three.
succeededrefusedfailedrecorded_after
string
optionalOnly entries whose at is after this moment, as an RFC 3339 timestamp in UTC; exclusive, so an entry at exactly this instant is left out. Leave it out to reach back to the oldest entry.
recorded_before
string
optionalOnly entries whose at is 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 bound a window, or leave it out to run up to the newest entry.
limit
integer
optionalHow many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.
starting_after
string
optionalThe id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.
Returns
A page of audit entries, and not one of them carries the values an attempt changed.
object
always "list"
requiredAlways list. Tells you which kind of record you are looking at, so one handler can read any response.
data
array of AuditEntry
requiredThe records on this page, in the order the list promises. Empty when nothing matched.
13 fields of AuditEntry
id
string
requiredThe entry's identifier, as GET /v1/payroll/audit_entries lists it. It starts with aud_ and never changes; pass it as audit_entry_id to GET /v1/payroll/audit_entries/{audit_entry_id}, or as starting_after to page past it.
object
always "audit_entry"
requiredAlways audit_entry. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm the action happened in. False is the sandbox.
mocked
boolean
requiredWhether the route this attempt was aimed at is mocked. It describes the route, not the answer: a refused attempt and a replayed idempotent request are both recorded as mocked when the route is. It is never derived from livemode.
at
string · date-time
requiredWhen the attempt was recorded, as an RFC 3339 timestamp in UTC. The list orders entries by it, newest first, and recorded_after and recorded_before bound it.
request_id
string
requiredThe request that made this attempt. It starts with req_ and is the same request_id an error body carries, so a refusal you were shown can be matched to its entry here.
actor_type
string
requiredWhat kind of credential acted: client for your API key or OAuth client, user for a signed-in person, staff for a Heros staff member on a support case, service when Heros acted for you.
actor_id
string
requiredWho acted, by actor_type: for client your API key's id (key_…, from GET /v1/api_keys) or OAuth client_id; for user the person's id (usr_…); for staff theirs; for service a name. Pass it as actor_id on the list to follow one.
action
string
requiredWhat was attempted, as the method and the route pattern.
resource
string · nullable
optionalThe kind of record the attempt was aimed at, as the collection name in the route, such as runs or payslips. null when the route named none.
resource_id
string · nullable
optionalThe id of the record the route named, such as a run's (run_enterprise_…, from POST /v1/payroll/runs) or a payslip's (run_enterprise_payslip_…, from GET /v1/payroll/payslips). null when the route named none, as on a create or a list.
outcome
string
requiredHow the attempt ended: succeeded for a status below 400, refused for a 4xx, failed for a 5xx. Pass refused as outcome on GET /v1/payroll/audit_entries to see every attempt that was turned away.
succeededrefusedfailedstatus
integer
requiredThe HTTP status the caller was given, such as 201 or 403. outcome is read from it, so the two never disagree.
has_more
boolean
requiredtrue when there are more records after this page. Pass the last record's id as starting_after to get the next page.
Other responses
Errors it can return
# query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payroll/audit_entries?action=POST%20%2Fv1%2Fpayroll%2Fruns&actor_id=usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=runs&resource_id=run_enterprise_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, RUNApi } from '@heros/sdk';
const api = new RUNApi(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.payrollAuditEntriesList({ action: 'POST /v1/payroll/runs', actorId: 'usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', resource: 'runs', resourceId: 'run_enterprise_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/payroll/audit_entries?action=POST%20%2Fv1%2Fpayroll%2Fruns&actor_id=usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=runs&resource_id=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(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.payroll_audit_entries_list(action='POST /v1/payroll/runs', actor_id='usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', resource='runs', resource_id='run_enterprise_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/payroll/audit_entries?action=POST%20%2Fv1%2Fpayroll%2Fruns&actor_id=usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=runs&resource_id=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(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->payrollAuditEntriesList(action: 'POST /v1/payroll/runs', actor_id: 'usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', resource: 'runs', resource_id: 'run_enterprise_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/payroll/audit_entries?action=POST%20%2Fv1%2Fpayroll%2Fruns&actor_id=usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=runs&resource_id=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(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.payrollAuditEntriesList("POST /v1/payroll/runs", "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "runs", "run_enterprise_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/payroll/audit_entries?action=POST%20%2Fv1%2Fpayroll%2Fruns&actor_id=usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=runs&resource_id=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(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.PayrollAuditEntriesList(action: "POST /v1/payroll/runs", actorId: "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", resource: "runs", resourceId: "run_enterprise_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/payroll/audit_entries?action=POST%20%2Fv1%2Fpayroll%2Fruns&actor_id=usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=runs&resource_id=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
result, _, err := client.RUNAPI.PayrollAuditEntriesList(ctx).Action("POST /v1/payroll/runs").ActorId("usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Resource("runs").ResourceId("run_enterprise_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/payroll/audit_entries?action=POST%20%2Fv1%2Fpayroll%2Fruns&actor_id=usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=runs&resource_id=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"object": "list",
"data": [
{
"id": "aud_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "audit_entry",
"livemode": false,
"mocked": true,
"at": "2026-09-01T09:00:00Z",
"request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"actor_type": "example",
"actor_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"action": "example",
"outcome": "succeeded",
"status": 1,
"resource": "runs",
"resource_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
],
"has_more": true
}
/v1/payroll/runs/{run_id}/approve#Approve a run for execution
payroll.runs.approve
This records the decision that a run should go ahead with the figures it holds. So an approval queue in your product has somewhere to put the answer.
Send the run's id in the path. You get the run back in the approved state, and approved_by names who approved it.
A run has to have been simulated first, at POST /v1/payroll/runs/{run_id}/simulate, and any finding of severity p0 on it has to be resolved.
Read the run's findings at GET /v1/payroll/runs/{run_id}/findings and clear each p0 with a reason code at POST /v1/payroll/runs/{run_id}/findings/{finding_id}/resolve. Approving a run that is not ready, or one with an open p0, is refused with conflict.
Approving pays nobody and writes no payslip. It publishes run.approved, so your product can act on the event. Execute it at POST /v1/payroll/runs/{run_id}/execute when you want the payslips written.
Path parameters
run_id
string
requiredThe run's identifier, from the id of the response to POST /v1/payroll/runs or of a run on GET /v1/payroll/runs. It starts with run_enterprise_.
Headers
Idempotency-Key
string
requiredA key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.
Returns
The run, with the period it pays, so an approval queue row can name the month signed off.
id
string
requiredThe run's identifier. It starts with run_enterprise_, is assigned when you create the run at POST /v1/payroll/runs and never changes; pass it as run_id wherever a call names this run.
object
always "payroll_run"
requiredAlways payroll_run. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
status
string
requiredYou move a run between states with endpoints, not by setting this field.
draftsimulatedpending_approvalapprovedexecutingcompletedpartially_settledfailedcancelledrun_type
string
requiredFixed at creation and immutable afterwards.
regularoff_cyclebonusthirteenth_monthcorrectionfinal_settlementperiod
string
requiredThe pay period this run covers, as year and month in YYYY-MM form, for example 2026-09.
payee_count
integer · minimum 0
optionalHow many payees are on this run, one for each roster entry it covers. A whole number, 0 or more.
rule_pack
RulePackReference
optional3 fields of RulePackReference
pack_id
string
requiredThe family of rules the pack belongs to, such as ng-payroll. It's pinned when you create the run at POST /v1/payroll/runs and, with version, names exactly which rates, bands and thresholds were applied.
version
string
requiredThe pack version that was applied, in the form 2026.08.1. A version never changes once active, so the same version always means the same rules.
content_hash
string
requiredA digest of the pack's content, prefixed sha256:. It proves which exact rules produced the figure, so an edited pack can never pass as the one you ran under.
totals
RunTotals
optionalEach figure is the sum of its payslip lines, never a percentage of an aggregate.
5 fields of RunTotals
gross
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
net
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
total_deductions
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
employer_contributions
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
payslip_count
integer · minimum 0
optionalHow many payslips these totals are summed from. On a simulation, how many the run would produce; nothing is written.
prepared_by
string · nullable
optionalWho prepared this run, as their user identifier. Above the configured maker-checker threshold the same person can't approve it, so their approve call is refused.
approved_by
string · nullable
optionalNever the same actor as prepared_by above the maker-checker threshold.
created_at
string · date-time
requiredWhen the record was created, as an RFC 3339 timestamp in UTC.
executed_at
string · date-time · nullable
optionalWhen the run was executed, as an RFC 3339 timestamp in UTC. null until it has been.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/approve" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollRunsApprove({ runId: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/approve', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Idempotency-Key': crypto.randomUUID(),
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_runs_approve(run_id='run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/approve',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollRunsApprove(run_id: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/approve');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollRunsApprove("run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/approve"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.header("Idempotency-Key", UUID.randomUUID().toString())
.method("POST", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollRunsApprove(runId: "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/approve");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollRunsApprove(ctx, "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/approve", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "payroll_run",
"livemode": false,
"mocked": true,
"status": "approved",
"run_type": "regular",
"period": "2026-09",
"created_at": "2026-09-01T09:00:00Z",
"payee_count": 0,
"rule_pack": {
"pack_id": "ng-payroll",
"version": "2026.08.1",
"content_hash": "sha256:9f2c1e00"
},
"totals": {
"gross": {
"amount": 52500000,
"currency": "NGN"
},
"net": {
"amount": 41250000,
"currency": "NGN"
},
"total_deductions": {
"amount": 11250000,
"currency": "NGN"
},
"employer_contributions": {
"amount": 5400000,
"currency": "NGN"
},
"payslip_count": 0
},
"prepared_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"approved_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"executed_at": "2026-09-01T09:00:00Z"
}
/v1/payroll/runs/{run_id}/totals#What a pay period comes to in money
payroll.runs.retrieve_totals
This gives you back the figures a simulated run came to, so a sign-off screen in your product can show what the period costs without reading every payslip. The same figures stand as the run's record once it has executed.
Send the run's id in the path. You get four money figures: gross, total_deductions, net, what the payees receive, and employer_contributions, what you owe on top of pay; payslip_count says how many people they cover.
The employer figure is cost on top of pay and no part of gross or net, so what the period costs you is gross plus employer_contributions. No field carries that sum.
Simulate the run first, at POST /v1/payroll/runs/{run_id}/simulate: that call is what works the figures out. The same five sit on the run as totals on GET /v1/payroll/runs/{run_id}. An id that matches no run of yours is refused with not_found.
Path parameters
run_id
string
requiredThe run's identifier, from the id of the response to POST /v1/payroll/runs or of a run on GET /v1/payroll/runs. It starts with run_enterprise_.
Returns
The totals.
gross
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
net
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
total_deductions
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
employer_contributions
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
payslip_count
integer · minimum 0
optionalHow many payslips these totals are summed from. On a simulation, how many the run would produce; nothing is written.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/totals" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollRunsRetrieveTotals({ runId: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/totals', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_runs_retrieve_totals(run_id='run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/totals',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollRunsRetrieveTotals(run_id: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/totals');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollRunsRetrieveTotals("run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/totals"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollRunsRetrieveTotals(runId: "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/totals");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollRunsRetrieveTotals(ctx, "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/totals", 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)
{
"gross": {
"amount": 52500000,
"currency": "NGN"
},
"net": {
"amount": 41250000,
"currency": "NGN"
},
"total_deductions": {
"amount": 11250000,
"currency": "NGN"
},
"employer_contributions": {
"amount": 5400000,
"currency": "NGN"
},
"payslip_count": 0
}
/v1/payroll/runs/{run_id}/findings#Check what was flagged before approving a run
payroll.run_findings.list
This is everything questioned about a run's figures before anybody signs them off. It gives your product the review step between simulating a run and approving it, while the figures can still be put right.
Send the run's id in the path and you get its findings, oldest first and not by severity. Each carries how serious it is, a code to match on, a detail sentence for a person, and the payee concerned.
Findings are raised when you simulate the run at POST /v1/payroll/runs/{run_id}/simulate, so an empty list on a run you have not simulated is not a clean run.
Ask for severity=p0 and status=open to see what is stopping the run: while a p0 is open, approve and execute both refuse and name it.
Clear one with a reason code at POST /v1/payroll/runs/{run_id}/findings/{finding_id}/resolve, sending its id as finding_id.
Path parameters
run_id
string
requiredThe run's identifier, from the id of the response to POST /v1/payroll/runs or of a run on GET /v1/payroll/runs. It starts with run_enterprise_.
Query parameters
limit
integer
optionalHow many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.
severity
string
optionalReturn only findings of this severity: p0, which blocks approval and execution until you resolve it, or p1 or p2, which are for you to review and block nothing. Leave it out to get findings of every severity.
p0p1p2status
string
optionalReturn only findings in this state: open (not yet resolved) or resolved (cleared with a reason code at POST /v1/payroll/runs/{run_id}/findings/{finding_id}/resolve). Leave it out to get both.
openresolvedReturns
The run's findings, enough to sort what blocks a sign-off from what is only worth reading.
object
always "list"
requiredAlways list. Tells you which kind of record you are looking at, so one handler can read any response.
data
array of RunFinding
requiredThe records on this page, in the order the list promises. Empty when nothing matched.
13 fields of RunFinding
id
string
requiredThe finding's identifier, as it reads on each finding from GET /v1/payroll/runs/{run_id}/findings. It never changes; pass it as finding_id in the path when you retrieve or resolve the finding.
object
always "run_finding"
requiredAlways run_finding. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
run_id
string
requiredThe id of the run this finding was raised on, starting with run_enterprise_, as returned by POST /v1/payroll/runs. A p0 finding blocks that run's approval and execution until you resolve it.
severity
string
requiredOnly p0 blocks approval and execution.
p0p1p2status
string
requiredopenresolvedcode
string
requiredA stable code naming what was found, for example net_pay_variance_above_threshold. Match on this in your own handling; detail is for people.
detail
string
requiredA sentence for a person saying what was found and why it matters, for example that a payee has no verified destination. Don't match on it; use code.
subject_id
string · nullable
optionalWho the finding is about, when it concerns one payee: their subject identifier, starting with sub_, as you sent it on their roster entry at POST /v1/payroll/roster_entries. null when it concerns the run as a whole.
resolved_by
string · nullable
optionalWho resolved the finding, as their user identifier, kept permanently with the reason code. null while it's still open.
reason_code
string · nullable
optionalThe reason code sent when the finding was resolved, for example no_payee_destination. null while it's still open.
resolved_at
string · date-time · nullable
optionalWhen the finding was resolved, as an RFC 3339 timestamp in UTC. null while it's still open.
has_more
boolean
requiredtrue when there are more records after this page. Pass the last record's id as starting_after to get the next page.
Other responses
Errors it can return
# query parameters: limit (optional), severity (optional), status (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings?limit=25&severity=p0&status=open" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
// query parameters: limit (optional), severity (optional), status (optional)
const result = await api.payrollRunFindingsList({ runId: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', limit: 25, severity: 'p0', status: 'open' });// query parameters: limit (optional), severity (optional), status (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings?limit=25&severity=p0&status=open', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
# query parameters: limit (optional), severity (optional), status (optional)
result = api.payroll_run_findings_list(run_id='run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', limit=25, severity='p0', status='open')import os
import requests
# query parameters: limit (optional), severity (optional), status (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings?limit=25&severity=p0&status=open',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
# query parameters: limit (optional), severity (optional), status (optional)
$result = $api->payrollRunFindingsList(run_id: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', limit: 25, severity: 'p0', status: 'open');<?php
// query parameters: limit (optional), severity (optional), status (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings?limit=25&severity=p0&status=open');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
import com.useheros.sdk.model.*;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
// query parameters: limit (optional), severity (optional), status (optional)
var result = api.payrollRunFindingsList("run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", 25, RunFindingSeverity.fromValue("p0"), RunFindingStatus.fromValue("open"));// query parameters: limit (optional), severity (optional), status (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings?limit=25&severity=p0&status=open"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
// query parameters: limit (optional), severity (optional), status (optional)
var result = api.PayrollRunFindingsList(runId: "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", limit: 25, severity: RunFindingSeverity.P0, status: RunFindingStatus.Open);// query parameters: limit (optional), severity (optional), status (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings?limit=25&severity=p0&status=open");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: limit (optional), severity (optional), status (optional)
result, _, err := client.RUNAPI.PayrollRunFindingsList(ctx, "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Limit(25).Severity(heros.RunFindingSeverity("p0")).Status(heros.RunFindingStatus("open")).Execute()// query parameters: limit (optional), severity (optional), status (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings?limit=25&severity=p0&status=open", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"object": "list",
"data": [
{
"id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "run_finding",
"livemode": false,
"mocked": true,
"run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"severity": "p0",
"status": "open",
"code": "net_pay_variance_above_threshold",
"detail": "The payee has no verified destination, so this line cannot be paid.",
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"resolved_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"reason_code": "no_payee_destination",
"resolved_at": "2026-09-01T09:00:00Z"
}
],
"has_more": true
}
/v1/payroll/events#Replay your payroll event history
payroll.events.list
RUN records what it did on your payroll as events, and you read them back here. So your product can catch up on deliveries your webhook receiver missed, and reconcile your own figures against the same bodies those deliveries carry.
Send nothing to read across every stream, oldest first. Each event comes back under an evt_ identifier, which is what you keep to read that one again.
To replay one record, pass its id as stream and the last sequence you handled as after. That read is sequence order exactly. after counts inside one stream, so it needs stream beside it.
Across streams you get the order events were recorded, which is not always occurred_at order. Sort on it yourself where that matters.
Read one at GET /v1/payroll/events/{event_id}.
Query parameters
stream
string
optionalThe id of the record whose history you want: a run's (run_enterprise_…, from POST /v1/payroll/runs) or a payslip's (run_enterprise_payslip_…, from GET /v1/payroll/payslips). Leave it out to page every stream with starting_after.
after
integer
optionalOnly events whose sequence is above this number in the stream you named, so pass the last sequence you handled; 0, or leaving it out, reads from the start. Needs stream beside it, because a sequence counts within one stream.
limit
integer
optionalHow many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.
starting_after
string
optionalThe id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.
Returns
A page of events, each dated by when it happened and not by when you were told.
object
always "list"
requiredAlways list. Tells you which kind of record you are looking at, so one handler can read any response.
data
array of StoredEvent
requiredThe records on this page, in the order the list promises. Empty when nothing matched.
13 fields of StoredEvent
id
string
requiredThe event's identifier, starting with evt_, the same on a webhook delivery and on the module's events list, such as GET /v1/payroll/events. It never changes: a redelivery carries the same id, so you can recognise an event you have already handled.
type
string
requiredWhat happened, as module.resource.past_tense_verb, for example run.payslip.calculated. Pick your handler on it; data takes the shape this type promises.
schema_version
integer · minimum 1
requiredThe version of the shape data takes for this type, starting at 1. A change to the shape raises it, so check it before you read data.
org_id
string
requiredThe organisation the event belongs to, by its id, which starts with org_: the one POST /v1/registrations gave you and GET /v1/me returns. You only ever receive events for your own organisation.
sequence
integer · minimum 0
requiredPer organisation and per stream. It is how a consumer tells a replay from a new event, and it is what the delivery guarantee rests on.
occurred_at
string · date-time
requiredWhen the event happened, as an RFC 3339 timestamp in UTC. Not when it was delivered: a redelivery carries the original value.
request_id
string
optionalThe request that caused this event, where one did: the Droomwork-Request-Id that request returned, starting with req_. Absent for an event a schedule raised, such as an engagement lapsing on its end date.
livemode
boolean
requiredWhich realm the event happened in. False is the sandbox.
mocked
boolean
requiredWhether a mock produced this fact, rather than an engine computing it. Recorded on the event when it was appended and never worked out afterwards from the realm: the two answers agree while every module is on its mock and part on the day the first engine ships. See ADR-0011.
source
string
requiredWhich part of Heros is the authority for this fact: anchor (identity), proof (credentials), rail (engagements), flow (sourcing), match (allocation), run (payroll), remit (remittance), route (payouts), gateway (the API's front door), iam (accounts and API keys), ledger (the books), registry (rule packs), delivery (webhooks and messages), documents (rendered payslips and instruments) or intelligence (AI decisions). Read the fact from there when it matters; your own copy is never the authority.
anchorproofrailflowmatchrunremitroutegatewayiamledgerregistrydeliverydocumentsintelligenceobject
always "event"
requiredAlways event. Tells you which kind of record you are looking at, so one handler can read any response.
stream
string
requiredThe stream the event belongs to: the id of the record it's about, a run's (run_enterprise_…) or a payslip's. Pass it as stream at GET /v1/payroll/events to replay it; sequence counts per stream and means nothing without this.
data
object
requiredThe record the event is about, in the shape its type names: a run, a finding, a payslip or an instruction set. The same body a webhook delivery carries.
has_more
boolean
requiredtrue when there are more records after this page. Pass the last record's id as starting_after to get the next page.
Other responses
Errors it can return
# query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payroll/events?stream=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(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.payrollEventsList({ stream: 'run_enterprise_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/payroll/events?stream=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
# query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
result = api.payroll_events_list(stream='run_enterprise_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/payroll/events?stream=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
# query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
$result = $api->payrollEventsList(stream: 'run_enterprise_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/payroll/events?stream=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
var result = api.payrollEventsList("run_enterprise_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/payroll/events?stream=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
var result = api.PayrollEventsList(stream: "run_enterprise_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/payroll/events?stream=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
result, _, err := client.RUNAPI.PayrollEventsList(ctx).Stream("run_enterprise_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/payroll/events?stream=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"object": "list",
"data": [
{
"id": "evt_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"type": "run.payslip.calculated",
"schema_version": 1,
"org_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"sequence": 0,
"occurred_at": "2026-09-01T09:00:00Z",
"livemode": false,
"mocked": true,
"source": "anchor",
"request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "event",
"stream": "example",
"data": {}
}
],
"has_more": true
}
/v1/payroll/audit_entries/{audit_entry_id}#Check one attempt and how it ended
payroll.audit_entries.retrieve
An audit entry is one attempt to change something, and this reads one back. So an audit screen in your product can open one line and name who approved a run, without reading the whole list.
Send the entry's id in the path. You get one entry: when it happened, who acted, what they attempted, and the status they were given. Another organisation's id answers not_found, not forbidden.
An attempt ends three ways: it went through, it was turned down, or it failed on our side. A screen offering only allowed and denied will call our failures refusals. An entry never says what changed, or the reason behind a refusal.
Open what resource and resource_id name: a run at GET /v1/payroll/runs/{run_id}, a payslip at GET /v1/payroll/payslips/{payslip_id}. Read entries in order at GET /v1/payroll/audit_entries.
Path parameters
audit_entry_id
string
requiredThe entry's identifier, from the id of an entry you listed at GET /v1/payroll/audit_entries. It starts with aud_.
Returns
The audit entry.
id
string
requiredThe entry's identifier, as GET /v1/payroll/audit_entries lists it. It starts with aud_ and never changes; pass it as audit_entry_id to GET /v1/payroll/audit_entries/{audit_entry_id}, or as starting_after to page past it.
object
always "audit_entry"
requiredAlways audit_entry. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm the action happened in. False is the sandbox.
mocked
boolean
requiredWhether the route this attempt was aimed at is mocked. It describes the route, not the answer: a refused attempt and a replayed idempotent request are both recorded as mocked when the route is. It is never derived from livemode.
at
string · date-time
requiredWhen the attempt was recorded, as an RFC 3339 timestamp in UTC. The list orders entries by it, newest first, and recorded_after and recorded_before bound it.
request_id
string
requiredThe request that made this attempt. It starts with req_ and is the same request_id an error body carries, so a refusal you were shown can be matched to its entry here.
actor_type
string
requiredWhat kind of credential acted: client for your API key or OAuth client, user for a signed-in person, staff for a Heros staff member on a support case, service when Heros acted for you.
actor_id
string
requiredWho acted, by actor_type: for client your API key's id (key_…, from GET /v1/api_keys) or OAuth client_id; for user the person's id (usr_…); for staff theirs; for service a name. Pass it as actor_id on the list to follow one.
action
string
requiredWhat was attempted, as the method and the route pattern.
resource
string · nullable
optionalThe kind of record the attempt was aimed at, as the collection name in the route, such as runs or payslips. null when the route named none.
resource_id
string · nullable
optionalThe id of the record the route named, such as a run's (run_enterprise_…, from POST /v1/payroll/runs) or a payslip's (run_enterprise_payslip_…, from GET /v1/payroll/payslips). null when the route named none, as on a create or a list.
outcome
string
requiredHow the attempt ended: succeeded for a status below 400, refused for a 4xx, failed for a 5xx. Pass refused as outcome on GET /v1/payroll/audit_entries to see every attempt that was turned away.
succeededrefusedfailedstatus
integer
requiredThe HTTP status the caller was given, such as 201 or 403. outcome is read from it, so the two never disagree.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/payroll/audit_entries/%7Baudit_entry_id%7D" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollAuditEntriesRetrieve({ auditEntryId: '{audit_entry_id}' });const response = await fetch('https://sandbox.droomwork.io/v1/payroll/audit_entries/%7Baudit_entry_id%7D', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_audit_entries_retrieve(audit_entry_id='{audit_entry_id}')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/audit_entries/%7Baudit_entry_id%7D',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollAuditEntriesRetrieve(audit_entry_id: '{audit_entry_id}');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/audit_entries/%7Baudit_entry_id%7D');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollAuditEntriesRetrieve("{audit_entry_id}");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/audit_entries/%7Baudit_entry_id%7D"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollAuditEntriesRetrieve(auditEntryId: "{audit_entry_id}");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/audit_entries/%7Baudit_entry_id%7D");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollAuditEntriesRetrieve(ctx, "{audit_entry_id}").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/audit_entries/%7Baudit_entry_id%7D", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "aud_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "audit_entry",
"livemode": false,
"mocked": true,
"at": "2026-09-01T09:00:00Z",
"request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"actor_type": "example",
"actor_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"action": "example",
"outcome": "succeeded",
"status": 1,
"resource": "runs",
"resource_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
/v1/payroll/runs/{run_id}/execute#Execute an approved run
payroll.runs.execute
This turns an approved run into the payslips themselves, one per payee, where simulating wrote none. So the figures somebody approved are the figures you pay, and conflict is what you get instead.
Send the run's id in the path and no request body. You get the run back with its figures recomputed and one payslip written for every payee.
Approve it first at POST /v1/payroll/runs/{run_id}/approve and resolve every p0 finding at POST /v1/payroll/runs/{run_id}/findings/{finding_id}/resolve. Skipping either gives you conflict, as do a run naming no roster entry and one pinned to a draft rule pack version.
Every payee must pass the payment gate: one who fails refuses the whole run, subject_not_anchored for identity and prerequisite_not_met otherwise. Each roster entry's payable and blocked_by name who and why.
Cancelling works before this and not after. List what it wrote at GET /v1/payroll/payslips with run_id.
Path parameters
run_id
string
requiredThe run's identifier, from the id of the response to POST /v1/payroll/runs or of a run on GET /v1/payroll/runs. It starts with run_enterprise_.
Headers
Idempotency-Key
string
requiredA key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.
Returns
Execution accepted. Poll the run, or wait for run.completed.
id
string
requiredThe run's identifier. It starts with run_enterprise_, is assigned when you create the run at POST /v1/payroll/runs and never changes; pass it as run_id wherever a call names this run.
object
always "payroll_run"
requiredAlways payroll_run. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
status
string
requiredYou move a run between states with endpoints, not by setting this field.
draftsimulatedpending_approvalapprovedexecutingcompletedpartially_settledfailedcancelledrun_type
string
requiredFixed at creation and immutable afterwards.
regularoff_cyclebonusthirteenth_monthcorrectionfinal_settlementperiod
string
requiredThe pay period this run covers, as year and month in YYYY-MM form, for example 2026-09.
payee_count
integer · minimum 0
optionalHow many payees are on this run, one for each roster entry it covers. A whole number, 0 or more.
rule_pack
RulePackReference
optional3 fields of RulePackReference
pack_id
string
requiredThe family of rules the pack belongs to, such as ng-payroll. It's pinned when you create the run at POST /v1/payroll/runs and, with version, names exactly which rates, bands and thresholds were applied.
version
string
requiredThe pack version that was applied, in the form 2026.08.1. A version never changes once active, so the same version always means the same rules.
content_hash
string
requiredA digest of the pack's content, prefixed sha256:. It proves which exact rules produced the figure, so an edited pack can never pass as the one you ran under.
totals
RunTotals
optionalEach figure is the sum of its payslip lines, never a percentage of an aggregate.
5 fields of RunTotals
gross
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
net
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
total_deductions
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
employer_contributions
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
payslip_count
integer · minimum 0
optionalHow many payslips these totals are summed from. On a simulation, how many the run would produce; nothing is written.
prepared_by
string · nullable
optionalWho prepared this run, as their user identifier. Above the configured maker-checker threshold the same person can't approve it, so their approve call is refused.
approved_by
string · nullable
optionalNever the same actor as prepared_by above the maker-checker threshold.
created_at
string · date-time
requiredWhen the record was created, as an RFC 3339 timestamp in UTC.
executed_at
string · date-time · nullable
optionalWhen the run was executed, as an RFC 3339 timestamp in UTC. null until it has been.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollRunsExecute({ runId: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Idempotency-Key': crypto.randomUUID(),
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_runs_execute(run_id='run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollRunsExecute(run_id: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollRunsExecute("run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.header("Idempotency-Key", UUID.randomUUID().toString())
.method("POST", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollRunsExecute(runId: "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollRunsExecute(ctx, "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "payroll_run",
"livemode": false,
"mocked": true,
"status": "executing",
"run_type": "regular",
"period": "2026-09",
"created_at": "2026-09-01T09:00:00Z",
"payee_count": 0,
"rule_pack": {
"pack_id": "ng-payroll",
"version": "2026.08.1",
"content_hash": "sha256:9f2c1e00"
},
"totals": {
"gross": {
"amount": 52500000,
"currency": "NGN"
},
"net": {
"amount": 41250000,
"currency": "NGN"
},
"total_deductions": {
"amount": 11250000,
"currency": "NGN"
},
"employer_contributions": {
"amount": 5400000,
"currency": "NGN"
},
"payslip_count": 0
},
"prepared_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"approved_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"executed_at": "2026-09-01T09:00:00Z"
}
/v1/payroll/runs/{run_id}/findings/{finding_id}#Open one finding and its answer
payroll.run_findings.retrieve
A finding is one thing questioned about a run. So a review screen in your product can open a single flagged item and say what was found, without listing the run's findings again.
Send the run's id and the finding's id in the path. You get the finding in full: code to match on, detail for people, the payee in subject_id, and whether somebody has cleared it, with their reason code.
Clear it at POST /v1/payroll/runs/{run_id}/findings/{finding_id}/resolve. This call only reads, and resolving does not delete the finding, so a resolved one still answers here. A finding id from another run answers not_found.
Path parameters
run_id
string
requiredThe run's identifier, from the id of the response to POST /v1/payroll/runs or of a run on GET /v1/payroll/runs. It starts with run_enterprise_.
finding_id
string
requiredThe finding's identifier, from the id of a finding on GET /v1/payroll/runs/{run_id}/findings for the same run. Pair it with that run's run_id.
Returns
One finding in full, the record to quote when somebody asks why it was cleared.
id
string
requiredThe finding's identifier, as it reads on each finding from GET /v1/payroll/runs/{run_id}/findings. It never changes; pass it as finding_id in the path when you retrieve or resolve the finding.
object
always "run_finding"
requiredAlways run_finding. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
run_id
string
requiredThe id of the run this finding was raised on, starting with run_enterprise_, as returned by POST /v1/payroll/runs. A p0 finding blocks that run's approval and execution until you resolve it.
severity
string
requiredOnly p0 blocks approval and execution.
p0p1p2status
string
requiredopenresolvedcode
string
requiredA stable code naming what was found, for example net_pay_variance_above_threshold. Match on this in your own handling; detail is for people.
detail
string
requiredA sentence for a person saying what was found and why it matters, for example that a payee has no verified destination. Don't match on it; use code.
subject_id
string · nullable
optionalWho the finding is about, when it concerns one payee: their subject identifier, starting with sub_, as you sent it on their roster entry at POST /v1/payroll/roster_entries. null when it concerns the run as a whole.
resolved_by
string · nullable
optionalWho resolved the finding, as their user identifier, kept permanently with the reason code. null while it's still open.
reason_code
string · nullable
optionalThe reason code sent when the finding was resolved, for example no_payee_destination. null while it's still open.
resolved_at
string · date-time · nullable
optionalWhen the finding was resolved, as an RFC 3339 timestamp in UTC. null while it's still open.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings/%7Bfinding_id%7D" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollRunFindingsRetrieve({ runId: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', findingId: '{finding_id}' });const response = await fetch('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings/%7Bfinding_id%7D', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_run_findings_retrieve(run_id='run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', finding_id='{finding_id}')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings/%7Bfinding_id%7D',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollRunFindingsRetrieve(run_id: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', finding_id: '{finding_id}');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings/%7Bfinding_id%7D');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollRunFindingsRetrieve("run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{finding_id}");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings/%7Bfinding_id%7D"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollRunFindingsRetrieve(runId: "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", findingId: "{finding_id}");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings/%7Bfinding_id%7D");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollRunFindingsRetrieve(ctx, "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{finding_id}").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings/%7Bfinding_id%7D", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "run_finding",
"livemode": false,
"mocked": true,
"run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"severity": "p0",
"status": "open",
"code": "net_pay_variance_above_threshold",
"detail": "The payee has no verified destination, so this line cannot be paid.",
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"resolved_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"reason_code": "no_payee_destination",
"resolved_at": "2026-09-01T09:00:00Z"
}
/v1/payroll/events/{event_id}#Check one event by its identifier
payroll.events.retrieve
An event identifier on its own, from your log or a delivery row, becomes the whole record here: what the event said, and which run or payslip it is about. So a receiver that handled one event can show a reader the record behind it and where that event sits in its history.
Send the event's id in the path. You get the event back with its data, the stream it belongs to and its sequence in that stream.
This log holds only what RUN is the authority for. An event another module raised is read on that module's own events list, and asking for it here answers not_found.
Carry stream and sequence to GET /v1/payroll/events as stream and after to read everything that followed in that record's history. Another organisation's id answers not_found, not forbidden.
Path parameters
event_id
string
requiredThe event's identifier: the id of an event you listed at GET /v1/payroll/events or received on a webhook delivery. It starts with evt_.
Returns
The whole event, whose type and schema_version say what shape data takes before you read it.
id
string
requiredThe event's identifier, starting with evt_, the same on a webhook delivery and on the module's events list, such as GET /v1/payroll/events. It never changes: a redelivery carries the same id, so you can recognise an event you have already handled.
type
string
requiredWhat happened, as module.resource.past_tense_verb, for example run.payslip.calculated. Pick your handler on it; data takes the shape this type promises.
schema_version
integer · minimum 1
requiredThe version of the shape data takes for this type, starting at 1. A change to the shape raises it, so check it before you read data.
org_id
string
requiredThe organisation the event belongs to, by its id, which starts with org_: the one POST /v1/registrations gave you and GET /v1/me returns. You only ever receive events for your own organisation.
sequence
integer · minimum 0
requiredPer organisation and per stream. It is how a consumer tells a replay from a new event, and it is what the delivery guarantee rests on.
occurred_at
string · date-time
requiredWhen the event happened, as an RFC 3339 timestamp in UTC. Not when it was delivered: a redelivery carries the original value.
request_id
string
optionalThe request that caused this event, where one did: the Droomwork-Request-Id that request returned, starting with req_. Absent for an event a schedule raised, such as an engagement lapsing on its end date.
livemode
boolean
requiredWhich realm the event happened in. False is the sandbox.
mocked
boolean
requiredWhether a mock produced this fact, rather than an engine computing it. Recorded on the event when it was appended and never worked out afterwards from the realm: the two answers agree while every module is on its mock and part on the day the first engine ships. See ADR-0011.
source
string
requiredWhich part of Heros is the authority for this fact: anchor (identity), proof (credentials), rail (engagements), flow (sourcing), match (allocation), run (payroll), remit (remittance), route (payouts), gateway (the API's front door), iam (accounts and API keys), ledger (the books), registry (rule packs), delivery (webhooks and messages), documents (rendered payslips and instruments) or intelligence (AI decisions). Read the fact from there when it matters; your own copy is never the authority.
anchorproofrailflowmatchrunremitroutegatewayiamledgerregistrydeliverydocumentsintelligenceobject
always "event"
requiredAlways event. Tells you which kind of record you are looking at, so one handler can read any response.
stream
string
requiredThe stream the event belongs to: the id of the record it's about, a run's (run_enterprise_…) or a payslip's. Pass it as stream at GET /v1/payroll/events to replay it; sequence counts per stream and means nothing without this.
data
object
requiredThe record the event is about, in the shape its type names: a run, a finding, a payslip or an instruction set. The same body a webhook delivery carries.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/payroll/events/%7Bevent_id%7D" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollEventsRetrieve({ eventId: '{event_id}' });const response = await fetch('https://sandbox.droomwork.io/v1/payroll/events/%7Bevent_id%7D', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_events_retrieve(event_id='{event_id}')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/events/%7Bevent_id%7D',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollEventsRetrieve(event_id: '{event_id}');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/events/%7Bevent_id%7D');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollEventsRetrieve("{event_id}");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/events/%7Bevent_id%7D"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollEventsRetrieve(eventId: "{event_id}");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/events/%7Bevent_id%7D");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollEventsRetrieve(ctx, "{event_id}").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/events/%7Bevent_id%7D", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "evt_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"type": "run.payslip.calculated",
"schema_version": 1,
"org_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"sequence": 0,
"occurred_at": "2026-09-01T09:00:00Z",
"livemode": false,
"mocked": true,
"source": "anchor",
"request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "event",
"stream": "example",
"data": {}
}
/v1/payroll/runs/{run_id}/findings/{finding_id}/resolve#Clear a finding and record why
payroll.run_findings.resolve
This clears one of a run's findings, recording who accepted it and why. So a reviewer in your product can take a blocked payroll forward, with a permanent answer behind each question.
Send the run's id and the finding's id in the path with the reason code you are clearing it under. You get it back as resolved, carrying that code, your user identifier and the time.
A finding has to exist. Simulating the run at POST /v1/payroll/runs/{run_id}/simulate raises them, and you read their identifiers at GET /v1/payroll/runs/{run_id}/findings.
Only a p0 finding blocks a run's approval and execution, and resolving it is what clears the way for POST /v1/payroll/runs/{run_id}/approve. Simulating again leaves a resolved finding resolved.
A missing or empty reason_code is refused with invalid_request, and a finding somebody has already resolved with conflict.
Path parameters
run_id
string
requiredThe run's identifier, from the id of the response to POST /v1/payroll/runs or of a run on GET /v1/payroll/runs. It starts with run_enterprise_.
finding_id
string
requiredThe finding's identifier, from the id of a finding on GET /v1/payroll/runs/{run_id}/findings for the same run. Pair it with that run's run_id.
Headers
Idempotency-Key
string
requiredA key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.
Body
reason_code
string
requiredWhy this finding is being cleared. Recorded permanently with the actor.
note
string
optionalFree text to keep with the resolution: what you checked, or why the reason code applies. Optional; reason_code is what's required.
Returns
The finding as it now reads, carrying no note: what you wrote with it never comes back.
id
string
requiredThe finding's identifier, as it reads on each finding from GET /v1/payroll/runs/{run_id}/findings. It never changes; pass it as finding_id in the path when you retrieve or resolve the finding.
object
always "run_finding"
requiredAlways run_finding. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
run_id
string
requiredThe id of the run this finding was raised on, starting with run_enterprise_, as returned by POST /v1/payroll/runs. A p0 finding blocks that run's approval and execution until you resolve it.
severity
string
requiredOnly p0 blocks approval and execution.
p0p1p2status
string
requiredopenresolvedcode
string
requiredA stable code naming what was found, for example net_pay_variance_above_threshold. Match on this in your own handling; detail is for people.
detail
string
requiredA sentence for a person saying what was found and why it matters, for example that a payee has no verified destination. Don't match on it; use code.
subject_id
string · nullable
optionalWho the finding is about, when it concerns one payee: their subject identifier, starting with sub_, as you sent it on their roster entry at POST /v1/payroll/roster_entries. null when it concerns the run as a whole.
resolved_by
string · nullable
optionalWho resolved the finding, as their user identifier, kept permanently with the reason code. null while it's still open.
reason_code
string · nullable
optionalThe reason code sent when the finding was resolved, for example no_payee_destination. null while it's still open.
resolved_at
string · date-time · nullable
optionalWhen the finding was resolved, as an RFC 3339 timestamp in UTC. null while it's still open.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings/%7Bfinding_id%7D/resolve" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"reason_code":"example","note":"example"}'import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollRunFindingsResolve({
runId: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
findingId: '{finding_id}',
idempotencyKey: crypto.randomUUID(),
runFindingResolveRequest: {"reasonCode":"example","note":"example"},
});const response = await fetch('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings/%7Bfinding_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({
"reason_code": "example",
"note": "example"
}),
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_run_findings_resolve(run_id='run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', finding_id='{finding_id}', body={"reason_code": "example", "note": "example"})import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings/%7Bfinding_id%7D/resolve',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"reason_code": "example", "note": "example"},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollRunFindingsResolve($idempotencyKey, json_decode('{"reason_code":"example","note":"example"}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings/%7Bfinding_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 => '{"reason_code":"example","note":"example"}',
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollRunFindingsResolve("run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{finding_id}", idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings/%7Bfinding_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("""
{
"reason_code": "example",
"note": "example"
}
"""))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollRunFindingsResolve(runId: "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", findingId: "{finding_id}", idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings/%7Bfinding_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("""
{
"reason_code": "example",
"note": "example"
}
""", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollRunFindingsResolve(ctx, "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{finding_id}").IdempotencyKey(key).RunFindingResolveRequest(body).Execute()body := strings.NewReader(`{
"reason_code": "example",
"note": "example"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings/%7Bfinding_id%7D/resolve", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "run_finding",
"livemode": false,
"mocked": true,
"run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"severity": "p0",
"status": "open",
"code": "net_pay_variance_above_threshold",
"detail": "The payee has no verified destination, so this line cannot be paid.",
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"resolved_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"reason_code": "no_payee_destination",
"resolved_at": "2026-09-01T09:00:00Z"
}
/v1/payroll/payslips#Find the payslips your runs produced
payroll.payslips.list
This gives you the payslips your runs have produced, newest first. So your product can show somebody their own pay history, or a finance team what one run came to.
Narrow the list to one run with run_id, to one person with subject_id, or to one person on one run by sending both; every filter is optional. Each payslip arrives whole, every line carrying the arithmetic that produced it.
This list takes no period, and a period can hold more than one run. Find a period's runs at GET /v1/payroll/runs?period=2026-09, then list each one's payslips here.
Payslips exist once a run executes at POST /v1/payroll/runs/{run_id}/execute. An unknown run_id or subject_id answers with an empty page, not a refusal. Five later calls name this id in the path, among them GET /v1/payroll/payslips/{payslip_id} and POST /v1/payroll/payslips/{payslip_id}/issue.
Query parameters
limit
integer
optionalHow many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.
starting_after
string
optionalThe id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.
run_id
string
optionalReturn only payslips written by this run: pass the run's id, starting with run_enterprise_, from POST /v1/payroll/runs or GET /v1/payroll/runs. Leave it out to get payslips from every run.
subject_id
string
optionalReturn only payslips for this person: pass their subject identifier, starting with sub_, the subject_id you sent on their roster entry at POST /v1/payroll/roster_entries. Leave it out to get payslips for everyone.
Returns
A page of payslips.
object
always "list"
requiredAlways list. Tells you which kind of record you are looking at, so one handler can read any response.
data
array of Payslip
requiredThe records on this page, in the order the list promises. Empty when nothing matched.
14 fields of Payslip
id
string
requiredThe payslip's identifier, starting with run_enterprise_payslip_. It's written when the run executes and first reaches you on GET /v1/payroll/payslips; it never changes, so pass it as payslip_id wherever a call names this payslip.
object
always "payslip"
requiredAlways payslip. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
run_id
string
requiredThe id of the run that wrote this payslip, starting with run_enterprise_, as returned by POST /v1/payroll/runs. Pass it as run_id on GET /v1/payroll/payslips to list every payslip from the same run.
subject_id
string
optionalWho this payslip is for: their subject identifier, starting with sub_, as you sent it on their roster entry at POST /v1/payroll/roster_entries. The same value is on their pay profile.
period
string
optionalThe pay period this payslip covers, as year and month, for example 2026-09. It matches the period of the run that wrote it.
gross
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
net
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
lines
array of PayslipLine
requiredEvery line on this payslip, each with its own calculation trace. Read kind to tell earnings from deductions, employer contributions and information lines.
7 fields of PayslipLine
id
string
requiredThe line's identifier, as it reads on each entry in lines on the payslip from GET /v1/payroll/payslips/{payslip_id}. Quote it when you ask about one figure on this payslip.
object
always "payslip_line"
requiredAlways payslip_line. Tells you which kind of record you are looking at, so one handler can read any response.
code
string
requiredWhich pay element this line is, as a code such as pension_employee. Use it to match lines across payslips and against the explanation.
kind
string
requiredearningdeductionemployer_contributioninformationamount
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
trace
CalculationTrace
requiredThe formula, the inputs, the intermediate values and the pack version behind one line. Every payslip line carries one, and any historical line replays to the kobo from it.
6 fields of CalculationTrace
formula
string
requiredThe calculation behind this line, written as its named inputs and operators, for example pensionable_earnings x pension_employee_rate. Each name appears in inputs.
inputs
array of object
requiredThe values the formula was evaluated with, each named as it appears in formula. An amount is Money in whole minor units and a rate is an exact fraction, so you can replay the line.
exact
Rate
requiredAn exact fraction of two whole numbers. Never a decimal, and it carries no currency.
2 fields of Rate
numerator
integer · int64
requiredThe top of the fraction, a whole number. 8 over 100 is eight percent.
denominator
integer · int64 · minimum 1
requiredThe bottom of the fraction, a whole number of 1 or more. Never zero, so every rate can be evaluated.
rounding
Rounding
required3 fields of Rounding
scale
string
requiredThe unit rounded to, always minor_unit: the kobo for NGN, so a rounded figure is always a whole number of minor units.
minor_unitmode
string
requiredHow a value between two minor units is settled: half_up takes a half up, half_down takes it down, half_even takes it to the even neighbour, ceiling always rounds up, floor always down, truncate drops the fraction. The rule pack sets it for each line; there's no default.
half_uphalf_evenhalf_downceilingfloortruncatepoint
string
requiredWhere rounding was applied: per_line rounds each line on its own, per_total rounds once on the total. The rule pack sets it for each line, and the two can differ by real money.
per_lineper_totalresult
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
rule_pack
RulePackReference
required3 fields of RulePackReference
pack_id
string
requiredThe family of rules the pack belongs to, such as ng-payroll. It's pinned when you create the run at POST /v1/payroll/runs and, with version, names exactly which rates, bands and thresholds were applied.
version
string
requiredThe pack version that was applied, in the form 2026.08.1. A version never changes once active, so the same version always means the same rules.
content_hash
string
requiredA digest of the pack's content, prefixed sha256:. It proves which exact rules produced the figure, so an edited pack can never pass as the one you ran under.
rule_pack
RulePackReference
optional3 fields of RulePackReference
pack_id
string
requiredThe family of rules the pack belongs to, such as ng-payroll. It's pinned when you create the run at POST /v1/payroll/runs and, with version, names exactly which rates, bands and thresholds were applied.
version
string
requiredThe pack version that was applied, in the form 2026.08.1. A version never changes once active, so the same version always means the same rules.
content_hash
string
requiredA digest of the pack's content, prefixed sha256:. It proves which exact rules produced the figure, so an edited pack can never pass as the one you ran under.
signature
string
optionalThe signature over this payslip, as base64, made with the key named by signing_key_id. What it covers is a record of what was paid, so it stays valid however the payslip is rendered. Absent when this payslip carries no signature, and one is never added after the fact.
signing_key_id
string
optionalWhich key made signature, such as sgk_01M1E34QM07M2N5P8R3T6V9W1Y. Get its public half from GET /v1/signing_keys/{signing_key_id}, which needs no credential, and verify the signature with that. No endpoint creates or lists keys: one is written when a payslip is signed and reaches you here. Keys are replaced over time and a payslip keeps naming the one that signed it, so an older payslip still verifies. Absent when this payslip carries no signature.
created_at
string · date-time
optionalWhen the record was created, as an RFC 3339 timestamp in UTC.
has_more
boolean
requiredtrue when there are more records after this page. Pass the last record's id as starting_after to get the next page.
Other responses
Errors it can return
# query parameters: limit (optional), starting_after (optional), run_id (optional), subject_id (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payroll/payslips?limit=25&run_id=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&subject_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
// query parameters: limit (optional), starting_after (optional), run_id (optional), subject_id (optional)
const result = await api.payrollPayslipsList({ limit: 25, runId: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', subjectId: 'sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });// query parameters: limit (optional), starting_after (optional), run_id (optional), subject_id (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/payroll/payslips?limit=25&run_id=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&subject_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
# query parameters: limit (optional), starting_after (optional), run_id (optional), subject_id (optional)
result = api.payroll_payslips_list(limit=25, run_id='run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', subject_id='sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import requests
# query parameters: limit (optional), starting_after (optional), run_id (optional), subject_id (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/payslips?limit=25&run_id=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&subject_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
# query parameters: limit (optional), starting_after (optional), run_id (optional), subject_id (optional)
$result = $api->payrollPayslipsList(limit: 25, run_id: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', subject_id: 'sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
// query parameters: limit (optional), starting_after (optional), run_id (optional), subject_id (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/payslips?limit=25&run_id=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&subject_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
// query parameters: limit (optional), starting_after (optional), run_id (optional), subject_id (optional)
var result = api.payrollPayslipsList(25, null, "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");// query parameters: limit (optional), starting_after (optional), run_id (optional), subject_id (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/payslips?limit=25&run_id=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&subject_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
// query parameters: limit (optional), starting_after (optional), run_id (optional), subject_id (optional)
var result = api.PayrollPayslipsList(limit: 25, runId: "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", subjectId: "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");// query parameters: limit (optional), starting_after (optional), run_id (optional), subject_id (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/payslips?limit=25&run_id=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&subject_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: limit (optional), starting_after (optional), run_id (optional), subject_id (optional)
result, _, err := client.RUNAPI.PayrollPayslipsList(ctx).Limit(25).RunId("run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").SubjectId("sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()// query parameters: limit (optional), starting_after (optional), run_id (optional), subject_id (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/payslips?limit=25&run_id=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&subject_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"object": "list",
"data": [
{
"id": "run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "payslip",
"livemode": false,
"mocked": true,
"run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"gross": {
"amount": 52500000,
"currency": "NGN"
},
"net": {
"amount": 41250000,
"currency": "NGN"
},
"lines": [
{
"id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "payslip_line",
"code": "pension_employee",
"kind": "earning",
"amount": {
"amount": 7500000,
"currency": "NGN"
},
"mocked": true,
"trace": {
"formula": "pensionable_earnings x pension_employee_rate",
"inputs": [
{}
],
"exact": {
"numerator": 1,
"denominator": 1
},
"rounding": {
"scale": "minor_unit",
"mode": "half_up",
"point": "per_line"
},
"result": {
"amount": 7500000,
"currency": "NGN"
},
"rule_pack": {
"pack_id": "ng-payroll",
"version": "2026.08.1",
"content_hash": "sha256:9f2c1e00"
}
}
}
],
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"period": "2026-09",
"rule_pack": {
"pack_id": "ng-payroll",
"version": "2026.08.1",
"content_hash": "sha256:9f2c1e00"
},
"signature": "P6ngn83ZJBAJr7ketLYJ7pRIU0WlMIeFh55g92shkhIJAsHNF9UI2dyMj6e4mwXi/sY6mB9n5/lWOvGDPsPugg==",
"signing_key_id": "sgk_01M1E34QM07M2N5P8R3T6V9W1Y",
"created_at": "2026-09-01T09:00:00Z"
}
],
"has_more": true
}
/v1/payroll/runs/{run_id}/instruction_set#What a run owes beyond the payees
payroll.instruction_sets.retrieve
This gives you what one payroll run owes beyond the payees themselves: the tax, the pension and the levies. So a finance team gets one list of what a run owes out, and nobody retypes a figure.
Send the run's id in the path, and you get PAYE lines, pension lines and one line per levy, each with its amount and a reconciliation_reference for your records. Another organisation's id answers not_found, not forbidden.
The set is the run's, not the month's: remit one for every run a month holds. Send it on as instructions on POST /v1/remittance/instruction_sets, with the month as period and the run's id as run_id.
Read mocked before you act on a line's authority or administrator: while it is true, both are stand-ins.
Path parameters
run_id
string
requiredThe run's identifier, from the id of the response to POST /v1/payroll/runs or of a run on GET /v1/payroll/runs. It starts with run_enterprise_.
Returns
The set for this run, with PAYE grouped by each employee's home state and not by yours.
id
string
requiredThe instruction set's identifier, which never changes: read it at GET /v1/payroll/runs/{run_id}/instruction_set when Heros ran the payroll, and send the set on unchanged to POST /v1/remittance/instruction_sets. Quote it when you ask about the set.
object
always "instruction_set"
requiredAlways instruction_set. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
run_id
string
requiredThe payroll run this set is owed for, by its id from POST /v1/payroll/runs, which starts with run_enterprise_. The set's totals reconcile to that run's totals to the kobo.
period
string
optionalThe pay period the amounts are owed for, as year and month, for example 2026-09.
paye
array of object
requiredGrouped by the State IRS of the employee's residence, not the employer's location.
5 fields
authority_id
string
requiredThe State Internal Revenue Service this PAYE is owed to, by its id from GET /v1/remittance/authorities, which starts with remit_authority_rail_obligation_. Use it wherever a call names the authority.
jurisdiction
string
requiredThe State Internal Revenue Service this line is grouped under, named in full, for example Rivers State Internal Revenue Service. Read it to label the line; authority_id names the authority in a call.
amount
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
payee_count
integer
optionalHow many employees this line's PAYE covers. Optional; when it is present, check it against your own headcount for that state.
reconciliation_reference
string
requiredThe reference that identifies this line when you reconcile it, for example paye-2026-09-rivers. Keep it with your own records so you can trace the line later.
pension
array of object
requiredGrouped by administrator and retirement savings account number.
6 fields
administrator_id
string
requiredThe Pension Fund Administrator these contributions are owed to, by its id from GET /v1/remittance/authorities with kind=pension_administrator, which starts with remit_authority_rail_obligation_. Use it wherever a call names the administrator.
administrator_name
string
optionalThe administrator's name in full, so you can label the line without a second call. Optional; administrator_id is what names the administrator in a call.
employee_amount
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
employer_amount
Money
optional2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
amount
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
reconciliation_reference
string
requiredThe reference that identifies this line when you reconcile it. Keep it with your own records so you can trace the line later.
levies
array of object
requiredNHF, NSITF, ITF and NHIA totals, each with a reconciliation reference.
3 fields
code
string
requiredWhich levy this line is for: nhf the National Housing Fund, nsitf the Nigeria Social Insurance Trust Fund, itf the Industrial Training Fund, nhia the National Health Insurance Authority. One line per levy.
nhfnsitfitfnhiaamount
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
reconciliation_reference
string
requiredThe reference that identifies this levy line when you reconcile it. Keep it with your own records so you can trace the line later.
totals
object
requiredThe set's grand total, which its lines sum to. From a Heros run it reconciles to the run's totals to the kobo.
1 field
total
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
created_at
string · date-time
optionalWhen the record was created, as an RFC 3339 timestamp in UTC.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruction_set" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollInstructionSetsRetrieve({ runId: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruction_set', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_instruction_sets_retrieve(run_id='run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruction_set',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollInstructionSetsRetrieve(run_id: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruction_set');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollInstructionSetsRetrieve("run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruction_set"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollInstructionSetsRetrieve(runId: "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruction_set");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollInstructionSetsRetrieve(ctx, "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruction_set", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "instruction_set",
"livemode": false,
"mocked": true,
"run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"paye": [
{
"authority_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"jurisdiction": "Rivers State Internal Revenue Service",
"amount": {
"amount": 7500000,
"currency": "NGN"
},
"reconciliation_reference": "paye-2026-09-rivers",
"payee_count": 1
}
],
"pension": [
{
"administrator_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"amount": {
"amount": 7500000,
"currency": "NGN"
},
"reconciliation_reference": "paye-2026-09-rivers",
"administrator_name": "example",
"employee_amount": {
"amount": 4200000,
"currency": "NGN"
},
"employer_amount": {
"amount": 5400000,
"currency": "NGN"
}
}
],
"levies": [
{
"code": "nhf",
"amount": {
"amount": 7500000,
"currency": "NGN"
},
"reconciliation_reference": "paye-2026-09-rivers"
}
],
"totals": {
"total": {
"amount": 57900000,
"currency": "NGN"
}
},
"period": "2026-09",
"created_at": "2026-09-01T09:00:00Z"
}
/v1/payroll/runs/{run_id}/gl_export#Ask for a general ledger export
payroll.gl_exports.create
This asks for a run's journal in the shape your accounting system reads, and answers its two sides: what the payroll costs and what it owes. So your product can tell a finance team that a run's debits and credits agree, and catch one whose figures do not.
Send the run's id in the path and the format you want. You get the export back with its own id, starting run_enterprise_gl_export_, and the two totals. Ask again with another format for a second export of the same run.
A run has to have executed, at POST /v1/payroll/runs/{run_id}/execute.
Nothing refuses you before that: a run with no payslips answers two zeros and balanced true. When balanced is false, that run's own lines disagree, and its payslips are at GET /v1/payroll/payslips.
Read the export again at GET /v1/payroll/runs/{run_id}/gl_export/{gl_export_id}, sending its id as gl_export_id.
Path parameters
run_id
string
requiredThe run's identifier, from the id of the response to POST /v1/payroll/runs or of a run on GET /v1/payroll/runs. It starts with run_enterprise_.
Headers
Idempotency-Key
string
requiredA key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.
Body
format
string
requiredThe shape your accounting system reads: csv or json for a plain file, xero or quickbooks for a file shaped for that system to import. You get it back as format on the export.
csvjsonxeroquickbookschart_of_accounts_id
string
optionalYour own identifier for the chart of accounts in your accounting system, when it needs the journal lines coded to your account codes. It's yours, not one we issue; leave it out when the lines don't need your codes.
Returns
The export, and no field on it records the chart_of_accounts_id you sent.
id
string
requiredThe export's identifier, from POST /v1/payroll/runs/{run_id}/gl_export. It starts with run_enterprise_gl_export_ and never changes; pass it as gl_export_id, with the same run_id, to fetch the export again.
object
always "gl_export"
requiredAlways gl_export. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
run_id
string
requiredThe run this export was produced from: its id, starting with run_enterprise_, from POST /v1/payroll/runs, as you named it in the path when you produced the export. Its totals reconcile to that run's totals to the kobo.
format
string
requiredThe format you asked for as format at POST /v1/payroll/runs/{run_id}/gl_export: csv, json, xero or quickbooks. It's what the file at download_url is in.
csvjsonxeroquickbooksstatus
string
requiredgenerating while the file is being produced, ready once download_url points at it, failed if it couldn't be produced. Fetch the export again at GET /v1/payroll/runs/{run_id}/gl_export/{gl_export_id} until it leaves generating.
generatingreadyfaileddownload_url
string · uri · nullable
optionalWhere to fetch the file, as a URL, once status is ready. null before that and when the export failed.
totals
object
requiredThe export's grand totals: what it debits, what it credits, and whether the two agree. Both sides reconcile to the run's totals to the kobo.
3 fields
debits
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
credits
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
balanced
boolean
requiredDebits equal credits, and both reconcile to run totals to the kobo.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/gl_export" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"format":"csv","chart_of_accounts_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}'import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollGlExportsCreate({
runId: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
idempotencyKey: crypto.randomUUID(),
runGlExportCreateRequest: {"format":"csv","chartOfAccountsId":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"},
});const response = await fetch('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/gl_export', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"format": "csv",
"chart_of_accounts_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}),
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_gl_exports_create(run_id='run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"format": "csv", "chart_of_accounts_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"})import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/gl_export',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"format": "csv", "chart_of_accounts_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollGlExportsCreate($idempotencyKey, json_decode('{"format":"csv","chart_of_accounts_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/gl_export');
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 => '{"format":"csv","chart_of_accounts_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}',
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollGlExportsCreate("run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/gl_export"))
.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("""
{
"format": "csv",
"chart_of_accounts_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
"""))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollGlExportsCreate(runId: "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/gl_export");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"format": "csv",
"chart_of_accounts_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
""", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollGlExportsCreate(ctx, "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).RunGlExportCreateRequest(body).Execute()body := strings.NewReader(`{
"format": "csv",
"chart_of_accounts_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/gl_export", body)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "gl_export",
"livemode": false,
"mocked": true,
"run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"format": "csv",
"status": "generating",
"totals": {
"debits": {
"amount": 57900000,
"currency": "NGN"
},
"credits": {
"amount": 57900000,
"currency": "NGN"
},
"balanced": true
},
"download_url": "https://files.sandbox.droomwork.com/example"
}
/v1/payroll/payslips/{payslip_id}#Check one payslip and the working behind it
payroll.payslips.retrieve
This reads back one person's pay for one period: the gross, the net and every line the payslip carries, each with the formula and values behind it. So a payslip view in your product can be built from a stored id, with the working included.
Send the payslip's id in the path, the one starting run_enterprise_payslip_ that GET /v1/payroll/payslips lists, and nothing else. You get that one payslip. A payslip has to exist, from a run that executed.
Read kind before you add lines up: employer contributions and information lines are in neither the gross nor the net.
The same payslip in words is at GET /v1/payroll/payslips/{payslip_id}/explanation, and as a PDF at GET /v1/payroll/payslips/{payslip_id}/document. To show a figure from months ago still stands up, recompute it at POST /v1/payroll/payslips/{payslip_id}/replay. Another organisation's id answers not_found, not forbidden.
Path parameters
payslip_id
string
requiredThe payslip's identifier, from the id of a payslip on GET /v1/payroll/payslips. It starts with run_enterprise_payslip_.
Returns
The payslip, with a traced line for every element.
id
string
requiredThe payslip's identifier, starting with run_enterprise_payslip_. It's written when the run executes and first reaches you on GET /v1/payroll/payslips; it never changes, so pass it as payslip_id wherever a call names this payslip.
object
always "payslip"
requiredAlways payslip. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
run_id
string
requiredThe id of the run that wrote this payslip, starting with run_enterprise_, as returned by POST /v1/payroll/runs. Pass it as run_id on GET /v1/payroll/payslips to list every payslip from the same run.
subject_id
string
optionalWho this payslip is for: their subject identifier, starting with sub_, as you sent it on their roster entry at POST /v1/payroll/roster_entries. The same value is on their pay profile.
period
string
optionalThe pay period this payslip covers, as year and month, for example 2026-09. It matches the period of the run that wrote it.
gross
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
net
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
lines
array of PayslipLine
requiredEvery line on this payslip, each with its own calculation trace. Read kind to tell earnings from deductions, employer contributions and information lines.
7 fields of PayslipLine
id
string
requiredThe line's identifier, as it reads on each entry in lines on the payslip from GET /v1/payroll/payslips/{payslip_id}. Quote it when you ask about one figure on this payslip.
object
always "payslip_line"
requiredAlways payslip_line. Tells you which kind of record you are looking at, so one handler can read any response.
code
string
requiredWhich pay element this line is, as a code such as pension_employee. Use it to match lines across payslips and against the explanation.
kind
string
requiredearningdeductionemployer_contributioninformationamount
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
trace
CalculationTrace
requiredThe formula, the inputs, the intermediate values and the pack version behind one line. Every payslip line carries one, and any historical line replays to the kobo from it.
6 fields of CalculationTrace
formula
string
requiredThe calculation behind this line, written as its named inputs and operators, for example pensionable_earnings x pension_employee_rate. Each name appears in inputs.
inputs
array of object
requiredThe values the formula was evaluated with, each named as it appears in formula. An amount is Money in whole minor units and a rate is an exact fraction, so you can replay the line.
exact
Rate
requiredAn exact fraction of two whole numbers. Never a decimal, and it carries no currency.
2 fields of Rate
numerator
integer · int64
requiredThe top of the fraction, a whole number. 8 over 100 is eight percent.
denominator
integer · int64 · minimum 1
requiredThe bottom of the fraction, a whole number of 1 or more. Never zero, so every rate can be evaluated.
rounding
Rounding
required3 fields of Rounding
scale
string
requiredThe unit rounded to, always minor_unit: the kobo for NGN, so a rounded figure is always a whole number of minor units.
minor_unitmode
string
requiredHow a value between two minor units is settled: half_up takes a half up, half_down takes it down, half_even takes it to the even neighbour, ceiling always rounds up, floor always down, truncate drops the fraction. The rule pack sets it for each line; there's no default.
half_uphalf_evenhalf_downceilingfloortruncatepoint
string
requiredWhere rounding was applied: per_line rounds each line on its own, per_total rounds once on the total. The rule pack sets it for each line, and the two can differ by real money.
per_lineper_totalresult
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
rule_pack
RulePackReference
required3 fields of RulePackReference
pack_id
string
requiredThe family of rules the pack belongs to, such as ng-payroll. It's pinned when you create the run at POST /v1/payroll/runs and, with version, names exactly which rates, bands and thresholds were applied.
version
string
requiredThe pack version that was applied, in the form 2026.08.1. A version never changes once active, so the same version always means the same rules.
content_hash
string
requiredA digest of the pack's content, prefixed sha256:. It proves which exact rules produced the figure, so an edited pack can never pass as the one you ran under.
rule_pack
RulePackReference
optional3 fields of RulePackReference
pack_id
string
requiredThe family of rules the pack belongs to, such as ng-payroll. It's pinned when you create the run at POST /v1/payroll/runs and, with version, names exactly which rates, bands and thresholds were applied.
version
string
requiredThe pack version that was applied, in the form 2026.08.1. A version never changes once active, so the same version always means the same rules.
content_hash
string
requiredA digest of the pack's content, prefixed sha256:. It proves which exact rules produced the figure, so an edited pack can never pass as the one you ran under.
signature
string
optionalThe signature over this payslip, as base64, made with the key named by signing_key_id. What it covers is a record of what was paid, so it stays valid however the payslip is rendered. Absent when this payslip carries no signature, and one is never added after the fact.
signing_key_id
string
optionalWhich key made signature, such as sgk_01M1E34QM07M2N5P8R3T6V9W1Y. Get its public half from GET /v1/signing_keys/{signing_key_id}, which needs no credential, and verify the signature with that. No endpoint creates or lists keys: one is written when a payslip is signed and reaches you here. Keys are replaced over time and a payslip keeps naming the one that signed it, so an older payslip still verifies. Absent when this payslip carries no signature.
created_at
string · date-time
optionalWhen the record was created, as an RFC 3339 timestamp in UTC.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollPayslipsRetrieve({ payslipId: 'run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_payslips_retrieve(payslip_id='run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollPayslipsRetrieve(payslip_id: 'run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollPayslipsRetrieve("run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollPayslipsRetrieve(payslipId: "run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollPayslipsRetrieve(ctx, "run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "payslip",
"livemode": false,
"mocked": true,
"run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"gross": {
"amount": 52500000,
"currency": "NGN"
},
"net": {
"amount": 41250000,
"currency": "NGN"
},
"lines": [
{
"id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "payslip_line",
"code": "pension_employee",
"kind": "earning",
"amount": {
"amount": 7500000,
"currency": "NGN"
},
"mocked": true,
"trace": {
"formula": "pensionable_earnings x pension_employee_rate",
"inputs": [
{}
],
"exact": {
"numerator": 1,
"denominator": 1
},
"rounding": {
"scale": "minor_unit",
"mode": "half_up",
"point": "per_line"
},
"result": {
"amount": 7500000,
"currency": "NGN"
},
"rule_pack": {
"pack_id": "ng-payroll",
"version": "2026.08.1",
"content_hash": "sha256:9f2c1e00"
}
}
}
],
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"period": "2026-09",
"rule_pack": {
"pack_id": "ng-payroll",
"version": "2026.08.1",
"content_hash": "sha256:9f2c1e00"
},
"signature": "P6ngn83ZJBAJr7ketLYJ7pRIU0WlMIeFh55g92shkhIJAsHNF9UI2dyMj6e4mwXi/sY6mB9n5/lWOvGDPsPugg==",
"signing_key_id": "sgk_01M1E34QM07M2N5P8R3T6V9W1Y",
"created_at": "2026-09-01T09:00:00Z"
}
/v1/payroll/payslips/{payslip_id}/replay#Replay a payslip from its stored inputs
payroll.payslips.replay
This recomputes a payslip from the inputs stored on it and compares the result with what was issued. So your product can check that a payslip it issued months ago still comes out the same.
Send the payslip's id in the path. You get back whether every line agrees to the kobo, and the issued amount beside the replay's for each line that moved. A payslip's gross and net are not part of that comparison.
The payslip must come from a run that executed at POST /v1/payroll/runs/{run_id}/execute.
The replay runs under the rule pack the payslip was issued with, never the one in force today, returned as rule_pack. So matches false means those inputs stopped producing the issued figures, never that the rules changed.
Read a line that moved, with its arithmetic, at GET /v1/payroll/payslips/{payslip_id}. The payslip is unchanged.
Path parameters
payslip_id
string
requiredThe payslip's identifier, from the id of a payslip on GET /v1/payroll/payslips. It starts with run_enterprise_payslip_.
Headers
Idempotency-Key
string
requiredA key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.
Returns
The comparison.
object
always "payslip_replay"
requiredAlways payslip_replay. Tells you which kind of record you are looking at, so one handler can read any response.
payslip_id
string
requiredThe payslip this comparison is for, starting with run_enterprise_payslip_: the payslip_id you sent in the path of POST /v1/payroll/payslips/{payslip_id}/replay.
matches
boolean
requiredtrue when every recomputed line agrees with what was issued, to the kobo. When false, differences names each line that moved.
rule_pack
RulePackReference
optional3 fields of RulePackReference
pack_id
string
requiredThe family of rules the pack belongs to, such as ng-payroll. It's pinned when you create the run at POST /v1/payroll/runs and, with version, names exactly which rates, bands and thresholds were applied.
version
string
requiredThe pack version that was applied, in the form 2026.08.1. A version never changes once active, so the same version always means the same rules.
content_hash
string
requiredA digest of the pack's content, prefixed sha256:. It proves which exact rules produced the figure, so an edited pack can never pass as the one you ran under.
differences
array of object
optionalEmpty when matches is true.
3 fields
code
string
requiredThe code of the payslip line that disagrees. Match it against lines on the payslip to find the line.
issued
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
recomputed
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
Other responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/replay" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollPayslipsReplay({ payslipId: 'run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });const response = await fetch('https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/replay', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Idempotency-Key': crypto.randomUUID(),
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_payslips_replay(payslip_id='run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/replay',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollPayslipsReplay(payslip_id: 'run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/replay');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollPayslipsReplay("run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/replay"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.header("Idempotency-Key", UUID.randomUUID().toString())
.method("POST", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollPayslipsReplay(payslipId: "run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/replay");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollPayslipsReplay(ctx, "run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/replay", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
req.Header.Set("Idempotency-Key", uuid.NewString())
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"object": "payslip_replay",
"payslip_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"matches": true,
"rule_pack": {
"pack_id": "ng-payroll",
"version": "2026.08.1",
"content_hash": "sha256:9f2c1e00"
},
"differences": [
{
"code": "pension_employee",
"issued": {
"amount": 41250000,
"currency": "NGN"
},
"recomputed": {
"amount": 41250000,
"currency": "NGN"
}
}
]
}
/v1/payroll/payslips/{payslip_id}/explanation#Put each payslip line into a sentence
payroll.payslips.retrieve_explanation
This puts one sentence against every line of a payslip: the formula the rule pack applied, the inputs it named with their figures, and what the line came to. So your product can show words beside each figure without laying out the trace itself.
Send the payslip's id in the path. One entry comes back per line, carrying its code and amount beside the words, in the payslip's line order.
The sentences exist as soon as a run executes, before the payslip is issued.
Ask for Pidgin with language=pcm; leave it out for English. Only the sentence changes: the formula and input names are the rule pack's own in either language.
Each line's full trace is at GET /v1/payroll/payslips/{payslip_id}.
Path parameters
payslip_id
string
requiredThe payslip's identifier, from the id of a payslip on GET /v1/payroll/payslips. It starts with run_enterprise_payslip_.
Query parameters
language
string
optionalWhich language to write the explanation in: en for English or pcm for Nigerian Pidgin. Leave it out for English.
enpcmReturns
The explanation, and no entry carries the line's kind, so a figure never says which way it moves pay.
object
always "payslip_explanation"
requiredAlways payslip_explanation. Tells you which kind of record you are looking at, so one handler can read any response.
payslip_id
string
requiredThe payslip this explanation is for, starting with run_enterprise_payslip_: the payslip_id you sent in the path of GET /v1/payroll/payslips/{payslip_id}/explanation.
language
string · defaults to "en"
requiredEnglish or Nigerian Pidgin.
enpcmlines
array of object
requiredOne entry per payslip line, in plain words. Each quotes the line's trace and carries the same code and amount as the line it explains.
3 fields
code
string
requiredThe code of the payslip line this entry explains. Match it against lines on the payslip.
text
string
requiredQuotes the trace. Performs no arithmetic of its own.
amount
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
Other responses
Errors it can return
# query parameters: language (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/explanation?language=en" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
// query parameters: language (optional)
const result = await api.payrollPayslipsRetrieveExplanation({ payslipId: 'run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', language: 'en' });// query parameters: language (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/explanation?language=en', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
# query parameters: language (optional)
result = api.payroll_payslips_retrieve_explanation(payslip_id='run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', language='en')import os
import requests
# query parameters: language (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/explanation?language=en',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
# query parameters: language (optional)
$result = $api->payrollPayslipsRetrieveExplanation(payslip_id: 'run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', language: 'en');<?php
// query parameters: language (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/explanation?language=en');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
import com.useheros.sdk.model.*;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
// query parameters: language (optional)
var result = api.payrollPayslipsRetrieveExplanation("run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", RunExplanationLanguage.fromValue("en"));// query parameters: language (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/explanation?language=en"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
// query parameters: language (optional)
var result = api.PayrollPayslipsRetrieveExplanation(payslipId: "run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", language: RunExplanationLanguage.En);// query parameters: language (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/explanation?language=en");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: language (optional)
result, _, err := client.RUNAPI.PayrollPayslipsRetrieveExplanation(ctx, "run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Language(heros.RunExplanationLanguage("en")).Execute()// query parameters: language (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/explanation?language=en", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"object": "payslip_explanation",
"payslip_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"language": "en",
"lines": [
{
"code": "pension_employee",
"text": "example",
"amount": {
"amount": 7500000,
"currency": "NGN"
}
}
]
}
/v1/payroll/payslips/{payslip_id}/issue#Issue a payslip to the worker
payroll.payslips.issue
Issuing is how a payslip leaves the run for the person it was computed for. So your product can finish a pay period worker by worker.
Send the payslip's id in the path with at least one channel. You get one entry back for each channel you named, and none for a channel you did not.
A payslip has to exist, which means a run that executed.
channels is the list for one call, and nothing carries over from the call before it. Issue the same payslip again for a channel you left out.
Path parameters
payslip_id
string
requiredThe payslip's identifier, from the id of a payslip on GET /v1/payroll/payslips. It starts with run_enterprise_payslip_.
Headers
Idempotency-Key
string
requiredA key you make up for this request, such as a UUID, up to 255 characters. Sending the same key with the same body returns the original response, marked Droomwork-Idempotent-Replay: true; sending it with a different body is refused with idempotency_key_reused.
Body
channels
array of DeliveryChannel
requiredWhere to deliver the payslip: any of email, whatsapp and portal. Name at least one; each channel you name gets its own delivery status in the response.
language
string · defaults to "en"
optionalEnglish or Nigerian Pidgin.
enpcmReturns
The issue record, and no entry names the address or number the payslip goes to.
object
always "payslip_issue"
requiredAlways payslip_issue. Tells you which kind of record you are looking at, so one handler can read any response.
payslip_id
string
requiredThe payslip being delivered, starting with run_enterprise_payslip_: the payslip_id you sent in the path of POST /v1/payroll/payslips/{payslip_id}/issue.
deliveries
array of object
requiredOne entry per channel you named, each with its own status. Accepted isn't delivered: read the status for each channel.
2 fields
channel
string
requiredemailwhatsappportalstatus
string
requiredWhere this delivery has got to: queued means it hasn't gone yet, delivered means it reached the worker on this channel, failed means it didn't. Accepted isn't delivered, so read it for each channel.
queueddeliveredfailedOther responses
Errors it can return
curl -X POST "https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/issue" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"channels":["email"],"language":"en"}'import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollPayslipsIssue({
payslipId: 'run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
idempotencyKey: crypto.randomUUID(),
runPayslipIssueRequest: {"channels":["email"],"language":"en"},
});const response = await fetch('https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/issue', {
method: 'POST',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
"channels": [
"email"
],
"language": "en"
}),
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_payslips_issue(payslip_id='run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', body={"channels": ["email"], "language": "en"})import os
import uuid
import requests
response = requests.request(
'POST',
'https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/issue',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json={"channels": ["email"], "language": "en"},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollPayslipsIssue($idempotencyKey, json_decode('{"channels":["email"],"language":"en"}', true));<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/issue');
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 => '{"channels":["email"],"language":"en"}',
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollPayslipsIssue("run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/issue"))
.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("""
{
"channels": [
"email"
],
"language": "en"
}
"""))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollPayslipsIssue(payslipId: "run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", idempotencyKey, body);using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/issue");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"channels": [
"email"
],
"language": "en"
}
""", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollPayslipsIssue(ctx, "run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").IdempotencyKey(key).RunPayslipIssueRequest(body).Execute()body := strings.NewReader(`{
"channels": [
"email"
],
"language": "en"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/issue", 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)
{
"object": "payslip_issue",
"payslip_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"deliveries": [
{
"channel": "email",
"status": "queued"
}
]
}
/v1/payroll/payslips/{payslip_id}/document#Hand over a payslip as a file
payroll.payslips.retrieve_document
This gives you a payslip as a document, a PDF to hand over or keep. So your product can give somebody a file that shows each line of their pay and what it came to.
Send the payslip's id in the path and you get a PDF back. Ask for json with format when you would like to lay the payslip out yourself.
A payslip has to exist, from a run that executed.
The PDF carries the same lines as the JSON, with the gross and the net under them, and prints the record's hash and signature. The signature covers the record and not this file, so an altered page still carries one that verifies: check the json form against the key at GET /v1/signing_keys/{signing_key_id}.
The JSON form is the same body as GET /v1/payroll/payslips/{payslip_id}. To have the payslip sent to the person it belongs to, use POST /v1/payroll/payslips/{payslip_id}/issue.
Path parameters
payslip_id
string
requiredThe payslip's identifier, from the id of a payslip on GET /v1/payroll/payslips. It starts with run_enterprise_payslip_.
Query parameters
format
string
optionalWhich form you want the signed payslip in: pdf for the document, json for the same content as a payslip record. Leave it out to get pdf.
pdfjsonReturns
The signed document.
id
string
requiredThe payslip's identifier, starting with run_enterprise_payslip_. It's written when the run executes and first reaches you on GET /v1/payroll/payslips; it never changes, so pass it as payslip_id wherever a call names this payslip.
object
always "payslip"
requiredAlways payslip. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
run_id
string
requiredThe id of the run that wrote this payslip, starting with run_enterprise_, as returned by POST /v1/payroll/runs. Pass it as run_id on GET /v1/payroll/payslips to list every payslip from the same run.
subject_id
string
optionalWho this payslip is for: their subject identifier, starting with sub_, as you sent it on their roster entry at POST /v1/payroll/roster_entries. The same value is on their pay profile.
period
string
optionalThe pay period this payslip covers, as year and month, for example 2026-09. It matches the period of the run that wrote it.
gross
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
net
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
lines
array of PayslipLine
requiredEvery line on this payslip, each with its own calculation trace. Read kind to tell earnings from deductions, employer contributions and information lines.
7 fields of PayslipLine
id
string
requiredThe line's identifier, as it reads on each entry in lines on the payslip from GET /v1/payroll/payslips/{payslip_id}. Quote it when you ask about one figure on this payslip.
object
always "payslip_line"
requiredAlways payslip_line. Tells you which kind of record you are looking at, so one handler can read any response.
code
string
requiredWhich pay element this line is, as a code such as pension_employee. Use it to match lines across payslips and against the explanation.
kind
string
requiredearningdeductionemployer_contributioninformationamount
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
trace
CalculationTrace
requiredThe formula, the inputs, the intermediate values and the pack version behind one line. Every payslip line carries one, and any historical line replays to the kobo from it.
6 fields of CalculationTrace
formula
string
requiredThe calculation behind this line, written as its named inputs and operators, for example pensionable_earnings x pension_employee_rate. Each name appears in inputs.
inputs
array of object
requiredThe values the formula was evaluated with, each named as it appears in formula. An amount is Money in whole minor units and a rate is an exact fraction, so you can replay the line.
exact
Rate
requiredAn exact fraction of two whole numbers. Never a decimal, and it carries no currency.
2 fields of Rate
numerator
integer · int64
requiredThe top of the fraction, a whole number. 8 over 100 is eight percent.
denominator
integer · int64 · minimum 1
requiredThe bottom of the fraction, a whole number of 1 or more. Never zero, so every rate can be evaluated.
rounding
Rounding
required3 fields of Rounding
scale
string
requiredThe unit rounded to, always minor_unit: the kobo for NGN, so a rounded figure is always a whole number of minor units.
minor_unitmode
string
requiredHow a value between two minor units is settled: half_up takes a half up, half_down takes it down, half_even takes it to the even neighbour, ceiling always rounds up, floor always down, truncate drops the fraction. The rule pack sets it for each line; there's no default.
half_uphalf_evenhalf_downceilingfloortruncatepoint
string
requiredWhere rounding was applied: per_line rounds each line on its own, per_total rounds once on the total. The rule pack sets it for each line, and the two can differ by real money.
per_lineper_totalresult
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
rule_pack
RulePackReference
required3 fields of RulePackReference
pack_id
string
requiredThe family of rules the pack belongs to, such as ng-payroll. It's pinned when you create the run at POST /v1/payroll/runs and, with version, names exactly which rates, bands and thresholds were applied.
version
string
requiredThe pack version that was applied, in the form 2026.08.1. A version never changes once active, so the same version always means the same rules.
content_hash
string
requiredA digest of the pack's content, prefixed sha256:. It proves which exact rules produced the figure, so an edited pack can never pass as the one you ran under.
rule_pack
RulePackReference
optional3 fields of RulePackReference
pack_id
string
requiredThe family of rules the pack belongs to, such as ng-payroll. It's pinned when you create the run at POST /v1/payroll/runs and, with version, names exactly which rates, bands and thresholds were applied.
version
string
requiredThe pack version that was applied, in the form 2026.08.1. A version never changes once active, so the same version always means the same rules.
content_hash
string
requiredA digest of the pack's content, prefixed sha256:. It proves which exact rules produced the figure, so an edited pack can never pass as the one you ran under.
signature
string
optionalThe signature over this payslip, as base64, made with the key named by signing_key_id. What it covers is a record of what was paid, so it stays valid however the payslip is rendered. Absent when this payslip carries no signature, and one is never added after the fact.
signing_key_id
string
optionalWhich key made signature, such as sgk_01M1E34QM07M2N5P8R3T6V9W1Y. Get its public half from GET /v1/signing_keys/{signing_key_id}, which needs no credential, and verify the signature with that. No endpoint creates or lists keys: one is written when a payslip is signed and reaches you here. Keys are replaced over time and a payslip keeps naming the one that signed it, so an older payslip still verifies. Absent when this payslip carries no signature.
created_at
string · date-time
optionalWhen the record was created, as an RFC 3339 timestamp in UTC.
Other responses
Errors it can return
# query parameters: format (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/document?format=pdf" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
// query parameters: format (optional)
const result = await api.payrollPayslipsRetrieveDocument({ payslipId: 'run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', format: 'pdf' });// query parameters: format (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/document?format=pdf', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
# query parameters: format (optional)
result = api.payroll_payslips_retrieve_document(payslip_id='run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', format='pdf')import os
import requests
# query parameters: format (optional)
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/document?format=pdf',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
# query parameters: format (optional)
$result = $api->payrollPayslipsRetrieveDocument(payslip_id: 'run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', format: 'pdf');<?php
// query parameters: format (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/document?format=pdf');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
// query parameters: format (optional)
var result = api.payrollPayslipsRetrieveDocument("run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "pdf");// query parameters: format (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/document?format=pdf"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
// query parameters: format (optional)
var result = api.PayrollPayslipsRetrieveDocument(payslipId: "run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", format: "pdf");// query parameters: format (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/document?format=pdf");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
// query parameters: format (optional)
result, _, err := client.RUNAPI.PayrollPayslipsRetrieveDocument(ctx, "run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Format("pdf").Execute()// query parameters: format (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/document?format=pdf", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "payslip",
"livemode": false,
"mocked": true,
"run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"gross": {
"amount": 52500000,
"currency": "NGN"
},
"net": {
"amount": 41250000,
"currency": "NGN"
},
"lines": [
{
"id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "payslip_line",
"code": "pension_employee",
"kind": "earning",
"amount": {
"amount": 7500000,
"currency": "NGN"
},
"mocked": true,
"trace": {
"formula": "pensionable_earnings x pension_employee_rate",
"inputs": [
{}
],
"exact": {
"numerator": 1,
"denominator": 1
},
"rounding": {
"scale": "minor_unit",
"mode": "half_up",
"point": "per_line"
},
"result": {
"amount": 7500000,
"currency": "NGN"
},
"rule_pack": {
"pack_id": "ng-payroll",
"version": "2026.08.1",
"content_hash": "sha256:9f2c1e00"
}
}
}
],
"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"period": "2026-09",
"rule_pack": {
"pack_id": "ng-payroll",
"version": "2026.08.1",
"content_hash": "sha256:9f2c1e00"
},
"signature": "P6ngn83ZJBAJr7ketLYJ7pRIU0WlMIeFh55g92shkhIJAsHNF9UI2dyMj6e4mwXi/sY6mB9n5/lWOvGDPsPugg==",
"signing_key_id": "sgk_01M1E34QM07M2N5P8R3T6V9W1Y",
"created_at": "2026-09-01T09:00:00Z"
}
/v1/payroll/runs/{run_id}/gl_export/{gl_export_id}#Check a pay run's journal balances
payroll.gl_exports.retrieve
This reads back a general ledger export: a pay run's journal, its two sides and whether they agree. So your product can show that journal again from the id you stored, and check a period balances before anybody posts it, without producing a second export.
Send the run's id and the export's id in the path. You get back totals.debits, totals.credits and totals.balanced, with the format you asked for.
totals.balanced is the field to read. Both sides are summed from the run's own payslip lines, so they reconcile to its totals to the kobo, and balanced is false the moment they stop agreeing.
There is no list of exports, so the id from POST /v1/payroll/runs/{run_id}/gl_export is your only way back to one. Keep it with the run_id you used: sent with any other run, it answers not_found.
Path parameters
run_id
string
requiredThe run's identifier, from the id of the response to POST /v1/payroll/runs or of a run on GET /v1/payroll/runs. It starts with run_enterprise_.
gl_export_id
string
requiredThe export's identifier: the id from the response to POST /v1/payroll/runs/{run_id}/gl_export. It starts with run_enterprise_gl_export_; pair it with the same run_id you produced the export under.
Returns
The export's record, carrying grand totals only: the journal's own lines are in the file, not here.
id
string
requiredThe export's identifier, from POST /v1/payroll/runs/{run_id}/gl_export. It starts with run_enterprise_gl_export_ and never changes; pass it as gl_export_id, with the same run_id, to fetch the export again.
object
always "gl_export"
requiredAlways gl_export. Tells you which kind of record you are looking at, so one handler can read any response.
livemode
boolean
requiredWhich realm this record is in: false is the sandbox, true is live. Read it before you act on anything.
mocked
boolean
requiredWhere these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.
run_id
string
requiredThe run this export was produced from: its id, starting with run_enterprise_, from POST /v1/payroll/runs, as you named it in the path when you produced the export. Its totals reconcile to that run's totals to the kobo.
format
string
requiredThe format you asked for as format at POST /v1/payroll/runs/{run_id}/gl_export: csv, json, xero or quickbooks. It's what the file at download_url is in.
csvjsonxeroquickbooksstatus
string
requiredgenerating while the file is being produced, ready once download_url points at it, failed if it couldn't be produced. Fetch the export again at GET /v1/payroll/runs/{run_id}/gl_export/{gl_export_id} until it leaves generating.
generatingreadyfaileddownload_url
string · uri · nullable
optionalWhere to fetch the file, as a URL, once status is ready. null before that and when the export failed.
totals
object
requiredThe export's grand totals: what it debits, what it credits, and whether the two agree. Both sides reconcile to the run's totals to the kobo.
3 fields
debits
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
credits
Money
required2 fields of Money
amount
integer · int64
requiredA whole number of the currency's minor unit, as defined by the ISO 4217 exponent. For NGN that is kobo, so 1234567 is twelve thousand three hundred and forty five naira and sixty seven kobo. A fractional value is refused with the code invalid_money_amount. Call GET /v1/currencies for the exponent of any currency. Never divide by a hundred by hand.
currency
string
requiredISO 4217 code.
balanced
boolean
requiredDebits equal credits, and both reconcile to run totals to the kobo.
Other responses
Errors it can return
curl -X GET "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/gl_export/%7Bgl_export_id%7D" \
-H "Droomwork-Api-Key: $DROOMWORK_API_KEY"import { Configuration, RUNApi } from '@heros/sdk';
const api = new RUNApi(new Configuration({ basePath: 'https://sandbox.droomwork.io', accessToken: process.env.DROOMWORK_API_KEY }));
const result = await api.payrollGlExportsRetrieve({ runId: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', glExportId: '{gl_export_id}' });const response = await fetch('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/gl_export/%7Bgl_export_id%7D', {
method: 'GET',
headers: {
'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
},
});
const result = await response.json();import os
import heros_sdk
config = heros_sdk.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = heros_sdk.ApiClient(config)
api = heros_sdk.RUNApi(client)
result = api.payroll_gl_exports_retrieve(run_id='run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', gl_export_id='{gl_export_id}')import os
import requests
response = requests.request(
'GET',
'https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/gl_export/%7Bgl_export_id%7D',
headers={
'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
},
)
result = response.json()<?php
require_once __DIR__ . '/vendor/autoload.php';
$config = HerosSdk\Configuration::getDefaultConfiguration()
->setHost('https://sandbox.droomwork.io')
->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new HerosSdk\Api\RUNApi(new GuzzleHttp\Client(), $config);
$result = $api->payrollGlExportsRetrieve(run_id: 'run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', gl_export_id: '{gl_export_id}');<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/gl_export/%7Bgl_export_id%7D');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.RunApi;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RunApi api = new RunApi(client);
var result = api.payrollGlExportsRetrieve("run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{gl_export_id}");var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/gl_export/%7Bgl_export_id%7D"))
.header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();using Heros.Sdk.Api;
using Heros.Sdk.Client;
var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new RUNApi(config);
var result = api.PayrollGlExportsRetrieve(runId: "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", glExportId: "{gl_export_id}");using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/gl_export/%7Bgl_export_id%7D");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();import heros "github.com/fenibofubara/heros-sdk-go"
ctx := context.WithValue(context.Background(), heros.ContextAPIKeys, map[string]heros.APIKey{
"apiKey": {Key: os.Getenv("DROOMWORK_API_KEY")},
})
cfg := heros.NewConfiguration()
cfg.Servers = heros.ServerConfigurations{{URL: "https://sandbox.droomwork.io"}}
client := heros.NewAPIClient(cfg)
result, _, err := client.RUNAPI.PayrollGlExportsRetrieve(ctx, "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "{gl_export_id}").Execute()req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/gl_export/%7Bgl_export_id%7D", nil)
req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
{
"id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"object": "gl_export",
"livemode": false,
"mocked": true,
"run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
"format": "csv",
"status": "generating",
"totals": {
"debits": {
"amount": 57900000,
"currency": "NGN"
},
"credits": {
"amount": 57900000,
"currency": "NGN"
},
"balanced": true
},
"download_url": "https://files.sandbox.droomwork.com/example"
}