DDroomwork Developers

Version 1.0.0

Droomwork RUN ENTERPRISE

Gross to net payroll calculation for Nigeria.

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

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?

GET/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 optional

How many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.

starting_after string optional

The id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.

subject_id string optional

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

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

data array of PayProfile required

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

12 fields of PayProfile
id string required

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

subject_id string required

The 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 required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

pay_frequency string optional

How 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_monthlyweekly
allowances array of object optional

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

Your 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 required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

effective_from string · date required

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

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

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

The id of the version this one replaced, or null for the first version.

has_more boolean required

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: 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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: limit (optional), starting_after (optional), 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)
Response
{
  "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
}
POST/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 required

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

Body

subject_id string required

The 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 required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

pay_frequency string optional

How 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_monthlyweekly
allowances array of object optional

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

Your 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 required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

effective_from string · date required

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

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

subject_id string required

The 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 required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

pay_frequency string optional

How 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_monthlyweekly
allowances array of object optional

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

Your 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 required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

effective_from string · date required

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

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

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

The id of the version this one replaced, or null for the first version.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
}
GET/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 optional

How many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.

starting_after string optional

The id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.

Returns

A page of imports, roster and pay profile alike, told apart by kind and nothing else.

object always "list" required

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

data array of Import required

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

8 fields of Import
id string required

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

kind string required

What 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_profiles
status string required

validating 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_acceptanceacceptedrejected
validation_report ValidationReport required

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

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

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

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

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

The row in a file, or the line in an instruction set.

severity string required

error means the entry failed and must be fixed. warning means it passed, but points at something you should confirm before you go ahead.

errorwarning
code string required

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

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

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

has_more boolean required

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: limit (optional), starting_after (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payroll/imports?limit=25" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: limit (optional), starting_after (optional)
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)
Response
{
  "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
}
POST/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 required

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

Returns

The import with its report, every flagged row named by its index in the file you sent.

id string required

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

kind string required

What 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_profiles
status string required

validating 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_acceptanceacceptedrejected
validation_report ValidationReport required

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

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

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

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

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

The row in a file, or the line in an instruction set.

severity string required

error means the entry failed and must be fixed. warning means it passed, but points at something you should confirm before you go ahead.

errorwarning
code string required

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

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

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
}
GET/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" required

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

module string required

The module whose readiness endpoint you called, such as GET /v1/payroll/readiness: anchor (identity), proof (credentials), rail (engagements), flow (sourcing), match (allocation), run (payroll), remit (remittance) or route (payouts).

anchorproofrailflowmatchrunremitroute
mode string required

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

integratedstandalone
ready boolean required

true when every row is satisfied and the module has what it needs from you. false when a required fact is missing or held at too low an assurance; rows says which.

rows array of ReadinessRow required

One row per fact the module requires: who owns it, the assurance it needs, what you hold and whether that satisfies it. The rows that are not satisfied are what to bring.

8 fields of ReadinessRow
fact string required

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

owner string required

The sibling module that owns this fact when running integrated.

contract_row string optional
required string required

How a fact was established. Recorded on the fact rather than in configuration, so an attested identity and a verified one stay distinguishable a year later, which is the distinction that matters when something is disputed.

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

Gates that hold in every mode and cannot be attested away. Consent before any source is queried, bank account validation, the tax identifiers returns are filed under, and duplicate detection within the organisation's own population.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/payroll/readiness" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
  ]
}
GET/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" required

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

livemode boolean required

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

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

An exact fraction of two whole numbers. Never a decimal, and it carries no currency.

2 fields of Rate
numerator integer · int64 required

The top of the fraction, a whole number. 8 over 100 is eight percent.

denominator integer · int64 · minimum 1 required

The bottom of the fraction, a whole number of 1 or more. Never zero, so every rate can be evaluated.

pension_employer_rate Rate optional

An exact fraction of two whole numbers. Never a decimal, and it carries no currency.

2 fields of Rate
numerator integer · int64 required

The top of the fraction, a whole number. 8 over 100 is eight percent.

denominator integer · int64 · minimum 1 required

The bottom of the fraction, a whole number of 1 or more. Never zero, so every rate can be evaluated.

maker_checker_threshold Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

updated_at string · date-time optional

When these settings last changed, from the database's own clock.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/payroll/settings" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
}
PATCH/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 required

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

Body

applicable object optional

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

An exact fraction of two whole numbers. Never a decimal, and it carries no currency.

2 fields of Rate
numerator integer · int64 required

The top of the fraction, a whole number. 8 over 100 is eight percent.

denominator integer · int64 · minimum 1 required

The bottom of the fraction, a whole number of 1 or more. Never zero, so every rate can be evaluated.

pension_employer_rate Rate optional

An exact fraction of two whole numbers. Never a decimal, and it carries no currency.

2 fields of Rate
numerator integer · int64 required

The top of the fraction, a whole number. 8 over 100 is eight percent.

denominator integer · int64 · minimum 1 required

The bottom of the fraction, a whole number of 1 or more. Never zero, so every rate can be evaluated.

maker_checker_threshold Money optional
2 fields of Money
amount integer · int64 required

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

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

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

livemode boolean required

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

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

An exact fraction of two whole numbers. Never a decimal, and it carries no currency.

2 fields of Rate
numerator integer · int64 required

The top of the fraction, a whole number. 8 over 100 is eight percent.

denominator integer · int64 · minimum 1 required

The bottom of the fraction, a whole number of 1 or more. Never zero, so every rate can be evaluated.

pension_employer_rate Rate optional

An exact fraction of two whole numbers. Never a decimal, and it carries no currency.

2 fields of Rate
numerator integer · int64 required

The top of the fraction, a whole number. 8 over 100 is eight percent.

denominator integer · int64 · minimum 1 required

The bottom of the fraction, a whole number of 1 or more. Never zero, so every rate can be evaluated.

maker_checker_threshold Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

updated_at string · date-time optional

When these settings last changed, from the database's own clock.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X 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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
}
GET/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 required

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

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

subject_id string required

The 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 required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

pay_frequency string optional

How 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_monthlyweekly
allowances array of object optional

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

Your 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 required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

effective_from string · date required

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

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

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

The id of the version this one replaced, or null for the first version.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/pay_profiles/run_enterprise_pay_profile_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
}
PATCH/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 required

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

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

Body

base_salary Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

allowances array of object optional

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

Your 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 required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

effective_from string · date required

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

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

subject_id string required

The 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 required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

pay_frequency string optional

How 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_monthlyweekly
allowances array of object optional

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

Your 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 required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

effective_from string · date required

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

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

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

The id of the version this one replaced, or null for the first version.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
}
POST/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 required

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

Body

subject_id string required

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

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

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

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

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

subject_id string required

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

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

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

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

blocked_by array of string optional

Empty when payable is true.

effective_from string · date required

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

The 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

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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",
  "blocked_by": [
    "no_anchored_identity"
  ],
  "effective_to": "2026-09-01"
}
GET/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 required

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

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

kind string required

What 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_profiles
status string required

validating 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_acceptanceacceptedrejected
validation_report ValidationReport required

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

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

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

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

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

The row in a file, or the line in an instruction set.

severity string required

error means the entry failed and must be fixed. warning means it passed, but points at something you should confirm before you go ahead.

errorwarning
code string required

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

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

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/payroll/imports/%7Bimport_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
}
POST/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 required

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

Body

period string required

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

Fixed at creation and immutable afterwards.

regularoff_cyclebonusthirteenth_monthcorrectionfinal_settlement
rule_pack_version string optional

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

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

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

status string required

You move a run between states with endpoints, not by setting this field.

draftsimulatedpending_approvalapprovedexecutingcompletedpartially_settledfailedcancelled
run_type string required

Fixed at creation and immutable afterwards.

regularoff_cyclebonusthirteenth_monthcorrectionfinal_settlement
period string required

The pay period this run covers, as year and month in YYYY-MM form, for example 2026-09.

payee_count integer · minimum 0 optional

How many payees are on this run, one for each roster entry it covers. A whole number, 0 or more.

rule_pack RulePackReference optional
3 fields of RulePackReference
pack_id string required

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

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

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

Each figure is the sum of its payslip lines, never a percentage of an aggregate.

5 fields of RunTotals
gross Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

net Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

total_deductions Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

employer_contributions Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

payslip_count integer · minimum 0 optional

How many payslips these totals are summed from. On a simulation, how many the run would produce; nothing is written.

prepared_by string · nullable optional

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

Never the same actor as prepared_by above the maker-checker threshold.

created_at string · date-time required

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

executed_at string · date-time · nullable optional

When the run was executed, as an RFC 3339 timestamp in UTC. null until it has been.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
}
GET/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 optional

How many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.

starting_after string optional

The id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.

payable boolean optional

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

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

data array of RosterEntry required

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

11 fields of RosterEntry
id string required

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

subject_id string required

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

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

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

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

blocked_by array of string optional

Empty when payable is true.

effective_from string · date required

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

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

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: 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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: limit (optional), starting_after (optional), 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)
Response
{
  "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",
      "blocked_by": [
        "no_anchored_identity"
      ],
      "effective_to": "2026-09-01"
    }
  ],
  "has_more": true
}
GET/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 required

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

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

subject_id string required

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

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

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

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

blocked_by array of string optional

Empty when payable is true.

effective_from string · date required

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

The 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

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/payroll/roster_entries/%7Broster_entry_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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",
  "blocked_by": [
    "no_anchored_identity"
  ],
  "effective_to": "2026-09-01"
}
PATCH/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 required

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

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

Body

pay_profile_id string optional

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

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

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

subject_id string required

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

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

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

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

blocked_by array of string optional

Empty when payable is true.

effective_from string · date required

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

The 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

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X 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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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",
  "blocked_by": [
    "no_anchored_identity"
  ],
  "effective_to": "2026-09-01"
}
GET/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 optional

How many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.

starting_after string optional

The id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.

status string optional

Return 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_settledfailedcancelled
period string optional

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

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

data array of Run required

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

14 fields of Run
id string required

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

status string required

You move a run between states with endpoints, not by setting this field.

draftsimulatedpending_approvalapprovedexecutingcompletedpartially_settledfailedcancelled
run_type string required

Fixed at creation and immutable afterwards.

regularoff_cyclebonusthirteenth_monthcorrectionfinal_settlement
period string required

The pay period this run covers, as year and month in YYYY-MM form, for example 2026-09.

payee_count integer · minimum 0 optional

How many payees are on this run, one for each roster entry it covers. A whole number, 0 or more.

rule_pack RulePackReference optional
3 fields of RulePackReference
pack_id string required

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

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

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

Each figure is the sum of its payslip lines, never a percentage of an aggregate.

5 fields of RunTotals
gross Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

net Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

total_deductions Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

employer_contributions Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

payslip_count integer · minimum 0 optional

How many payslips these totals are summed from. On a simulation, how many the run would produce; nothing is written.

prepared_by string · nullable optional

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

Never the same actor as prepared_by above the maker-checker threshold.

created_at string · date-time required

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

executed_at string · date-time · nullable optional

When the run was executed, as an RFC 3339 timestamp in UTC. null until it has been.

has_more boolean required

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: 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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RunApi;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: limit (optional), starting_after (optional), status (optional), period (optional)
result, _, err := client.RUNAPI.PayrollRunsList(ctx).Limit(25).Status(droomwork.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)
Response
{
  "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
}
GET/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 required

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

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

status string required

You move a run between states with endpoints, not by setting this field.

draftsimulatedpending_approvalapprovedexecutingcompletedpartially_settledfailedcancelled
run_type string required

Fixed at creation and immutable afterwards.

regularoff_cyclebonusthirteenth_monthcorrectionfinal_settlement
period string required

The pay period this run covers, as year and month in YYYY-MM form, for example 2026-09.

payee_count integer · minimum 0 optional

How many payees are on this run, one for each roster entry it covers. A whole number, 0 or more.

rule_pack RulePackReference optional
3 fields of RulePackReference
pack_id string required

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

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

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

Each figure is the sum of its payslip lines, never a percentage of an aggregate.

5 fields of RunTotals
gross Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

net Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

total_deductions Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

employer_contributions Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

payslip_count integer · minimum 0 optional

How many payslips these totals are summed from. On a simulation, how many the run would produce; nothing is written.

prepared_by string · nullable optional

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

Never the same actor as prepared_by above the maker-checker threshold.

created_at string · date-time required

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

executed_at string · date-time · nullable optional

When the run was executed, as an RFC 3339 timestamp in UTC. null until it has been.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
}
PATCH/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 required

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

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

Body

roster_entry_ids array of string optional

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

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

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

status string required

You move a run between states with endpoints, not by setting this field.

draftsimulatedpending_approvalapprovedexecutingcompletedpartially_settledfailedcancelled
run_type string required

Fixed at creation and immutable afterwards.

regularoff_cyclebonusthirteenth_monthcorrectionfinal_settlement
period string required

The pay period this run covers, as year and month in YYYY-MM form, for example 2026-09.

payee_count integer · minimum 0 optional

How many payees are on this run, one for each roster entry it covers. A whole number, 0 or more.

rule_pack RulePackReference optional
3 fields of RulePackReference
pack_id string required

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

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

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

Each figure is the sum of its payslip lines, never a percentage of an aggregate.

5 fields of RunTotals
gross Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

net Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

total_deductions Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

employer_contributions Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

payslip_count integer · minimum 0 optional

How many payslips these totals are summed from. On a simulation, how many the run would produce; nothing is written.

prepared_by string · nullable optional

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

Never the same actor as prepared_by above the maker-checker threshold.

created_at string · date-time required

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

executed_at string · date-time · nullable optional

When the run was executed, as an RFC 3339 timestamp in UTC. null until it has been.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X 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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
}
POST/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 required

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

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

Body optional

draft_rule_pack_version string optional

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

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

run_id string required

The 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 optional
3 fields of RulePackReference
pack_id string required

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

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

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

Each figure is the sum of its payslip lines, never a percentage of an aggregate.

5 fields of RunTotals
gross Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

net Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

total_deductions Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

employer_contributions Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

payslip_count integer · minimum 0 optional

How many payslips these totals are summed from. On a simulation, how many the run would produce; nothing is written.

wrote_records always false required

Always false. A simulation writes nothing and moves nothing.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
  }
}
POST/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 required

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

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

Returns

The run record, with no field for when this call happened; the audit entry carries that time.

id string required

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

status string required

You move a run between states with endpoints, not by setting this field.

draftsimulatedpending_approvalapprovedexecutingcompletedpartially_settledfailedcancelled
run_type string required

Fixed at creation and immutable afterwards.

regularoff_cyclebonusthirteenth_monthcorrectionfinal_settlement
period string required

The pay period this run covers, as year and month in YYYY-MM form, for example 2026-09.

payee_count integer · minimum 0 optional

How many payees are on this run, one for each roster entry it covers. A whole number, 0 or more.

rule_pack RulePackReference optional
3 fields of RulePackReference
pack_id string required

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

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

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

Each figure is the sum of its payslip lines, never a percentage of an aggregate.

5 fields of RunTotals
gross Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

net Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

total_deductions Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

employer_contributions Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

payslip_count integer · minimum 0 optional

How many payslips these totals are summed from. On a simulation, how many the run would produce; nothing is written.

prepared_by string · nullable optional

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

Never the same actor as prepared_by above the maker-checker threshold.

created_at string · date-time required

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

executed_at string · date-time · nullable optional

When the run was executed, as an RFC 3339 timestamp in UTC. null until it has been.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/cancel" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
}
GET/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 Droomwork 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 optional

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

Only attempts by one actor, matched to actor_id on each entry: an API key's id (api_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 optional

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

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

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

succeededrefusedfailed
recorded_after string optional

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

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

How many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.

starting_after string optional

The id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.

Returns

A page of audit entries, and not one of them carries the values an attempt changed.

object always "list" required

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

data array of AuditEntry required

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

13 fields of AuditEntry
id string required

The entry's identifier, as GET /v1/payroll/audit_entries lists it. It starts with audit_entry_ 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" required

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

livemode boolean required

Which realm the action happened in. False is the sandbox.

mocked boolean required

Whether the route this attempt was aimed at is 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 required

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

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

What kind of credential acted: client for your API key or OAuth client, user for a signed-in person, staff for a Droomwork staff member on a support case, service when Droomwork acted for you.

actor_id string required

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

What was attempted, as the method and the route pattern.

resource string · nullable optional

The kind of record the attempt was aimed at, as the collection name in the route, such as runs or payslips. null when the route named none.

resource_id string · nullable optional

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

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

succeededrefusedfailed
status integer required

The HTTP status the caller was given, such as 201 or 403. outcome is read from it, so the two never disagree.

has_more boolean required

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
curl -X GET "https://sandbox.droomwork.io/v1/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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: action (optional), actor_id (optional), resource (optional), resource_id (optional), outcome (optional), recorded_after (optional), recorded_before (optional), limit (optional), starting_after (optional)
result, _, err := client.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)
Response
{
  "object": "list",
  "data": [
    {
      "id": "audit_entry_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
}
POST/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 required

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

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

Returns

The run, with the period it pays, so an approval queue row can name the month signed off.

id string required

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

status string required

You move a run between states with endpoints, not by setting this field.

draftsimulatedpending_approvalapprovedexecutingcompletedpartially_settledfailedcancelled
run_type string required

Fixed at creation and immutable afterwards.

regularoff_cyclebonusthirteenth_monthcorrectionfinal_settlement
period string required

The pay period this run covers, as year and month in YYYY-MM form, for example 2026-09.

payee_count integer · minimum 0 optional

How many payees are on this run, one for each roster entry it covers. A whole number, 0 or more.

rule_pack RulePackReference optional
3 fields of RulePackReference
pack_id string required

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

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

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

Each figure is the sum of its payslip lines, never a percentage of an aggregate.

5 fields of RunTotals
gross Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

net Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

total_deductions Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

employer_contributions Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

payslip_count integer · minimum 0 optional

How many payslips these totals are summed from. On a simulation, how many the run would produce; nothing is written.

prepared_by string · nullable optional

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

Never the same actor as prepared_by above the maker-checker threshold.

created_at string · date-time required

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

executed_at string · date-time · nullable optional

When the run was executed, as an RFC 3339 timestamp in UTC. null until it has been.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/approve" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
}
GET/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 required

The 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 required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

net Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

total_deductions Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

employer_contributions Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

payslip_count integer · minimum 0 optional

How many payslips these totals are summed from. On a simulation, how many the run would produce; nothing is written.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/totals" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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
}
GET/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 required

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

How many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.

severity string optional

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

p0p1p2
status string optional

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

openresolved

Returns

The run's findings, enough to sort what blocks a sign-off from what is only worth reading.

object always "list" required

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

data array of RunFinding required

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

13 fields of RunFinding
id string required

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

run_id string required

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

Only p0 blocks approval and execution.

p0p1p2
status string required
openresolved
code string required

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

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

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

Who resolved the finding, as their user identifier, kept permanently with the reason code. null while it's still open.

reason_code string · nullable optional

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

When the finding was resolved, as an RFC 3339 timestamp in UTC. null while it's still open.

has_more boolean required

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# 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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RunApi;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: limit (optional), severity (optional), status (optional)
result, _, err := client.RUNAPI.PayrollRunFindingsList(ctx, "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Limit(25).Severity(droomwork.RunFindingSeverity("p0")).Status(droomwork.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)
Response
{
  "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
}
GET/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 optional

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

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

How many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.

starting_after string optional

The id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.

Returns

A page of events, each dated by when it happened and not by when you were told.

object always "list" required

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

data array of StoredEvent required

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

13 fields of StoredEvent
id string required

The event's identifier, starting with evt_, the same on a webhook delivery and on the module's events list, such as GET /v1/payroll/events. It never changes: a redelivery carries the same id, so you can recognise an event you have already handled.

type string required

What happened, as module.resource.past_tense_verb, for example run.payslip.calculated. Pick your handler on it; data takes the shape this type promises.

schema_version integer · minimum 1 required

The version of the shape data takes for this type, starting at 1. A change to the shape raises it, so check it before you read data.

org_id string required

The organisation the event belongs to, by its id, which starts with org_: the one POST /v1/registrations gave you and GET /v1/me returns. You only ever receive events for your own organisation.

sequence integer · minimum 0 required

Per organisation and per stream. It is how a consumer tells a replay from a new event, and it is what the delivery guarantee rests on.

occurred_at string · date-time required

When the event happened, as an RFC 3339 timestamp in UTC. Not when it was delivered: a redelivery carries the original value.

request_id string optional

The request that caused this event, where one did: the Droomwork-Request-Id that request returned, starting with req_. Absent for an event a schedule raised, such as an engagement lapsing on its end date.

livemode boolean required

Which realm the event happened in. False is the sandbox.

mocked boolean required

Whether a mock produced this fact, rather than an engine computing it. Recorded on the event when it was appended and never worked out afterwards from the realm: the two answers agree while every module is on its mock and part on the day the first engine ships. See ADR-0011.

source string required

Which part of Droomwork is the authority for this fact: anchor (identity), proof (credentials), rail (engagements), flow (sourcing), match (allocation), run (payroll), remit (remittance), route (payouts), gateway (the API's front door), iam (accounts and API keys), ledger (the books), registry (rule packs), delivery (webhooks and messages), documents (rendered payslips and instruments) or intelligence (AI decisions). Read the fact from there when it matters; your own copy is never the authority.

anchorproofrailflowmatchrunremitroutegatewayiamledgerregistrydeliverydocumentsintelligence
object always "event" required

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

stream string required

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

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

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payroll/events?stream=run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
result, _, err := client.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)
Response
{
  "object": "list",
  "data": [
    {
      "id": "evt_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "type": "run.payslip.calculated",
      "schema_version": 1,
      "org_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "sequence": 0,
      "occurred_at": "2026-09-01T09:00:00Z",
      "livemode": false,
      "mocked": true,
      "source": "anchor",
      "request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "event",
      "stream": "example",
      "data": {}
    }
  ],
  "has_more": true
}
GET/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 required

The entry's identifier, from the id of an entry you listed at GET /v1/payroll/audit_entries. It starts with audit_entry_.

Returns

The audit entry.

id string required

The entry's identifier, as GET /v1/payroll/audit_entries lists it. It starts with audit_entry_ 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" required

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

livemode boolean required

Which realm the action happened in. False is the sandbox.

mocked boolean required

Whether the route this attempt was aimed at is 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 required

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

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

What kind of credential acted: client for your API key or OAuth client, user for a signed-in person, staff for a Droomwork staff member on a support case, service when Droomwork acted for you.

actor_id string required

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

What was attempted, as the method and the route pattern.

resource string · nullable optional

The kind of record the attempt was aimed at, as the collection name in the route, such as runs or payslips. null when the route named none.

resource_id string · nullable optional

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

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

succeededrefusedfailed
status integer required

The HTTP status the caller was given, such as 201 or 403. outcome is read from it, so the two never disagree.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/payroll/audit_entries/%7Baudit_entry_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "id": "audit_entry_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"
}
POST/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 required

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

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

Returns

Execution accepted. Poll the run, or wait for run.completed.

id string required

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

status string required

You move a run between states with endpoints, not by setting this field.

draftsimulatedpending_approvalapprovedexecutingcompletedpartially_settledfailedcancelled
run_type string required

Fixed at creation and immutable afterwards.

regularoff_cyclebonusthirteenth_monthcorrectionfinal_settlement
period string required

The pay period this run covers, as year and month in YYYY-MM form, for example 2026-09.

payee_count integer · minimum 0 optional

How many payees are on this run, one for each roster entry it covers. A whole number, 0 or more.

rule_pack RulePackReference optional
3 fields of RulePackReference
pack_id string required

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

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

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

Each figure is the sum of its payslip lines, never a percentage of an aggregate.

5 fields of RunTotals
gross Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

net Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

total_deductions Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

employer_contributions Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

payslip_count integer · minimum 0 optional

How many payslips these totals are summed from. On a simulation, how many the run would produce; nothing is written.

prepared_by string · nullable optional

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

Never the same actor as prepared_by above the maker-checker threshold.

created_at string · date-time required

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

executed_at string · date-time · nullable optional

When the run was executed, as an RFC 3339 timestamp in UTC. null until it has been.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/execute" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
}
GET/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 required

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

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

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

run_id string required

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

Only p0 blocks approval and execution.

p0p1p2
status string required
openresolved
code string required

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

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

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

Who resolved the finding, as their user identifier, kept permanently with the reason code. null while it's still open.

reason_code string · nullable optional

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

When the finding was resolved, as an RFC 3339 timestamp in UTC. null while it's still open.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/findings/%7Bfinding_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
}
GET/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 required

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

The event's identifier, starting with evt_, the same on a webhook delivery and on the module's events list, such as GET /v1/payroll/events. It never changes: a redelivery carries the same id, so you can recognise an event you have already handled.

type string required

What happened, as module.resource.past_tense_verb, for example run.payslip.calculated. Pick your handler on it; data takes the shape this type promises.

schema_version integer · minimum 1 required

The version of the shape data takes for this type, starting at 1. A change to the shape raises it, so check it before you read data.

org_id string required

The organisation the event belongs to, by its id, which starts with org_: the one POST /v1/registrations gave you and GET /v1/me returns. You only ever receive events for your own organisation.

sequence integer · minimum 0 required

Per organisation and per stream. It is how a consumer tells a replay from a new event, and it is what the delivery guarantee rests on.

occurred_at string · date-time required

When the event happened, as an RFC 3339 timestamp in UTC. Not when it was delivered: a redelivery carries the original value.

request_id string optional

The request that caused this event, where one did: the Droomwork-Request-Id that request returned, starting with req_. Absent for an event a schedule raised, such as an engagement lapsing on its end date.

livemode boolean required

Which realm the event happened in. False is the sandbox.

mocked boolean required

Whether a mock produced this fact, rather than an engine computing it. Recorded on the event when it was appended and never worked out afterwards from the realm: the two answers agree while every module is on its mock and part on the day the first engine ships. See ADR-0011.

source string required

Which part of Droomwork is the authority for this fact: anchor (identity), proof (credentials), rail (engagements), flow (sourcing), match (allocation), run (payroll), remit (remittance), route (payouts), gateway (the API's front door), iam (accounts and API keys), ledger (the books), registry (rule packs), delivery (webhooks and messages), documents (rendered payslips and instruments) or intelligence (AI decisions). Read the fact from there when it matters; your own copy is never the authority.

anchorproofrailflowmatchrunremitroutegatewayiamledgerregistrydeliverydocumentsintelligence
object always "event" required

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

stream string required

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

The 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

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/payroll/events/%7Bevent_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "id": "evt_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "type": "run.payslip.calculated",
  "schema_version": 1,
  "org_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "sequence": 0,
  "occurred_at": "2026-09-01T09:00:00Z",
  "livemode": false,
  "mocked": true,
  "source": "anchor",
  "request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "event",
  "stream": "example",
  "data": {}
}
POST/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 required

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

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

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

Body

reason_code string required

Why this finding is being cleared. Recorded permanently with the actor.

note string optional

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

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

run_id string required

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

Only p0 blocks approval and execution.

p0p1p2
status string required
openresolved
code string required

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

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

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

Who resolved the finding, as their user identifier, kept permanently with the reason code. null while it's still open.

reason_code string · nullable optional

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

When the finding was resolved, as an RFC 3339 timestamp in UTC. null while it's still open.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
}
GET/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 optional

How many records to return on one page, from 1 to 100, and 25 if you leave it out. When has_more is true, pass the last record's id as starting_after to get the next page.

starting_after string optional

The id of the last record on the previous page of this same list. Leave it out to get the first page; when a page comes back with has_more true, send its last record's id here to get the page after it.

run_id string optional

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

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

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

data array of Payslip required

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

12 fields of Payslip
id string required

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

run_id string required

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

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

The 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 required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

net Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

lines array of PayslipLine required

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

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

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

code string required

Which 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 required
earningdeductionemployer_contributioninformation
amount Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

trace CalculationTrace required

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

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

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

An exact fraction of two whole numbers. Never a decimal, and it carries no currency.

2 fields of Rate
numerator integer · int64 required

The top of the fraction, a whole number. 8 over 100 is eight percent.

denominator integer · int64 · minimum 1 required

The bottom of the fraction, a whole number of 1 or more. Never zero, so every rate can be evaluated.

rounding Rounding required
3 fields of Rounding
scale string required

The unit rounded to, always minor_unit: the kobo for NGN, so a rounded figure is always a whole number of minor units.

minor_unit
mode string required

How 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_downceilingfloortruncate
point string required

Where 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_total
result Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

rule_pack RulePackReference required
3 fields of RulePackReference
pack_id string required

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

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

A 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 optional
3 fields of RulePackReference
pack_id string required

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

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

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

created_at string · date-time optional

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

has_more boolean required

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: 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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: limit (optional), starting_after (optional), 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)
Response
{
  "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"
      },
      "created_at": "2026-09-01T09:00:00Z"
    }
  ],
  "has_more": true
}
GET/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 required

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

The instruction set's identifier, which never changes: read it at GET /v1/payroll/runs/{run_id}/instruction_set when Droomwork 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" required

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

run_id string required

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

The pay period the amounts are owed for, as year and month, for example 2026-09.

paye array of object required

Grouped by the State IRS of the employee's residence, not the employer's location.

5 fields
authority_id string required

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

The 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 required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

payee_count integer optional

How many employees this line's PAYE covers. Optional; when it is present, check it against your own headcount for that state.

reconciliation_reference string required

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

Grouped by administrator and retirement savings account number.

6 fields
administrator_id string required

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

The 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 optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

employer_amount Money optional
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

amount Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

reconciliation_reference string required

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

NHF, NSITF, ITF and NHIA totals, each with a reconciliation reference.

3 fields
code string required

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

nhfnsitfitfnhia
amount Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

reconciliation_reference string required

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

The set's grand total, which its lines sum to. From a Droomwork run it reconciles to the run's totals to the kobo.

1 field
total Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

created_at string · date-time optional

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/instruction_set" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
}
POST/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 required

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

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

Body

format string required

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

csvjsonxeroquickbooks
chart_of_accounts_id string optional

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

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

run_id string required

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

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

csvjsonxeroquickbooks
status string required

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

generatingreadyfailed
download_url string · uri · nullable optional

Where to fetch the file, as a URL, once status is ready. null before that and when the export failed.

totals object required

The 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 required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

credits Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

balanced boolean required

Debits equal credits, and both reconcile to run totals to the kobo.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
}
GET/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 required

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

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

run_id string required

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

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

The 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 required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

net Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

lines array of PayslipLine required

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

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

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

code string required

Which 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 required
earningdeductionemployer_contributioninformation
amount Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

trace CalculationTrace required

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

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

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

An exact fraction of two whole numbers. Never a decimal, and it carries no currency.

2 fields of Rate
numerator integer · int64 required

The top of the fraction, a whole number. 8 over 100 is eight percent.

denominator integer · int64 · minimum 1 required

The bottom of the fraction, a whole number of 1 or more. Never zero, so every rate can be evaluated.

rounding Rounding required
3 fields of Rounding
scale string required

The unit rounded to, always minor_unit: the kobo for NGN, so a rounded figure is always a whole number of minor units.

minor_unit
mode string required

How 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_downceilingfloortruncate
point string required

Where 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_total
result Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

rule_pack RulePackReference required
3 fields of RulePackReference
pack_id string required

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

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

A 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 optional
3 fields of RulePackReference
pack_id string required

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

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

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

created_at string · date-time optional

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
  },
  "created_at": "2026-09-01T09:00:00Z"
}
POST/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 required

The payslip's identifier, from the id of a payslip on GET /v1/payroll/payslips. It starts with run_enterprise_payslip_.

Headers

Idempotency-Key string required

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

Returns

The comparison.

object always "payslip_replay" required

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

payslip_id string required

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

true when every recomputed line agrees with what was issued, to the kobo. When false, differences names each line that moved.

rule_pack RulePackReference optional
3 fields of RulePackReference
pack_id string required

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

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

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

Empty when matches is true.

3 fields
code string required

The code of the payslip line that disagrees. Match it against lines on the payslip to find the line.

issued Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

recomputed Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/payroll/payslips/run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/replay" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
      }
    }
  ]
}
GET/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 required

The payslip's identifier, from the id of a payslip on GET /v1/payroll/payslips. It starts with run_enterprise_payslip_.

Query parameters

language string optional

Which language to write the explanation in: en for English or pcm for Nigerian Pidgin. Leave it out for English.

enpcm

Returns

The explanation, and no entry carries the line's kind, so a figure never says which way it moves pay.

object always "payslip_explanation" required

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

payslip_id string required

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

English or Nigerian Pidgin.

enpcm
lines array of object required

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

The code of the payslip line this entry explains. Match it against lines on the payslip.

text string required

Quotes the trace. Performs no arithmetic of its own.

amount Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# 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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RunApi;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: language (optional)
result, _, err := client.RUNAPI.PayrollPayslipsRetrieveExplanation(ctx, "run_enterprise_payslip_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Language(droomwork.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)
Response
{
  "object": "payslip_explanation",
  "payslip_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "language": "en",
  "lines": [
    {
      "code": "pension_employee",
      "text": "example",
      "amount": {
        "amount": 7500000,
        "currency": "NGN"
      }
    }
  ]
}
POST/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 required

The payslip's identifier, from the id of a payslip on GET /v1/payroll/payslips. It starts with run_enterprise_payslip_.

Headers

Idempotency-Key string required

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

Body

channels array of DeliveryChannel required

Where 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" optional

English or Nigerian Pidgin.

enpcm

Returns

The issue record, and no entry names the address or number the payslip goes to.

object always "payslip_issue" required

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

payslip_id string required

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

One entry per channel you named, each with its own status. Accepted isn't delivered: read the status for each channel.

2 fields
channel string required
emailwhatsappportal
status string required

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

queueddeliveredfailed

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "object": "payslip_issue",
  "payslip_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "deliveries": [
    {
      "channel": "email",
      "status": "queued"
    }
  ]
}
GET/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. The working behind each line stays in the JSON, so the page will not settle a question about one figure.

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 required

The payslip's identifier, from the id of a payslip on GET /v1/payroll/payslips. It starts with run_enterprise_payslip_.

Query parameters

format string optional

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

pdfjson

Returns

The signed document.

id string required

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

run_id string required

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

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

The 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 required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

net Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

lines array of PayslipLine required

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

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

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

code string required

Which 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 required
earningdeductionemployer_contributioninformation
amount Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

trace CalculationTrace required

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

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

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

An exact fraction of two whole numbers. Never a decimal, and it carries no currency.

2 fields of Rate
numerator integer · int64 required

The top of the fraction, a whole number. 8 over 100 is eight percent.

denominator integer · int64 · minimum 1 required

The bottom of the fraction, a whole number of 1 or more. Never zero, so every rate can be evaluated.

rounding Rounding required
3 fields of Rounding
scale string required

The unit rounded to, always minor_unit: the kobo for NGN, so a rounded figure is always a whole number of minor units.

minor_unit
mode string required

How 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_downceilingfloortruncate
point string required

Where 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_total
result Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

rule_pack RulePackReference required
3 fields of RulePackReference
pack_id string required

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

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

A 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 optional
3 fields of RulePackReference
pack_id string required

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

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

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

created_at string · date-time optional

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# 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 '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: 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)
Response
{
  "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"
  },
  "created_at": "2026-09-01T09:00:00Z"
}
GET/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 required

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

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

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

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

livemode boolean required

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

mocked boolean required

Where these figures came from: true when they were mocked, false when they were computed for real. Not the inverse of livemode: each record carries the answer that was true for it.

run_id string required

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

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

csvjsonxeroquickbooks
status string required

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

generatingreadyfailed
download_url string · uri · nullable optional

Where to fetch the file, as a URL, once status is ready. null before that and when the export failed.

totals object required

The 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 required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

credits Money required
2 fields of Money
amount integer · int64 required

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

ISO 4217 code.

balanced boolean required

Debits equal credits, and both reconcile to run totals to the kobo.

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/payroll/runs/run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/gl_export/%7Bgl_export_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, RUNApi } from '@droomwork/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 droomwork

config = droomwork.Configuration(access_token=os.environ['DROOMWORK_API_KEY'])
config.host = 'https://sandbox.droomwork.io'
client = droomwork.ApiClient(config)
api = droomwork.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 = DroomworkSdk\Configuration::getDefaultConfiguration()
  ->setHost('https://sandbox.droomwork.io')
  ->setAccessToken(getenv('DROOMWORK_API_KEY'));
$api = new DroomworkSdk\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.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.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 Droomwork.Sdk.Api;
using Droomwork.Sdk.Client;

var config = new Configuration { BasePath = "https://sandbox.droomwork.io" };
config.AccessToken = Environment.GetEnvironmentVariable("DROOMWORK_API_KEY");
var api = new 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 droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

result, _, err := client.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)
Response
{
  "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"
}