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, pin a rule pack version, and you get payslips that replay to the kobo years later.

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. Read how many minor units make one major unit from GET /v1/currencies; don't assume it's a hundred.

Every payslip line carries its own trace. The formula, the inputs, the exact value before rounding, the rounding policy applied and the rule pack version. You never get a line without one. Keep it; it's what you show when a figure is questioned.

A run moves through states, and you move it with endpoints. You can't execute a run that was never approved, and you can't approve one you prepared yourself above the configured threshold. There's no status field to set.

Two fields tell you what you're looking at. livemode says whether the record belongs to the live realm or the sandbox. mocked says whether the figures are stand-ins rather than a real calculation. They answer different questions and both appear on every record.

Getting started

Get a sandbox key from the developer portal. Create a run. Simulate it. Approve it. Execute it. The sandbox keeps state per account, so the run you create is the run you fetch back.

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/payroll/runs#

List payroll runs

payroll.runs.list

Your runs, newest first.

Query parameters

limit integer optional

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

starting_after string optional

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

status string optional

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.

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

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": true,
      "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": 1234567,
          "currency": "NGN"
        },
        "net": {
          "amount": 1234567,
          "currency": "NGN"
        },
        "total_deductions": {
          "amount": 1234567,
          "currency": "NGN"
        },
        "employer_contributions": {
          "amount": 1234567,
          "currency": "NGN"
        },
        "payslip_count": 0
      },
      "prepared_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "approved_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "executed_at": "2026-09-01T09:00:00Z"
    }
  ],
  "has_more": true
}
POST/v1/payroll/runs#

Create a payroll run

payroll.runs.create

Opens a run in draft. Choose the run type now; you can't change it afterwards.

The rule pack version is pinned now, not at execution. A pack published between drafting and running won't change your figures.

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.

roster_entry_ids array of string optional

Defaults to every payable roster entry for the period.

Returns

The run, in draft.

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.08.1","roster_entry_ids":["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.payrollRunsCreate({
  idempotencyKey: crypto.randomUUID(),
  runRunCreateRequest: {"period":"2026-09","runType":"regular","rulePackVersion":"2026.08.1","rosterEntryIds":["sub_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.08.1",
    "roster_entry_ids": [
      "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_runs_create(body={"period": "2026-09", "run_type": "regular", "rule_pack_version": "2026.08.1", "roster_entry_ids": ["sub_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.08.1", "roster_entry_ids": ["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->payrollRunsCreate($idempotencyKey, json_decode('{"period":"2026-09","run_type":"regular","rule_pack_version":"2026.08.1","roster_entry_ids":["sub_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.08.1","roster_entry_ids":["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.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.08.1",
          "roster_entry_ids": [
            "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.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.08.1",
      "roster_entry_ids": [
        "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.PayrollRunsCreate(ctx).IdempotencyKey(key).RunRunCreateRequest(body).Execute()
body := strings.NewReader(`{
  "period": "2026-09",
  "run_type": "regular",
  "rule_pack_version": "2026.08.1",
  "roster_entry_ids": [
    "sub_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": true,
  "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": 1234567,
      "currency": "NGN"
    },
    "net": {
      "amount": 1234567,
      "currency": "NGN"
    },
    "total_deductions": {
      "amount": 1234567,
      "currency": "NGN"
    },
    "employer_contributions": {
      "amount": 1234567,
      "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}#

Retrieve a payroll run

payroll.runs.retrieve

The run, with its current state, the rule pack it's pinned to and its totals.

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

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": true,
  "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": 1234567,
      "currency": "NGN"
    },
    "net": {
      "amount": 1234567,
      "currency": "NGN"
    },
    "total_deductions": {
      "amount": 1234567,
      "currency": "NGN"
    },
    "employer_contributions": {
      "amount": 1234567,
      "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}#

Update a draft run

payroll.runs.update

You can change a run only while it's in draft. Once it has been simulated or approved, the change 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

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.

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": true,
  "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": 1234567,
      "currency": "NGN"
    },
    "net": {
      "amount": 1234567,
      "currency": "NGN"
    },
    "total_deductions": {
      "amount": 1234567,
      "currency": "NGN"
    },
    "employer_contributions": {
      "amount": 1234567,
      "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#

Simulate a run without committing anything

payroll.runs.simulate

Computes the run and returns the totals it would produce. No payslips are written and no money moves.

Name a draft rule pack version to test a rate change before the pack is activated. Simulating against a draft pack doesn't activate it.

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": 1234567,
      "currency": "NGN"
    },
    "net": {
      "amount": 1234567,
      "currency": "NGN"
    },
    "total_deductions": {
      "amount": 1234567,
      "currency": "NGN"
    },
    "employer_contributions": {
      "amount": 1234567,
      "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}/approve#

Approve a run for execution

payroll.runs.approve

Refused if you prepared the run and it's above the configured maker-checker threshold. Refused while any finding of severity p0 is open.

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

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": true,
  "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": 1234567,
      "currency": "NGN"
    },
    "net": {
      "amount": 1234567,
      "currency": "NGN"
    },
    "total_deductions": {
      "amount": 1234567,
      "currency": "NGN"
    },
    "employer_contributions": {
      "amount": 1234567,
      "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}/execute#

Execute an approved run

payroll.runs.execute

Computes and writes every payslip, then emits the statutory instruction set.

Every payee is checked before anything is computed. One payee without an anchored identity or without a live engagement fails the whole run, not one payslip.

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": true,
  "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": 1234567,
      "currency": "NGN"
    },
    "net": {
      "amount": 1234567,
      "currency": "NGN"
    },
    "total_deductions": {
      "amount": 1234567,
      "currency": "NGN"
    },
    "employer_contributions": {
      "amount": 1234567,
      "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}/cancel#

Cancel a run before execution

payroll.runs.cancel

Ends a run that hasn't executed. Once a run has written payslips you can't cancel it; open a correction run instead.

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

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": true,
  "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": 1234567,
      "currency": "NGN"
    },
    "net": {
      "amount": 1234567,
      "currency": "NGN"
    },
    "total_deductions": {
      "amount": 1234567,
      "currency": "NGN"
    },
    "employer_contributions": {
      "amount": 1234567,
      "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#

Retrieve run totals

payroll.runs.retrieve_totals

Every total is the sum of its payslip lines, in a stated order. No total is a percentage of an aggregate.

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": 1234567,
    "currency": "NGN"
  },
  "net": {
    "amount": 1234567,
    "currency": "NGN"
  },
  "total_deductions": {
    "amount": 1234567,
    "currency": "NGN"
  },
  "employer_contributions": {
    "amount": 1234567,
    "currency": "NGN"
  },
  "payslip_count": 0
}
GET/v1/payroll/runs/{run_id}/findings#

List anomaly findings on a run

payroll.run_findings.list

Every run is screened by Anomaly Shield before execution. A finding of severity p0 blocks approval and execution until you resolve it.

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

A page of findings.

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

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": true,
      "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/runs/{run_id}/findings/{finding_id}#

Retrieve a finding

payroll.run_findings.retrieve

One finding: its severity, and if it's resolved, the reason code and who resolved it.

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

The finding.

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": true,
  "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"
}
POST/v1/payroll/runs/{run_id}/findings/{finding_id}/resolve#

Resolve a finding

payroll.run_findings.resolve

Send a reason code to clear a finding. Who cleared it is recorded with it. A finding is never deleted.

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 resolved finding.

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":"no_payee_destination","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":"no_payee_destination","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": "no_payee_destination",
    "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": "no_payee_destination", "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": "no_payee_destination", "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":"no_payee_destination","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":"no_payee_destination","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": "no_payee_destination",
          "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": "no_payee_destination",
      "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": "no_payee_destination",
  "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": true,
  "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#

List payslips

payroll.payslips.list

Your payslips. Filter by run or by subject.

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

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": true,
      "mocked": true,
      "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "gross": {
        "amount": 1234567,
        "currency": "NGN"
      },
      "net": {
        "amount": 1234567,
        "currency": "NGN"
      },
      "lines": [
        {
          "id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
          "object": "payslip_line",
          "code": "pension_employee",
          "kind": "earning",
          "amount": {
            "amount": 1234567,
            "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": 1234567,
              "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/payslips/{payslip_id}#

Retrieve a payslip

payroll.payslips.retrieve

Every line carries its calculation trace inline. There's no separate trace endpoint.

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": true,
  "mocked": true,
  "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "gross": {
    "amount": 1234567,
    "currency": "NGN"
  },
  "net": {
    "amount": 1234567,
    "currency": "NGN"
  },
  "lines": [
    {
      "id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "payslip_line",
      "code": "pension_employee",
      "kind": "earning",
      "amount": {
        "amount": 1234567,
        "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": 1234567,
          "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

Recomputes the payslip from the inputs and the rule pack version stored on it, then compares the result with what was issued. Any historical payslip replays to the kobo.

It's a POST because it computes rather than reads.

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": "no_payee_destination",
      "issued": {
        "amount": 1234567,
        "currency": "NGN"
      },
      "recomputed": {
        "amount": 1234567,
        "currency": "NGN"
      }
    }
  ]
}
GET/v1/payroll/payslips/{payslip_id}/explanation#

Retrieve a plain language explanation of a payslip

payroll.payslips.retrieve_explanation

Explains each line in English or Nigerian Pidgin. The explanation quotes the trace and does no arithmetic of its own, so it can't disagree with the payslip.

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.

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

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": "no_payee_destination",
      "text": "example",
      "amount": {
        "amount": 1234567,
        "currency": "NGN"
      }
    }
  ]
}
POST/v1/payroll/payslips/{payslip_id}/issue#

Issue a payslip to the worker

payroll.payslips.issue

Delivers the payslip on the channels you name. The document is signed before it leaves.

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

Delivery accepted.

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#

Retrieve the signed payslip document

payroll.payslips.retrieve_document

The signed payslip as a PDF, or the same content as JSON.

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

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": true,
  "mocked": true,
  "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "gross": {
    "amount": 1234567,
    "currency": "NGN"
  },
  "net": {
    "amount": 1234567,
    "currency": "NGN"
  },
  "lines": [
    {
      "id": "run_enterprise_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "payslip_line",
      "code": "pension_employee",
      "kind": "earning",
      "amount": {
        "amount": 1234567,
        "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": 1234567,
          "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/pay_profiles#

List pay profiles

payroll.pay_profiles.list

Your pay profiles, including superseded versions, so you can explain a historical payslip.

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

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": true,
      "mocked": true,
      "subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "base_salary": {
        "amount": 1234567,
        "currency": "NGN"
      },
      "effective_from": "2026-09-01",
      "version": 1,
      "pay_frequency": "monthly",
      "allowances": [
        {
          "code": "no_payee_destination",
          "amount": {
            "amount": 1234567,
            "currency": "NGN"
          }
        }
      ],
      "effective_to": "2026-09-01",
      "supersedes": "example"
    }
  ],
  "has_more": true
}
POST/v1/pay_profiles#

Create a pay profile

payroll.pay_profiles.create

Profiles are effective dated. A mid-period salary revision is a new version, not an edit, so a payslip issued last month still computes from what was true last month.

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":1234567,"currency":"NGN"},"effective_from":"2026-09-01","pay_frequency":"monthly","allowances":[{"code":"no_payee_destination","amount":{"amount":1234567,"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":1234567,"currency":"NGN"},"effectiveFrom":"2026-09-01","payFrequency":"monthly","allowances":[{"code":"no_payee_destination","amount":{"amount":1234567,"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": 1234567,
      "currency": "NGN"
    },
    "effective_from": "2026-09-01",
    "pay_frequency": "monthly",
    "allowances": [
      {
        "code": "no_payee_destination",
        "amount": {
          "amount": 1234567,
          "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": 1234567, "currency": "NGN"}, "effective_from": "2026-09-01", "pay_frequency": "monthly", "allowances": [{"code": "no_payee_destination", "amount": {"amount": 1234567, "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": 1234567, "currency": "NGN"}, "effective_from": "2026-09-01", "pay_frequency": "monthly", "allowances": [{"code": "no_payee_destination", "amount": {"amount": 1234567, "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":1234567,"currency":"NGN"},"effective_from":"2026-09-01","pay_frequency":"monthly","allowances":[{"code":"no_payee_destination","amount":{"amount":1234567,"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":1234567,"currency":"NGN"},"effective_from":"2026-09-01","pay_frequency":"monthly","allowances":[{"code":"no_payee_destination","amount":{"amount":1234567,"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": 1234567,
            "currency": "NGN"
          },
          "effective_from": "2026-09-01",
          "pay_frequency": "monthly",
          "allowances": [
            {
              "code": "no_payee_destination",
              "amount": {
                "amount": 1234567,
                "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": 1234567,
        "currency": "NGN"
      },
      "effective_from": "2026-09-01",
      "pay_frequency": "monthly",
      "allowances": [
        {
          "code": "no_payee_destination",
          "amount": {
            "amount": 1234567,
            "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": 1234567,
    "currency": "NGN"
  },
  "effective_from": "2026-09-01",
  "pay_frequency": "monthly",
  "allowances": [
    {
      "code": "no_payee_destination",
      "amount": {
        "amount": 1234567,
        "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": true,
  "mocked": true,
  "subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "base_salary": {
    "amount": 1234567,
    "currency": "NGN"
  },
  "effective_from": "2026-09-01",
  "version": 1,
  "pay_frequency": "monthly",
  "allowances": [
    {
      "code": "no_payee_destination",
      "amount": {
        "amount": 1234567,
        "currency": "NGN"
      }
    }
  ],
  "effective_to": "2026-09-01",
  "supersedes": "example"
}
GET/v1/pay_profiles/{pay_profile_id}#

Retrieve a pay profile

payroll.pay_profiles.retrieve

One version of a pay profile.

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

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": true,
  "mocked": true,
  "subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "base_salary": {
    "amount": 1234567,
    "currency": "NGN"
  },
  "effective_from": "2026-09-01",
  "version": 1,
  "pay_frequency": "monthly",
  "allowances": [
    {
      "code": "no_payee_destination",
      "amount": {
        "amount": 1234567,
        "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

Creates a new effective dated version. There's no delete. A leaver is an end date.

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.

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 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":1234567,"currency":"NGN"},"allowances":[{"code":"no_payee_destination","amount":{"amount":1234567,"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":1234567,"currency":"NGN"},"allowances":[{"code":"no_payee_destination","amount":{"amount":1234567,"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": 1234567,
      "currency": "NGN"
    },
    "allowances": [
      {
        "code": "no_payee_destination",
        "amount": {
          "amount": 1234567,
          "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": 1234567, "currency": "NGN"}, "allowances": [{"code": "no_payee_destination", "amount": {"amount": 1234567, "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": 1234567, "currency": "NGN"}, "allowances": [{"code": "no_payee_destination", "amount": {"amount": 1234567, "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":1234567,"currency":"NGN"},"allowances":[{"code":"no_payee_destination","amount":{"amount":1234567,"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":1234567,"currency":"NGN"},"allowances":[{"code":"no_payee_destination","amount":{"amount":1234567,"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": 1234567,
            "currency": "NGN"
          },
          "allowances": [
            {
              "code": "no_payee_destination",
              "amount": {
                "amount": 1234567,
                "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": 1234567,
        "currency": "NGN"
      },
      "allowances": [
        {
          "code": "no_payee_destination",
          "amount": {
            "amount": 1234567,
            "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": 1234567,
    "currency": "NGN"
  },
  "allowances": [
    {
      "code": "no_payee_destination",
      "amount": {
        "amount": 1234567,
        "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": true,
  "mocked": true,
  "subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "base_salary": {
    "amount": 1234567,
    "currency": "NGN"
  },
  "effective_from": "2026-09-01",
  "version": 1,
  "pay_frequency": "monthly",
  "allowances": [
    {
      "code": "no_payee_destination",
      "amount": {
        "amount": 1234567,
        "currency": "NGN"
      }
    }
  ],
  "effective_to": "2026-09-01",
  "supersedes": "example"
}
GET/v1/payroll/roster_entries#

List roster entries

payroll.roster_entries.list

Who's on your roster, and whether each person currently satisfies the payment gate.

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.

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

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": true,
      "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
}
POST/v1/payroll/roster_entries#

Add someone to the roster

payroll.roster_entries.create

Adds someone to the roster. Being on the roster doesn't make them payable: the gate is checked at execution, and the entry tells you whether they pass it.

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 roster 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.
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": true,
  "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/roster_entries/{roster_entry_id}#

Retrieve a roster entry

payroll.roster_entries.retrieve

One roster entry, including what's blocking payment if anything is.

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": true,
  "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}#

Update a roster entry

payroll.roster_entries.update

Change the pay profile or set an end date.

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 updated 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

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": true,
  "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#

List bulk imports

payroll.imports.list

Your previous bulk imports and their validation reports.

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.

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": true,
      "mocked": true,
      "kind": "roster",
      "status": "validating",
      "validation_report": {
        "entry_count": 0,
        "error_count": 0,
        "warning_count": 0,
        "entries": [
          {
            "index": 1,
            "severity": "error",
            "code": "no_payee_destination",
            "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#

Import a roster or pay profile file

payroll.imports.create

Send CSV or XLSX. You get a report naming every bad row, not just the first, so one upload tells you everything you have to fix.

Nothing is committed until the import is accepted.

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 validation report.

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": true,
  "mocked": true,
  "kind": "roster",
  "status": "validating",
  "validation_report": {
    "entry_count": 0,
    "error_count": 0,
    "warning_count": 0,
    "entries": [
      {
        "index": 1,
        "severity": "error",
        "code": "no_payee_destination",
        "detail": "The payee has no verified destination, so this line cannot be paid."
      }
    ]
  },
  "created_at": "2026-09-01T09:00:00Z"
}
GET/v1/payroll/imports/{import_id}#

Retrieve an import and its validation report

payroll.imports.retrieve

The import, with the report naming every row that failed and why.

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.

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": true,
  "mocked": true,
  "kind": "roster",
  "status": "validating",
  "validation_report": {
    "entry_count": 0,
    "error_count": 0,
    "warning_count": 0,
    "entries": [
      {
        "index": 1,
        "severity": "error",
        "code": "no_payee_destination",
        "detail": "The payee has no verified destination, so this line cannot be paid."
      }
    ]
  },
  "created_at": "2026-09-01T09:00:00Z"
}
GET/v1/payroll/runs/{run_id}/instruction_set#

Retrieve the statutory instruction set for a run

payroll.instruction_sets.retrieve

What you owe to whom, grouped the way each authority needs it. PAYE by the State Internal Revenue Service of the employee's residence, pension by administrator and retirement savings account number, and totals for NHF, NSITF, ITF and NHIA, each with a reconciliation reference.

Hand it to REMIT. Its totals reconcile to run totals to the kobo.

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 instruction set.

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": true,
  "mocked": true,
  "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "paye": [
    {
      "authority_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "jurisdiction": "Rivers State Internal Revenue Service",
      "amount": {
        "amount": 1234567,
        "currency": "NGN"
      },
      "reconciliation_reference": "paye-2026-09-rivers",
      "payee_count": 1
    }
  ],
  "pension": [
    {
      "administrator_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "amount": {
        "amount": 1234567,
        "currency": "NGN"
      },
      "reconciliation_reference": "paye-2026-09-rivers",
      "administrator_name": "Rivers State Internal Revenue Service",
      "employee_amount": {
        "amount": 1234567,
        "currency": "NGN"
      },
      "employer_amount": {
        "amount": 1234567,
        "currency": "NGN"
      }
    }
  ],
  "levies": [
    {
      "code": "nhf",
      "amount": {
        "amount": 1234567,
        "currency": "NGN"
      },
      "reconciliation_reference": "paye-2026-09-rivers"
    }
  ],
  "totals": {
    "total": {
      "amount": 1234567,
      "currency": "NGN"
    }
  },
  "period": "2026-09",
  "created_at": "2026-09-01T09:00:00Z"
}
POST/v1/payroll/runs/{run_id}/gl_export#

Produce a general ledger export for a run

payroll.gl_exports.create

Produces a general ledger export in the format your accounting system reads.

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.

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.
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": true,
  "mocked": true,
  "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "format": "csv",
  "status": "generating",
  "totals": {
    "debits": {
      "amount": 1234567,
      "currency": "NGN"
    },
    "credits": {
      "amount": 1234567,
      "currency": "NGN"
    },
    "balanced": true
  },
  "download_url": "https://files.sandbox.droomwork.com/example"
}
GET/v1/payroll/runs/{run_id}/gl_export/{gl_export_id}#

Retrieve a general ledger export

payroll.gl_exports.retrieve

Your export. It reconciles to run totals to the kobo.

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.

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": true,
  "mocked": true,
  "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "format": "csv",
  "status": "generating",
  "totals": {
    "debits": {
      "amount": 1234567,
      "currency": "NGN"
    },
    "credits": {
      "amount": 1234567,
      "currency": "NGN"
    },
    "balanced": true
  },
  "download_url": "https://files.sandbox.droomwork.com/example"
}
GET/v1/payroll/readiness#

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

payroll.readiness.retrieve

RUN needs an anchored identity and a live engagement for every payee. You can satisfy that through ANCHOR and RAIL, or by supplying the facts yourself under an attestation; what was relied on is recorded on the fact.

Call this before your first run, so you don't discover a missing prerequisite at execution.

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": "anchor",
  "mode": "integrated",
  "ready": true,
  "rows": [
    {
      "fact": "anchored_subject",
      "owner": "anchor",
      "required": "attested",
      "held": "attested",
      "satisfied": true,
      "contract_row": "6.1.13",
      "source": "integrated",
      "missing_because": "not_supplied"
    }
  ],
  "always_enforced": [
    "example"
  ]
}
GET/v1/payroll/events#

List events

payroll.events.list

The append only record of everything RUN is the authority for, oldest first. Every event here is one the webhook catalogue declares, so the code that handles a delivery handles a replay too.

Two ways to read it. Pass stream with after to replay one stream from the sequence you last handled; that's exact and needs no cursor, because the sequence counts per organisation and per stream. Without stream, you get events across streams in the order they were recorded, paged with starting_after.

Query parameters

stream string optional

The id of the record whose history you want: 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.

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": true,
      "mocked": true,
      "source": "anchor",
      "request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "event",
      "stream": "example",
      "data": {}
    }
  ],
  "has_more": true
}
GET/v1/payroll/events/{event_id}#

Retrieve an event

payroll.events.retrieve

One event. An identifier belonging to another organisation comes back as not found, not refused.

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

id string required

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

type string required

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

schema_version integer · minimum 1 required

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

org_id string required

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

sequence integer · minimum 0 required

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

occurred_at string · date-time required

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

request_id string optional

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

livemode boolean required

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

mocked boolean required

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

source string required

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

anchorproofrailflowmatchrunremitroutegatewayiamledgerregistrydeliverydocumentsintelligence
object always "event" required

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

stream string required

The 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": true,
  "mocked": true,
  "source": "anchor",
  "request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "event",
  "stream": "example",
  "data": {}
}
GET/v1/payroll/audit_entries#

List audit entries

payroll.audit_entries.list

Who did what, newest first. One row per attempt, not per success: refusals are recorded too. A read that succeeded is not recorded.

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.

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": true,
      "mocked": true,
      "at": "2026-09-01T09:00:00Z",
      "request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "actor_type": "example",
      "actor_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "action": "example",
      "outcome": "succeeded",
      "status": 1,
      "resource": "example",
      "resource_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
    }
  ],
  "has_more": true
}
GET/v1/payroll/audit_entries/{audit_entry_id}#

Retrieve an audit entry

payroll.audit_entries.retrieve

One entry. An identifier belonging to another organisation comes back as not found, not refused.

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": true,
  "mocked": true,
  "at": "2026-09-01T09:00:00Z",
  "request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "actor_type": "example",
  "actor_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "action": "example",
  "outcome": "succeeded",
  "status": 1,
  "resource": "example",
  "resource_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}