DDroomwork Developers

Version 1.0.0

Droomwork ROUTE MULTI-RAIL

Disbursement across Nigerian payment rails, with failover and proof.

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 →

ROUTE pays your people. Hand it instructions computed elsewhere and it chooses a rail, keeps trying, and tells you what happened.

What you should know before you start

You never send an amount here. Instructions arrive signed from payroll or remittance. You can't post an arbitrary payment, and there's no endpoint that would let you.

unknown is a real state and you have to handle it. A payout that times out doesn't become failed. It becomes unknown, and it resolves by asking the provider what happened, never by assuming. Treat unknown as failure and someone gets paid twice.

Idempotency keys are derived, not invented. The key for a payment comes from the instruction itself, so the same payment always produces the same key and your retry after a timeout is safe.

The destination comes from the payee, not from you. An instruction names a person. Where the money goes is resolved from their identity and engagement records, so a compromised integration can't redirect a salary.

Validate accounts days before payday, not during the run. A name mismatch found at 6am on payday is a problem. Found the week before, it's an errand.

Getting started

Get a sandbox key from the developer portal. The rail simulator covers timeout, double fire, unknown state and degradation under month end load, so you can exercise the paths that matter before you meet them in production.

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/payouts/instructions#

See every instruction you have submitted

payouts.instructions.list

This is every payout instruction your organisation has submitted, each with the state it reached. So a payday screen in your product can carry a row for every one and show which are still moving, without opening each in turn.

Send nothing and they come back, each carrying its own id, which starts route_multi_rail_payout_, along with its status and its created_at.

A row's status is the instruction's own. It says how far that payday got, and never whether one particular person was paid.

Read one instruction in full at GET /v1/payouts/instructions/{instruction_id}, passing a row's id as instruction_id.

Query parameters

limit integer optional

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

starting_after string optional

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

status string optional

Return only instructions in one state: received, quarantined (held after a failed check), funding_pending, planned, executing, settled, partially_settled or failed. Leave it out to get instructions in every state.

receivedquarantinedfunding_pendingplannedexecutingunknownsettledpartially_settledfailed
run_id string optional

Return only the instructions from one payroll run: the run's id from POST /v1/payroll/runs or GET /v1/payroll/runs, starting with run_enterprise_. Leave it out to get instructions from every run and remittance.

Returns

Your instructions, each with the state it reached and the identifier later calls take.

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

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

15 fields of PayoutInstruction
id string required

The instruction's identifier, starting with route_multi_rail_payout_; it never changes. You get it from POST /v1/payouts/instructions when you submit, or from GET /v1/payouts/instructions, and pass it as instruction_id in a path.

object always "payout_instruction" required

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

unknown is a real state and not a failure. An instruction whose rail did not answer is resolved by asking what happened, never by assuming the money did not move. Treating unknown as failed is how someone gets paid twice.

receivedquarantinedfunding_pendingplannedexecutingunknownsettledpartially_settledfailed
source string required

Which of these signed the amounts. You never supply an amount yourself.

runremit
run_id string · nullable optional

The payroll run these payouts came from, when source is run: the run's id from POST /v1/payroll/runs, starting with run_enterprise_, as you sent it in run_id. null when the instruction came from a remittance.

remittance_id string · nullable optional

The remittance these payouts came from, when source is remit: the remittance's id from POST /v1/remittance/remittances, starting with remit_authority_rail_remittance_, as you sent it. null when they came from a payroll run.

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.

line_count integer · minimum 1 required

How many lines the instruction carries, at least 1. Each line names one payee, so this is how many people it pays, whatever splits they carry.

idempotency_key string optional

Derived from the instruction itself, so the same payment always produces the same key. This is what makes a retry after a timeout safe.

signature_verified boolean optional

true when the signature from payroll or remittance checked out, false when it didn't. A failed signature quarantines the instruction with quarantine_reason signature_invalid.

quarantine_reason string · nullable optional

Why the instruction was held, null unless status is quarantined: signature_invalid, duplicate_instruction (already submitted) or integrity_mismatch (a line failed its check). A held instruction is kept as evidence and never executed.

signature_invalidduplicate_instructionintegrity_mismatchnull
route_plan_id string · nullable optional

The plan that will carry this instruction: the id, starting with route_multi_rail_route_plan_, of the plan made by POST /v1/payouts/instructions/{instruction_id}/plan. null until you plan it, and execute is refused while it is null.

created_at string · date-time optional

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

has_more boolean required

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

Other responses

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

Errors it can return

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

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

// query parameters: limit (optional), starting_after (optional), status (optional), run_id (optional)
const result = await api.payoutsInstructionsList({ limit: 25, status: 'received', runId: 'sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
// query parameters: limit (optional), starting_after (optional), status (optional), run_id (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/instructions?limit=25&status=received&run_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.ROUTEApi(client)

# query parameters: limit (optional), starting_after (optional), status (optional), run_id (optional)
result = api.payouts_instructions_list(limit=25, status='received', run_id='sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

# query parameters: limit (optional), starting_after (optional), status (optional), run_id (optional)
response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/instructions?limit=25&status=received&run_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\ROUTEApi(new GuzzleHttp\Client(), $config);

# query parameters: limit (optional), starting_after (optional), status (optional), run_id (optional)
$result = $api->payoutsInstructionsList(limit: 25, status: 'received', run_id: 'sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
// query parameters: limit (optional), starting_after (optional), status (optional), run_id (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/instructions?limit=25&status=received&run_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.RouteApi;
import com.droomwork.sdk.model.*;

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

// query parameters: limit (optional), starting_after (optional), status (optional), run_id (optional)
var result = api.payoutsInstructionsList(25, null, RouteInstructionStatus.fromValue("received"), "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
// query parameters: limit (optional), starting_after (optional), status (optional), run_id (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/instructions?limit=25&status=received&run_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 ROUTEApi(config);

// query parameters: limit (optional), starting_after (optional), status (optional), run_id (optional)
var result = api.PayoutsInstructionsList(limit: 25, status: RouteInstructionStatus.Received, runId: "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
// query parameters: limit (optional), starting_after (optional), status (optional), run_id (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/instructions?limit=25&status=received&run_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), status (optional), run_id (optional)
result, _, err := client.ROUTEAPI.PayoutsInstructionsList(ctx).Limit(25).Status(droomwork.RouteInstructionStatus("received")).RunId("sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
// query parameters: limit (optional), starting_after (optional), status (optional), run_id (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/instructions?limit=25&status=received&run_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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "payout_instruction",
      "livemode": false,
      "mocked": true,
      "status": "received",
      "source": "run",
      "total": {
        "amount": 57900000,
        "currency": "NGN"
      },
      "line_count": 1,
      "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "remittance_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "idempotency_key": "example",
      "signature_verified": true,
      "quarantine_reason": "signature_invalid",
      "route_plan_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "created_at": "2026-09-01T09:00:00Z"
    }
  ],
  "has_more": true
}
POST/v1/payouts/instructions#

Submit a signed payout instruction

payouts.instructions.create

A payout instruction is one payday as a single record: who is being paid, and what each of them is due. So your product can run a whole payday from one identifier, from the route it takes to the state it ends in.

Send source, the signature that came with it, and one line per payee, each naming the payee as subject_id and the signed amount that line arrived with, in whole minor units with its currency. You get the instruction back under its own id, starting route_multi_rail_payout_.

Nothing in ROUTE has to exist before this call. Submitting pays nobody: the instruction sits in received until you plan it and then execute it, both calls of your own.

Pass the id as instruction_id to POST /v1/payouts/instructions/{instruction_id}/plan, the only transition received accepts. Execute is the only transition planned accepts.

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

source string required

Which of these signed the amounts. You never supply an amount yourself.

runremit
run_id string optional

The payroll run these payouts came from: the run's id from POST /v1/payroll/runs or GET /v1/payroll/runs, starting with run_enterprise_. Send it when source is run; every settlement then maps back to that run and its payslip lines.

remittance_id string optional

The remittance these payouts came from: its id from POST /v1/remittance/remittances or GET /v1/remittance/remittances, starting with remit_authority_rail_remittance_. Send it when source is remit, so every settlement maps back to it.

signature string required

The signature from payroll or remittance. We verify it before accepting anything.

lines array of object required

One entry per payee, at least one: who to pay, the signed amount, the payslip line it came from, and an optional split across destinations. Every line is checked before the instruction is accepted.

4 fields
subject_id string required

Who to pay: their subject identifier, starting with sub_, the subject_id on their payslip at GET /v1/payroll/payslips and roster entry at POST /v1/payroll/roster_entries. Where the money goes comes from their records, not from you.

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.

payslip_line_id string optional

The payslip line this amount came from: the id of an entry in lines on the payee's payslip at GET /v1/payroll/payslips/{payslip_id}. Send it so the settlement can be traced back to the payslip and line it paid.

split array of object optional

Each part settles independently as its own sub-leg.

2 fields
destination_ref string required

Which of the payee's destinations this part goes to: a reference to one held on their identity and engagement records, the ones a payout's destination resolves from. Never an account number; where money goes comes from the payee, not from you.

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.

Returns

The instruction as stored, in received, with the id every later call in this module takes.

id string required

The instruction's identifier, starting with route_multi_rail_payout_; it never changes. You get it from POST /v1/payouts/instructions when you submit, or from GET /v1/payouts/instructions, and pass it as instruction_id in a path.

object always "payout_instruction" required

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

unknown is a real state and not a failure. An instruction whose rail did not answer is resolved by asking what happened, never by assuming the money did not move. Treating unknown as failed is how someone gets paid twice.

receivedquarantinedfunding_pendingplannedexecutingunknownsettledpartially_settledfailed
source string required

Which of these signed the amounts. You never supply an amount yourself.

runremit
run_id string · nullable optional

The payroll run these payouts came from, when source is run: the run's id from POST /v1/payroll/runs, starting with run_enterprise_, as you sent it in run_id. null when the instruction came from a remittance.

remittance_id string · nullable optional

The remittance these payouts came from, when source is remit: the remittance's id from POST /v1/remittance/remittances, starting with remit_authority_rail_remittance_, as you sent it. null when they came from a payroll run.

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.

line_count integer · minimum 1 required

How many lines the instruction carries, at least 1. Each line names one payee, so this is how many people it pays, whatever splits they carry.

idempotency_key string optional

Derived from the instruction itself, so the same payment always produces the same key. This is what makes a retry after a timeout safe.

signature_verified boolean optional

true when the signature from payroll or remittance checked out, false when it didn't. A failed signature quarantines the instruction with quarantine_reason signature_invalid.

quarantine_reason string · nullable optional

Why the instruction was held, null unless status is quarantined: signature_invalid, duplicate_instruction (already submitted) or integrity_mismatch (a line failed its check). A held instruction is kept as evidence and never executed.

signature_invalidduplicate_instructionintegrity_mismatchnull
route_plan_id string · nullable optional

The plan that will carry this instruction: the id, starting with route_multi_rail_route_plan_, of the plan made by POST /v1/payouts/instructions/{instruction_id}/plan. null until you plan it, and execute is refused while it is null.

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.
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/payouts/instructions" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"source":"run","signature":"example","lines":[{"subject_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","amount":{"amount":7500000,"currency":"NGN"},"payslip_line_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","split":[{"destination_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","amount":{"amount":7500000,"currency":"NGN"}}]}],"run_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","remittance_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}'
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsInstructionsCreate({
  idempotencyKey: crypto.randomUUID(),
  routePayoutInstructionCreateRequest: {"source":"run","signature":"example","lines":[{"subjectId":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","amount":{"amount":7500000,"currency":"NGN"},"payslipLineId":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","split":[{"destinationRef":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","amount":{"amount":7500000,"currency":"NGN"}}]}],"runId":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","remittanceId":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"},
});
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/instructions', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    "source": "run",
    "signature": "example",
    "lines": [
      {
        "subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
        "amount": {
          "amount": 7500000,
          "currency": "NGN"
        },
        "payslip_line_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
        "split": [
          {
            "destination_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
            "amount": {
              "amount": 7500000,
              "currency": "NGN"
            }
          }
        ]
      }
    ],
    "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
    "remittance_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.ROUTEApi(client)

result = api.payouts_instructions_create(body={"source": "run", "signature": "example", "lines": [{"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "amount": {"amount": 7500000, "currency": "NGN"}, "payslip_line_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "split": [{"destination_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "amount": {"amount": 7500000, "currency": "NGN"}}]}], "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "remittance_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"})
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/payouts/instructions',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={"source": "run", "signature": "example", "lines": [{"subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "amount": {"amount": 7500000, "currency": "NGN"}, "payslip_line_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "split": [{"destination_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "amount": {"amount": 7500000, "currency": "NGN"}}]}], "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "remittance_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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsInstructionsCreate($idempotencyKey, json_decode('{"source":"run","signature":"example","lines":[{"subject_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","amount":{"amount":7500000,"currency":"NGN"},"payslip_line_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","split":[{"destination_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","amount":{"amount":7500000,"currency":"NGN"}}]}],"run_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","remittance_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/instructions');
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 => '{"source":"run","signature":"example","lines":[{"subject_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","amount":{"amount":7500000,"currency":"NGN"},"payslip_line_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","split":[{"destination_ref":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","amount":{"amount":7500000,"currency":"NGN"}}]}],"run_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","remittance_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.RouteApi;

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

var result = api.payoutsInstructionsCreate(idempotencyKey, body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/instructions"))
    .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("""
        {
          "source": "run",
          "signature": "example",
          "lines": [
            {
              "subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
              "amount": {
                "amount": 7500000,
                "currency": "NGN"
              },
              "payslip_line_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
              "split": [
                {
                  "destination_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
                  "amount": {
                    "amount": 7500000,
                    "currency": "NGN"
                  }
                }
              ]
            }
          ],
          "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
          "remittance_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 ROUTEApi(config);

var result = api.PayoutsInstructionsCreate(idempotencyKey, body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payouts/instructions");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
    {
      "source": "run",
      "signature": "example",
      "lines": [
        {
          "subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
          "amount": {
            "amount": 7500000,
            "currency": "NGN"
          },
          "payslip_line_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
          "split": [
            {
              "destination_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
              "amount": {
                "amount": 7500000,
                "currency": "NGN"
              }
            }
          ]
        }
      ],
      "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "remittance_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.ROUTEAPI.PayoutsInstructionsCreate(ctx).IdempotencyKey(key).RoutePayoutInstructionCreateRequest(body).Execute()
body := strings.NewReader(`{
  "source": "run",
  "signature": "example",
  "lines": [
    {
      "subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "amount": {
        "amount": 7500000,
        "currency": "NGN"
      },
      "payslip_line_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "split": [
        {
          "destination_ref": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
          "amount": {
            "amount": 7500000,
            "currency": "NGN"
          }
        }
      ]
    }
  ],
  "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "remittance_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payouts/instructions", 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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "payout_instruction",
  "livemode": false,
  "mocked": true,
  "status": "received",
  "source": "run",
  "total": {
    "amount": 57900000,
    "currency": "NGN"
  },
  "line_count": 1,
  "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "remittance_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "idempotency_key": "example",
  "signature_verified": true,
  "quarantine_reason": "signature_invalid",
  "route_plan_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "created_at": "2026-09-01T09:00:00Z"
}
GET/v1/payouts/instructions/{instruction_id}#

Check where one instruction has got to

payouts.instructions.retrieve

This reads one instruction back as it stands now, which is how far its payday has got. So a product holding an identifier it stored at submission can show somebody the current answer, without keeping a copy of the states itself.

Send the instruction's id in the path. You get the record back, with status for the state it is in and created_at for when you submitted it.

An instruction's id and a payout's id carry the same route_multi_rail_payout_ prefix, so the value alone will not tell you which you are holding.

See them all at GET /v1/payouts/instructions, and the route this one was given, once it is planned, at GET /v1/payouts/instructions/{instruction_id}/route_plan.

Path parameters

instruction_id string required

The instruction's identifier, starting with route_multi_rail_payout_: the id returned by POST /v1/payouts/instructions when you submitted it, or of one listed at GET /v1/payouts/instructions.

Returns

One instruction as it stands, with the state it has reached and when it arrived.

id string required

The instruction's identifier, starting with route_multi_rail_payout_; it never changes. You get it from POST /v1/payouts/instructions when you submit, or from GET /v1/payouts/instructions, and pass it as instruction_id in a path.

object always "payout_instruction" required

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

unknown is a real state and not a failure. An instruction whose rail did not answer is resolved by asking what happened, never by assuming the money did not move. Treating unknown as failed is how someone gets paid twice.

receivedquarantinedfunding_pendingplannedexecutingunknownsettledpartially_settledfailed
source string required

Which of these signed the amounts. You never supply an amount yourself.

runremit
run_id string · nullable optional

The payroll run these payouts came from, when source is run: the run's id from POST /v1/payroll/runs, starting with run_enterprise_, as you sent it in run_id. null when the instruction came from a remittance.

remittance_id string · nullable optional

The remittance these payouts came from, when source is remit: the remittance's id from POST /v1/remittance/remittances, starting with remit_authority_rail_remittance_, as you sent it. null when they came from a payroll run.

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.

line_count integer · minimum 1 required

How many lines the instruction carries, at least 1. Each line names one payee, so this is how many people it pays, whatever splits they carry.

idempotency_key string optional

Derived from the instruction itself, so the same payment always produces the same key. This is what makes a retry after a timeout safe.

signature_verified boolean optional

true when the signature from payroll or remittance checked out, false when it didn't. A failed signature quarantines the instruction with quarantine_reason signature_invalid.

quarantine_reason string · nullable optional

Why the instruction was held, null unless status is quarantined: signature_invalid, duplicate_instruction (already submitted) or integrity_mismatch (a line failed its check). A held instruction is kept as evidence and never executed.

signature_invalidduplicate_instructionintegrity_mismatchnull
route_plan_id string · nullable optional

The plan that will carry this instruction: the id, starting with route_multi_rail_route_plan_, of the plan made by POST /v1/payouts/instructions/{instruction_id}/plan. null until you plan it, and execute is refused while it is null.

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/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsInstructionsRetrieve({ instructionId: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_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.ROUTEApi(client)

result = api.payouts_instructions_retrieve(instruction_id='route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsInstructionsRetrieve(instruction_id: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_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.RouteApi;

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

var result = api.payoutsInstructionsRetrieve("route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_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 ROUTEApi(config);

var result = api.PayoutsInstructionsRetrieve(instructionId: "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_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.ROUTEAPI.PayoutsInstructionsRetrieve(ctx, "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "payout_instruction",
  "livemode": false,
  "mocked": true,
  "status": "received",
  "source": "run",
  "total": {
    "amount": 57900000,
    "currency": "NGN"
  },
  "line_count": 1,
  "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "remittance_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "idempotency_key": "example",
  "signature_verified": true,
  "quarantine_reason": "signature_invalid",
  "route_plan_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "created_at": "2026-09-01T09:00:00Z"
}
GET/v1/payouts/instructions/{instruction_id}/route_plan#

See which rails an instruction will try

payouts.instructions.retrieve_route_plan

A route plan is the order of rails one instruction will be tried on: the one to try first, and the ones after it. So when a payment lands on a fallback, your product can show the order that was fixed before anything was sent.

Send the instruction's id in the path. You get primary_rail_id, the rail to try first, and fallback_rail_ids, the rails to try next in the order they will be tried.

The order is fixed when the instruction is planned and never decided while the payment is running. So a failover you read about afterwards can be checked against the order it was meant to follow.

Plan the instruction first, at POST /v1/payouts/instructions/{instruction_id}/plan. Read the rails themselves at GET /v1/payouts/rails, where each carries its kind and its health.

Path parameters

instruction_id string required

The instruction's identifier, starting with route_multi_rail_payout_: the id returned by POST /v1/payouts/instructions when you submitted it, or of one listed at GET /v1/payouts/instructions.

Returns

The order of rails this instruction will be tried on, the primary first.

id string required

The plan's identifier, starting with route_multi_rail_route_plan_; it never changes. It is the route_plan_id on a planned instruction, and you pass it as route_plan_id at GET /v1/payouts/route_plans/{route_plan_id}.

object always "route_plan" required

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

instruction_id string required

The instruction this plan routes: its id, starting with route_multi_rail_payout_, from POST /v1/payouts/instructions. Pass it as instruction_id at GET /v1/payouts/route_plans to find the plan behind an instruction.

primary_rail_id string required

The rail to try first: the id of a rail from GET /v1/payouts/rails, starting with route_multi_rail_payout_. Fixed when the plan is made, never decided at run time, so a failover away from it can be reconstructed from the attempts.

fallback_rail_ids array of string required

The rails to try next, in order, when the primary won't carry the payment, each by its id; there is always at least one. Failover to the next fires within 15 seconds at the 95th percentile.

reasoning array of string optional

Why this order, kept so you can audit the plan.

max_attempts integer · minimum 1 optional

How many attempts the plan allows across the primary rail and its fallbacks, at least 1. Once they are used up nothing more is tried; each one is on the payout's attempts list.

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/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/route_plan" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsInstructionsRetrieveRoutePlan({ instructionId: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/route_plan', {
  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.ROUTEApi(client)

result = api.payouts_instructions_retrieve_route_plan(instruction_id='route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/route_plan',
    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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsInstructionsRetrieveRoutePlan(instruction_id: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/route_plan');
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.RouteApi;

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

var result = api.payoutsInstructionsRetrieveRoutePlan("route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/route_plan"))
    .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 ROUTEApi(config);

var result = api.PayoutsInstructionsRetrieveRoutePlan(instructionId: "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/route_plan");
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.ROUTEAPI.PayoutsInstructionsRetrieveRoutePlan(ctx, "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/route_plan", 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": "route_multi_rail_route_plan_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "route_plan",
  "instruction_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "primary_rail_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "fallback_rail_ids": [
    "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
  ],
  "reasoning": [
    "example"
  ],
  "max_attempts": 1,
  "created_at": "2026-09-01T09:00:00Z"
}
GET/v1/payouts/instructions/{instruction_id}/funding_check#

Whether the account can cover an instruction

payouts.funding_checks.retrieve

A funding check is one answer to one question: does your account hold everything an instruction would pay out. This is the read of that answer, so a screen in your product can show it without attempting anything on a rail.

You send the instruction's identifier in the path, starting with route_multi_rail_payout_, and get back what the instruction would pay out and what your account held, each in whole minor units with its currency.

Read sufficient for the verdict. A funding check coming back at all is not a yes: the record is there either way.

When the account is short, fund it and ask again at POST /v1/payouts/instructions/{instruction_id}/funding_check.

Path parameters

instruction_id string required

The instruction's identifier, starting with route_multi_rail_payout_: the id returned by POST /v1/payouts/instructions when you submitted it, or of one listed at GET /v1/payouts/instructions.

Returns

The funding check, naming what the instruction needs, what your account held and whether that covers it.

object always "funding_check" required

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

instruction_id string required

The instruction this check covers: its id, starting with route_multi_rail_payout_, from POST /v1/payouts/instructions. It is the instruction_id you passed in the path, returned so the record stands on its own.

sufficient boolean required

true when your account covers required_amount in full, false when it doesn't. On false nothing executes: no part of the instruction is paid, and shortfall says what's missing.

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

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

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

checked_at string · date-time optional

When the check ran, as an RFC 3339 timestamp in UTC. A balance moves, so check again if time has passed before you plan.

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/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/funding_check" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsFundingChecksRetrieve({ instructionId: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/funding_check', {
  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.ROUTEApi(client)

result = api.payouts_funding_checks_retrieve(instruction_id='route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/funding_check',
    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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsFundingChecksRetrieve(instruction_id: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/funding_check');
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.RouteApi;

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

var result = api.payoutsFundingChecksRetrieve("route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/funding_check"))
    .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 ROUTEApi(config);

var result = api.PayoutsFundingChecksRetrieve(instructionId: "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/funding_check");
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.ROUTEAPI.PayoutsFundingChecksRetrieve(ctx, "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/funding_check", 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": "funding_check",
  "instruction_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "sufficient": true,
  "required_amount": {
    "amount": 41250000,
    "currency": "NGN"
  },
  "available_amount": {
    "amount": 30000000,
    "currency": "NGN"
  },
  "shortfall": {
    "amount": 11250000,
    "currency": "NGN"
  },
  "checked_at": "2026-09-01T09:00:00Z"
}
POST/v1/payouts/instructions/{instruction_id}/funding_check#

Confirm funding before any rail is attempted

payouts.funding_checks.create

This asks whether your account covers everything one instruction would pay out. Ask it before you plan the route, while the instruction is the only thing that exists: that is where a funding gate of your own can sit.

Send no request body. The instruction is named in the path by the id that POST /v1/payouts/instructions returned, starting with route_multi_rail_payout_.

A funding check answers for the moment it ran. A balance moves, so ask again if time has passed before you plan.

What comes next is POST /v1/payouts/instructions/{instruction_id}/plan, which chooses the rails; the read beside this one is GET /v1/payouts/instructions/{instruction_id}/funding_check.

Path parameters

instruction_id string required

The instruction's identifier, starting with route_multi_rail_payout_: the id returned by POST /v1/payouts/instructions when you submitted it, or of one listed at GET /v1/payouts/instructions.

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 check, with the figure behind it and not only a yes or no.

object always "funding_check" required

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

instruction_id string required

The instruction this check covers: its id, starting with route_multi_rail_payout_, from POST /v1/payouts/instructions. It is the instruction_id you passed in the path, returned so the record stands on its own.

sufficient boolean required

true when your account covers required_amount in full, false when it doesn't. On false nothing executes: no part of the instruction is paid, and shortfall says what's missing.

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

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

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

checked_at string · date-time optional

When the check ran, as an RFC 3339 timestamp in UTC. A balance moves, so check again if time has passed before you plan.

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/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/funding_check" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsFundingChecksCreate({ instructionId: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/funding_check', {
  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.ROUTEApi(client)

result = api.payouts_funding_checks_create(instruction_id='route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/funding_check',
    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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsFundingChecksCreate(instruction_id: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/funding_check');
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.RouteApi;

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

var result = api.payoutsFundingChecksCreate("route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/funding_check"))
    .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 ROUTEApi(config);

var result = api.PayoutsFundingChecksCreate(instructionId: "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/funding_check");
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.ROUTEAPI.PayoutsFundingChecksCreate(ctx, "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/funding_check", 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": "funding_check",
  "instruction_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "sufficient": true,
  "required_amount": {
    "amount": 41250000,
    "currency": "NGN"
  },
  "available_amount": {
    "amount": 30000000,
    "currency": "NGN"
  },
  "shortfall": {
    "amount": 11250000,
    "currency": "NGN"
  },
  "checked_at": "2026-09-01T09:00:00Z"
}
GET/v1/payouts/route_plans#

See which rails each instruction was given

payouts.route_plans.list

This hands you every route plan your organisation holds. A plan is the rail order chosen before anything is attempted, so one screen in your product can show what was decided for every payment, and each row is the record a later question is answered from.

You get each plan with the instruction it belongs to, the rail named first, the rails to try after it in order, and how many attempts the plan allows across all of them.

Carry a row's id as route_plan_id to GET /v1/payouts/route_plans/{route_plan_id} to read one on its own. To go the other way, from an instruction to the plan carrying it, read GET /v1/payouts/instructions/{instruction_id}/route_plan.

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.

instruction_id string optional

Return only the plans made for one instruction: pass its id, starting with route_multi_rail_payout_, from POST /v1/payouts/instructions or GET /v1/payouts/instructions. Leave it out to get every stored plan.

Returns

The route plans your organisation holds, each with the rail order it names.

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

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

8 fields of RoutePlan
id string required

The plan's identifier, starting with route_multi_rail_route_plan_; it never changes. It is the route_plan_id on a planned instruction, and you pass it as route_plan_id at GET /v1/payouts/route_plans/{route_plan_id}.

object always "route_plan" required

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

instruction_id string required

The instruction this plan routes: its id, starting with route_multi_rail_payout_, from POST /v1/payouts/instructions. Pass it as instruction_id at GET /v1/payouts/route_plans to find the plan behind an instruction.

primary_rail_id string required

The rail to try first: the id of a rail from GET /v1/payouts/rails, starting with route_multi_rail_payout_. Fixed when the plan is made, never decided at run time, so a failover away from it can be reconstructed from the attempts.

fallback_rail_ids array of string required

The rails to try next, in order, when the primary won't carry the payment, each by its id; there is always at least one. Failover to the next fires within 15 seconds at the 95th percentile.

reasoning array of string optional

Why this order, kept so you can audit the plan.

max_attempts integer · minimum 1 optional

How many attempts the plan allows across the primary rail and its fallbacks, at least 1. Once they are used up nothing more is tried; each one is on the payout's attempts list.

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), instruction_id (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payouts/route_plans?limit=25&instruction_id=route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

// query parameters: limit (optional), starting_after (optional), instruction_id (optional)
const result = await api.payoutsRoutePlansList({ limit: 25, instructionId: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
// query parameters: limit (optional), starting_after (optional), instruction_id (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/route_plans?limit=25&instruction_id=route_multi_rail_payout_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.ROUTEApi(client)

# query parameters: limit (optional), starting_after (optional), instruction_id (optional)
result = api.payouts_route_plans_list(limit=25, instruction_id='route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

# query parameters: limit (optional), starting_after (optional), instruction_id (optional)
response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/route_plans?limit=25&instruction_id=route_multi_rail_payout_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\ROUTEApi(new GuzzleHttp\Client(), $config);

# query parameters: limit (optional), starting_after (optional), instruction_id (optional)
$result = $api->payoutsRoutePlansList(limit: 25, instruction_id: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
// query parameters: limit (optional), starting_after (optional), instruction_id (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/route_plans?limit=25&instruction_id=route_multi_rail_payout_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.RouteApi;

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

// query parameters: limit (optional), starting_after (optional), instruction_id (optional)
var result = api.payoutsRoutePlansList(25, null, "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
// query parameters: limit (optional), starting_after (optional), instruction_id (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/route_plans?limit=25&instruction_id=route_multi_rail_payout_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 ROUTEApi(config);

// query parameters: limit (optional), starting_after (optional), instruction_id (optional)
var result = api.PayoutsRoutePlansList(limit: 25, instructionId: "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
// query parameters: limit (optional), starting_after (optional), instruction_id (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/route_plans?limit=25&instruction_id=route_multi_rail_payout_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), instruction_id (optional)
result, _, err := client.ROUTEAPI.PayoutsRoutePlansList(ctx).Limit(25).InstructionId("route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
// query parameters: limit (optional), starting_after (optional), instruction_id (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/route_plans?limit=25&instruction_id=route_multi_rail_payout_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": "route_multi_rail_route_plan_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "route_plan",
      "instruction_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "primary_rail_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "fallback_rail_ids": [
        "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
      ],
      "reasoning": [
        "example"
      ],
      "max_attempts": 1,
      "created_at": "2026-09-01T09:00:00Z"
    }
  ],
  "has_more": true
}
POST/v1/payouts/route_plans#

Record a routing decision taken outside Droomwork

payouts.route_plans.create

This stores a routing decision you took yourself as a plan of its own. So a payment routed outside Droomwork can still be named in a call and read back later.

You get the plan back with an id starting route_multi_rail_route_plan_ to keep, the rail named first, the rails to try after it in order, and how many attempts the plan allows across all of them.

Nothing has to exist first.

The request names the rails primary and fallbacks, and they come back as primary_rail_id and fallback_rail_ids. Read the response's names, not the ones you sent.

Keep the id: it is what GET /v1/payouts/route_plans/{route_plan_id} reads back.

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

primary string optional

The rail to try first.

fallbacks array of string optional

Rails to try in order when the primary will not carry the payment.

Returns

The stored plan, with the identifier to quote when you read it back.

id string required

The plan's identifier, starting with route_multi_rail_route_plan_; it never changes. It is the route_plan_id on a planned instruction, and you pass it as route_plan_id at GET /v1/payouts/route_plans/{route_plan_id}.

object always "route_plan" required

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

instruction_id string required

The instruction this plan routes: its id, starting with route_multi_rail_payout_, from POST /v1/payouts/instructions. Pass it as instruction_id at GET /v1/payouts/route_plans to find the plan behind an instruction.

primary_rail_id string required

The rail to try first: the id of a rail from GET /v1/payouts/rails, starting with route_multi_rail_payout_. Fixed when the plan is made, never decided at run time, so a failover away from it can be reconstructed from the attempts.

fallback_rail_ids array of string required

The rails to try next, in order, when the primary won't carry the payment, each by its id; there is always at least one. Failover to the next fires within 15 seconds at the 95th percentile.

reasoning array of string optional

Why this order, kept so you can audit the plan.

max_attempts integer · minimum 1 optional

How many attempts the plan allows across the primary rail and its fallbacks, at least 1. Once they are used up nothing more is tried; each one is on the payout's attempts list.

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.
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/payouts/route_plans" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"primary":"example","fallbacks":["example"]}'
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsRoutePlansCreate({});
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/route_plans', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    "primary": "example",
    "fallbacks": [
      "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.ROUTEApi(client)

result = api.payouts_route_plans_create()
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/payouts/route_plans',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={"primary": "example", "fallbacks": ["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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsRoutePlansCreate();
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/route_plans');
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 => '{"primary":"example","fallbacks":["example"]}',
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RouteApi;

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

var result = api.payoutsRoutePlansCreate();
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/route_plans"))
    .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("""
        {
          "primary": "example",
          "fallbacks": [
            "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 ROUTEApi(config);

var result = api.PayoutsRoutePlansCreate();
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payouts/route_plans");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
    {
      "primary": "example",
      "fallbacks": [
        "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.ROUTEAPI.PayoutsRoutePlansCreate(ctx).Execute()
body := strings.NewReader(`{
  "primary": "example",
  "fallbacks": [
    "example"
  ]
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payouts/route_plans", 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": "route_multi_rail_route_plan_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "route_plan",
  "instruction_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "primary_rail_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "fallback_rail_ids": [
    "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
  ],
  "reasoning": [
    "example"
  ],
  "max_attempts": 1,
  "created_at": "2026-09-01T09:00:00Z"
}
GET/v1/payouts/route_plans/{route_plan_id}#

Check the rail order behind one payment

payouts.route_plans.retrieve

This reads one route plan back as it was stored, the rail order chosen before anything is attempted. So when somebody asks which rail a payment was meant to take, the answer comes from a record written before the attempt and not from anybody's memory of that morning.

Send the plan's id in the path. You get the rail named first, the rails to try after it in order, how many attempts the plan allows across all of them, the instruction it belongs to and when it was written.

A plan is the decision and never the outcome. What each rail answered is kept on the payout at GET /v1/payouts/payouts/{payout_id}/attempts. A route_plan_id nothing holds answers not_found.

Path parameters

route_plan_id string required

The route plan's identifier, starting with route_multi_rail_route_plan_: the id of a plan you created at POST /v1/payouts/route_plans or listed at GET /v1/payouts/route_plans, or the route_plan_id on an instruction you planned.

Returns

One plan as it was stored, with the rails it named and the attempt cap.

id string required

The plan's identifier, starting with route_multi_rail_route_plan_; it never changes. It is the route_plan_id on a planned instruction, and you pass it as route_plan_id at GET /v1/payouts/route_plans/{route_plan_id}.

object always "route_plan" required

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

instruction_id string required

The instruction this plan routes: its id, starting with route_multi_rail_payout_, from POST /v1/payouts/instructions. Pass it as instruction_id at GET /v1/payouts/route_plans to find the plan behind an instruction.

primary_rail_id string required

The rail to try first: the id of a rail from GET /v1/payouts/rails, starting with route_multi_rail_payout_. Fixed when the plan is made, never decided at run time, so a failover away from it can be reconstructed from the attempts.

fallback_rail_ids array of string required

The rails to try next, in order, when the primary won't carry the payment, each by its id; there is always at least one. Failover to the next fires within 15 seconds at the 95th percentile.

reasoning array of string optional

Why this order, kept so you can audit the plan.

max_attempts integer · minimum 1 optional

How many attempts the plan allows across the primary rail and its fallbacks, at least 1. Once they are used up nothing more is tried; each one is on the payout's attempts list.

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/payouts/route_plans/route_multi_rail_route_plan_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsRoutePlansRetrieve({ routePlanId: 'route_multi_rail_route_plan_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/route_plans/route_multi_rail_route_plan_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.ROUTEApi(client)

result = api.payouts_route_plans_retrieve(route_plan_id='route_multi_rail_route_plan_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/route_plans/route_multi_rail_route_plan_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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsRoutePlansRetrieve(route_plan_id: 'route_multi_rail_route_plan_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/route_plans/route_multi_rail_route_plan_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.RouteApi;

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

var result = api.payoutsRoutePlansRetrieve("route_multi_rail_route_plan_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/route_plans/route_multi_rail_route_plan_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 ROUTEApi(config);

var result = api.PayoutsRoutePlansRetrieve(routePlanId: "route_multi_rail_route_plan_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/route_plans/route_multi_rail_route_plan_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.ROUTEAPI.PayoutsRoutePlansRetrieve(ctx, "route_multi_rail_route_plan_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/route_plans/route_multi_rail_route_plan_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": "route_multi_rail_route_plan_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "route_plan",
  "instruction_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "primary_rail_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "fallback_rail_ids": [
    "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
  ],
  "reasoning": [
    "example"
  ],
  "max_attempts": 1,
  "created_at": "2026-09-01T09:00:00Z"
}
GET/v1/payouts/rails#

See which rails can carry your payouts

payouts.rails.list

A rail is one way money leaves Droomwork for a payee's account, and rails differ in what they will carry and how soon they settle. This is where that set comes from, so your product can show which rails are usable and what each one carries without a rail table written into your own code.

Ask without naming a rail and they come back together, each with its id, its kind, the capabilities that say what it will carry, and the health it reports.

Store the id and not the name. The id never changes and it is what you quote in a call; name is there to put in front of a person.

When you need one rail's record on its own later, GET /v1/payouts/rails/{rail_id} takes the id you stored.

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.

health string optional

Return only rails in one state: healthy, degraded (up, but not at full service) or unavailable (not carrying payouts right now). Leave it out to get every rail.

healthydegradedunavailable

Returns

The rails, each with its kind, what it will carry and the health it reports.

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

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

7 fields of Rail
id string required

The rail's identifier, starting with route_multi_rail_payout_; it never changes. You get it from GET /v1/payouts/rails, and it is the primary_rail_id and fallback_rail_ids on a route plan and the rail_id on an attempt.

object always "rail" required

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

name string required

The rail's name as people know it, such as NIP instant transfer. For display; name the rail by id in a call.

kind string required

What sort of rail it is: instant_transfer (such as NIP), direct_bank (a direct bank integration), wallet or mobile_money. All four sit behind the same interface.

instant_transferdirect_bankwalletmobile_money
health string required
healthydegradedunavailable
capabilities object optional

What the rail can do: the largest amount it will carry, whether it can confirm an account name before paying, and how soon it settles.

3 fields
max_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.

supports_name_enquiry boolean optional

true when the rail can return the name on an account before any money moves, which is what a name mismatch check needs. false when it can't.

settlement_window string optional

How soon money sent on this rail lands, in words such as near instant, same day or next business day.

credentials_held boolean optional

Whether your credentials for this rail are in place: true when your payouts can go through it, false when it can't carry your money yet. GET /v1/payouts/readiness names the rails still without them.

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), health (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payouts/rails?limit=25&health=healthy" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

// query parameters: limit (optional), health (optional)
const result = await api.payoutsRailsList({ limit: 25, health: 'healthy' });
// query parameters: limit (optional), health (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/rails?limit=25&health=healthy', {
  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.ROUTEApi(client)

# query parameters: limit (optional), health (optional)
result = api.payouts_rails_list(limit=25, health='healthy')
import os

import requests

# query parameters: limit (optional), health (optional)
response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/rails?limit=25&health=healthy',
    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\ROUTEApi(new GuzzleHttp\Client(), $config);

# query parameters: limit (optional), health (optional)
$result = $api->payoutsRailsList(limit: 25, health: 'healthy');
<?php
// query parameters: limit (optional), health (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/rails?limit=25&health=healthy');
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.RouteApi;
import com.droomwork.sdk.model.*;

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

// query parameters: limit (optional), health (optional)
var result = api.payoutsRailsList(25, RouteRailHealth.fromValue("healthy"));
// query parameters: limit (optional), health (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/rails?limit=25&health=healthy"))
    .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 ROUTEApi(config);

// query parameters: limit (optional), health (optional)
var result = api.PayoutsRailsList(limit: 25, health: RouteRailHealth.Healthy);
// query parameters: limit (optional), health (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/rails?limit=25&health=healthy");
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), health (optional)
result, _, err := client.ROUTEAPI.PayoutsRailsList(ctx).Limit(25).Health(droomwork.RouteRailHealth("healthy")).Execute()
// query parameters: limit (optional), health (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/rails?limit=25&health=healthy", 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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "rail",
      "name": "NIP instant transfer",
      "kind": "instant_transfer",
      "health": "healthy",
      "capabilities": {
        "max_amount": {
          "amount": 100000000,
          "currency": "NGN"
        },
        "supports_name_enquiry": true,
        "settlement_window": "near instant"
      },
      "credentials_held": true
    }
  ],
  "has_more": true
}
GET/v1/payouts/rails/{rail_id}#

Check what one rail will carry

payouts.rails.retrieve

A rail is one way money leaves Droomwork for a payee's account. This reads one of them on its own, so a product that already stored a rail's identifier can ask what that rail will carry without pulling the whole set.

Put the rail's identifier in the path and that one record comes back: its kind, the capabilities that say what it will carry and how soon it settles, the health it reports, and whether your credentials for it are in place.

capabilities.max_amount is the largest single payout the rail will carry, in whole minor units with its currency. credentials_held is a separate question from health: a rail can report itself healthy and still be one your money cannot go through yet.

Use an identifier that came back from GET /v1/payouts/rails; a rail_id nothing holds answers not_found.

Path parameters

rail_id string required

The rail's identifier, starting with route_multi_rail_payout_: the id of a rail listed at GET /v1/payouts/rails, the primary_rail_id on a route plan, or the rail_id on an attempt at GET /v1/payouts/payouts/{payout_id}/attempts.

Returns

The one rail, with its capabilities, the health it reports and whether your credentials are in place.

id string required

The rail's identifier, starting with route_multi_rail_payout_; it never changes. You get it from GET /v1/payouts/rails, and it is the primary_rail_id and fallback_rail_ids on a route plan and the rail_id on an attempt.

object always "rail" required

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

name string required

The rail's name as people know it, such as NIP instant transfer. For display; name the rail by id in a call.

kind string required

What sort of rail it is: instant_transfer (such as NIP), direct_bank (a direct bank integration), wallet or mobile_money. All four sit behind the same interface.

instant_transferdirect_bankwalletmobile_money
health string required
healthydegradedunavailable
capabilities object optional

What the rail can do: the largest amount it will carry, whether it can confirm an account name before paying, and how soon it settles.

3 fields
max_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.

supports_name_enquiry boolean optional

true when the rail can return the name on an account before any money moves, which is what a name mismatch check needs. false when it can't.

settlement_window string optional

How soon money sent on this rail lands, in words such as near instant, same day or next business day.

credentials_held boolean optional

Whether your credentials for this rail are in place: true when your payouts can go through it, false when it can't carry your money yet. GET /v1/payouts/readiness names the rails still without them.

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/payouts/rails/%7Brail_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsRailsRetrieve({ railId: '{rail_id}' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/rails/%7Brail_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.ROUTEApi(client)

result = api.payouts_rails_retrieve(rail_id='{rail_id}')
import os

import requests

response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/rails/%7Brail_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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsRailsRetrieve(rail_id: '{rail_id}');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/rails/%7Brail_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.RouteApi;

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

var result = api.payoutsRailsRetrieve("{rail_id}");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/rails/%7Brail_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 ROUTEApi(config);

var result = api.PayoutsRailsRetrieve(railId: "{rail_id}");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/rails/%7Brail_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.ROUTEAPI.PayoutsRailsRetrieve(ctx, "{rail_id}").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/rails/%7Brail_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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "rail",
  "name": "NIP instant transfer",
  "kind": "instant_transfer",
  "health": "healthy",
  "capabilities": {
    "max_amount": {
      "amount": 100000000,
      "currency": "NGN"
    },
    "supports_name_enquiry": true,
    "settlement_window": "near instant"
  },
  "credentials_held": true
}
GET/v1/payouts/payouts#

Track each payee's payment separately

payouts.payouts.list

ROUTE turns one instruction into the payments it actually makes: one for each payee, or one for each sub-leg where a payee's line splits across destinations. This list is where you see them, and each one settles on its own, so a payee whose payment fails tells you nothing about the instruction's other payouts.

You get your organisation's payouts, each carrying the payee's subject_id, the amount in whole minor units with its currency, the account it was sent to with its number masked, and where that one payment has got to.

Take an id from the list, beginning route_multi_rail_payout_, to GET /v1/payouts/payouts/{payout_id} for one payment on its own, or pass it as payout_id to POST /v1/payouts/payouts/{payout_id}/resolve when a payment's outcome is in doubt.

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.

instruction_id string optional

Return only the payouts of one instruction: pass its id, starting with route_multi_rail_payout_, from POST /v1/payouts/instructions or GET /v1/payouts/instructions. Leave it out to get payouts from every instruction.

status string optional

Return only payouts in one state: pending (no attempt yet), attempting (a rail is being tried), unknown (timed out, awaiting the provider's answer), settled or failed. Leave it out to get every state.

pendingattemptingunknownsettledfailed

Returns

Your organisation's payouts, each with its payee, its amount and where that one payment has got to.

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

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

13 fields of Payout
id string required

The payout's identifier, starting with route_multi_rail_payout_; it never changes. You get it from GET /v1/payouts/payouts, and pass it as payout_id to retrieve or resolve the payout or list its attempts.

object always "payout" required

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

instruction_id string required

The instruction this payout belongs to: its id, starting with route_multi_rail_payout_, from POST /v1/payouts/instructions. One instruction yields one payout per payee, or one per sub-leg where a line splits across destinations.

subject_id string required

Who is being paid: their subject identifier, starting with sub_, exactly as you sent subject_id on the instruction line at POST /v1/payouts/instructions. Where the money goes is resolved from their records, not from anything you send.

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.

status string required

unknown is a real state. A payout that timed out resolves by asking the provider, never by assuming failure. Treating unknown as failed is how someone gets paid twice.

pendingattemptingunknownsettledfailed
destination Destination optional

Resolved from the payee's identity and engagement records, never supplied on the instruction. The account number is masked.

5 fields of Destination
bank_code string required

The code that identifies the payee's bank to the rail. Read bank_name for the name a person would recognise.

bank_name string optional

The payee's bank, under the name a person would recognise. bank_code is the same bank as the rail identifies it.

account_number_masked string required

The payee's account number with all but its last four digits hidden, such as ******4821. The full number is never returned.

account_name string required

The name the account is held in, from the payee's own records rather than anything you sent. validated says whether the bank has confirmed it.

validated boolean optional

true once an account validation came back valid for this account. false when it hasn't been checked, or the outcome was a name mismatch, not found or unresolved.

attempt_count integer · minimum 0 optional

How many attempts have been made against this payout so far, from 0 before the first. Each one is listed at GET /v1/payouts/payouts/{payout_id}/attempts with its rail and outcome.

unknown_since string · date-time · nullable optional

Set while the provider is being polled. Cleared on resolution.

settlement_id string · nullable optional

The settlement that closed this payout, once there is one: the id, starting with route_multi_rail_settlement_, of a settlement at GET /v1/payouts/settlements. null until a settlement, paid or failed, has been recorded.

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), instruction_id (optional), status (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payouts/payouts?limit=25&instruction_id=route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=pending" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

// query parameters: limit (optional), starting_after (optional), instruction_id (optional), status (optional)
const result = await api.payoutsPayoutsList({ limit: 25, instructionId: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', status: 'pending' });
// query parameters: limit (optional), starting_after (optional), instruction_id (optional), status (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/payouts?limit=25&instruction_id=route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=pending', {
  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.ROUTEApi(client)

# query parameters: limit (optional), starting_after (optional), instruction_id (optional), status (optional)
result = api.payouts_payouts_list(limit=25, instruction_id='route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', status='pending')
import os

import requests

# query parameters: limit (optional), starting_after (optional), instruction_id (optional), status (optional)
response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/payouts?limit=25&instruction_id=route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=pending',
    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\ROUTEApi(new GuzzleHttp\Client(), $config);

# query parameters: limit (optional), starting_after (optional), instruction_id (optional), status (optional)
$result = $api->payoutsPayoutsList(limit: 25, instruction_id: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', status: 'pending');
<?php
// query parameters: limit (optional), starting_after (optional), instruction_id (optional), status (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/payouts?limit=25&instruction_id=route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=pending');
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.RouteApi;
import com.droomwork.sdk.model.*;

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

// query parameters: limit (optional), starting_after (optional), instruction_id (optional), status (optional)
var result = api.payoutsPayoutsList(25, null, "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", RoutePayoutStatus.fromValue("pending"));
// query parameters: limit (optional), starting_after (optional), instruction_id (optional), status (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/payouts?limit=25&instruction_id=route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=pending"))
    .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 ROUTEApi(config);

// query parameters: limit (optional), starting_after (optional), instruction_id (optional), status (optional)
var result = api.PayoutsPayoutsList(limit: 25, instructionId: "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", status: RoutePayoutStatus.Pending);
// query parameters: limit (optional), starting_after (optional), instruction_id (optional), status (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/payouts?limit=25&instruction_id=route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=pending");
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), instruction_id (optional), status (optional)
result, _, err := client.ROUTEAPI.PayoutsPayoutsList(ctx).Limit(25).InstructionId("route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Status(droomwork.RoutePayoutStatus("pending")).Execute()
// query parameters: limit (optional), starting_after (optional), instruction_id (optional), status (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/payouts?limit=25&instruction_id=route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&status=pending", 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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "payout",
      "livemode": false,
      "mocked": true,
      "instruction_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "amount": {
        "amount": 7500000,
        "currency": "NGN"
      },
      "status": "pending",
      "destination": {
        "bank_code": "example",
        "account_number_masked": "******4821",
        "account_name": "example",
        "bank_name": "example",
        "validated": true
      },
      "attempt_count": 0,
      "unknown_since": "2026-09-01T09:00:00Z",
      "settlement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "created_at": "2026-09-01T09:00:00Z"
    }
  ],
  "has_more": true
}
GET/v1/payouts/payouts/{payout_id}#

Check where one payee's money stands

payouts.payouts.retrieve

When a payee says the money never arrived, this is the record that answers for them. A payout is the one payment made for one payee out of an instruction, so you can answer that one question without reading the whole instruction.

Pass the payout_id in the path and you get that payout: its status, the amount in whole minor units with its currency, and the account it was sent to.

You only ever get account_number_masked, the last four digits of the account. Match a payout to your own records by subject_id and the amount, and do not read destination.validated as the bank disagreeing: it is false for an account nobody checked too.

What was tried is a separate call. GET /v1/payouts/payouts/{payout_id}/attempts lists every rail attempt made for this payout and what came back from each. A payout_id nothing holds answers not_found.

Path parameters

payout_id string required

The payout's identifier, starting with route_multi_rail_payout_: the id of a payout from GET /v1/payouts/payouts, or the payout_id on a settlement or an attempt.

Returns

The one payment made for one payee, with its state and the account it was sent to.

id string required

The payout's identifier, starting with route_multi_rail_payout_; it never changes. You get it from GET /v1/payouts/payouts, and pass it as payout_id to retrieve or resolve the payout or list its attempts.

object always "payout" required

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

instruction_id string required

The instruction this payout belongs to: its id, starting with route_multi_rail_payout_, from POST /v1/payouts/instructions. One instruction yields one payout per payee, or one per sub-leg where a line splits across destinations.

subject_id string required

Who is being paid: their subject identifier, starting with sub_, exactly as you sent subject_id on the instruction line at POST /v1/payouts/instructions. Where the money goes is resolved from their records, not from anything you send.

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.

status string required

unknown is a real state. A payout that timed out resolves by asking the provider, never by assuming failure. Treating unknown as failed is how someone gets paid twice.

pendingattemptingunknownsettledfailed
destination Destination optional

Resolved from the payee's identity and engagement records, never supplied on the instruction. The account number is masked.

5 fields of Destination
bank_code string required

The code that identifies the payee's bank to the rail. Read bank_name for the name a person would recognise.

bank_name string optional

The payee's bank, under the name a person would recognise. bank_code is the same bank as the rail identifies it.

account_number_masked string required

The payee's account number with all but its last four digits hidden, such as ******4821. The full number is never returned.

account_name string required

The name the account is held in, from the payee's own records rather than anything you sent. validated says whether the bank has confirmed it.

validated boolean optional

true once an account validation came back valid for this account. false when it hasn't been checked, or the outcome was a name mismatch, not found or unresolved.

attempt_count integer · minimum 0 optional

How many attempts have been made against this payout so far, from 0 before the first. Each one is listed at GET /v1/payouts/payouts/{payout_id}/attempts with its rail and outcome.

unknown_since string · date-time · nullable optional

Set while the provider is being polled. Cleared on resolution.

settlement_id string · nullable optional

The settlement that closed this payout, once there is one: the id, starting with route_multi_rail_settlement_, of a settlement at GET /v1/payouts/settlements. null until a settlement, paid or failed, has been recorded.

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/payouts/payouts/%7Bpayout_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsPayoutsRetrieve({ payoutId: '{payout_id}' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_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.ROUTEApi(client)

result = api.payouts_payouts_retrieve(payout_id='{payout_id}')
import os

import requests

response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsPayoutsRetrieve(payout_id: '{payout_id}');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_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.RouteApi;

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

var result = api.payoutsPayoutsRetrieve("{payout_id}");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_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 ROUTEApi(config);

var result = api.PayoutsPayoutsRetrieve(payoutId: "{payout_id}");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_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.ROUTEAPI.PayoutsPayoutsRetrieve(ctx, "{payout_id}").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "payout",
  "livemode": false,
  "mocked": true,
  "instruction_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "amount": {
    "amount": 7500000,
    "currency": "NGN"
  },
  "status": "pending",
  "destination": {
    "bank_code": "example",
    "account_number_masked": "******4821",
    "account_name": "example",
    "bank_name": "example",
    "validated": true
  },
  "attempt_count": 0,
  "unknown_since": "2026-09-01T09:00:00Z",
  "settlement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "created_at": "2026-09-01T09:00:00Z"
}
POST/v1/payouts/payouts/{payout_id}/resolve#

Close one payout whose outcome was in doubt

payouts.payouts.resolve

One payout can end up with its outcome in doubt while everything else in the instruction is done. This call works on that one payout, by its own identifier, so you act on one payee's position and leave the instruction's other payouts alone.

Send no request body. The payout_id in the path is the whole request, and it must name a payout you already hold from GET /v1/payouts/payouts. The payout comes back with its state, so you read it here and not on a second call.

A payout_id nothing holds answers not_found.

Path parameters

payout_id string required

The payout's identifier, starting with route_multi_rail_payout_: the id of a payout from GET /v1/payouts/payouts, or the payout_id on a settlement or an attempt.

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 payout as it stands after the call, with its state, its payee and its amount.

id string required

The payout's identifier, starting with route_multi_rail_payout_; it never changes. You get it from GET /v1/payouts/payouts, and pass it as payout_id to retrieve or resolve the payout or list its attempts.

object always "payout" required

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

instruction_id string required

The instruction this payout belongs to: its id, starting with route_multi_rail_payout_, from POST /v1/payouts/instructions. One instruction yields one payout per payee, or one per sub-leg where a line splits across destinations.

subject_id string required

Who is being paid: their subject identifier, starting with sub_, exactly as you sent subject_id on the instruction line at POST /v1/payouts/instructions. Where the money goes is resolved from their records, not from anything you send.

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.

status string required

unknown is a real state. A payout that timed out resolves by asking the provider, never by assuming failure. Treating unknown as failed is how someone gets paid twice.

pendingattemptingunknownsettledfailed
destination Destination optional

Resolved from the payee's identity and engagement records, never supplied on the instruction. The account number is masked.

5 fields of Destination
bank_code string required

The code that identifies the payee's bank to the rail. Read bank_name for the name a person would recognise.

bank_name string optional

The payee's bank, under the name a person would recognise. bank_code is the same bank as the rail identifies it.

account_number_masked string required

The payee's account number with all but its last four digits hidden, such as ******4821. The full number is never returned.

account_name string required

The name the account is held in, from the payee's own records rather than anything you sent. validated says whether the bank has confirmed it.

validated boolean optional

true once an account validation came back valid for this account. false when it hasn't been checked, or the outcome was a name mismatch, not found or unresolved.

attempt_count integer · minimum 0 optional

How many attempts have been made against this payout so far, from 0 before the first. Each one is listed at GET /v1/payouts/payouts/{payout_id}/attempts with its rail and outcome.

unknown_since string · date-time · nullable optional

Set while the provider is being polled. Cleared on resolution.

settlement_id string · nullable optional

The settlement that closed this payout, once there is one: the id, starting with route_multi_rail_settlement_, of a settlement at GET /v1/payouts/settlements. null until a settlement, paid or failed, has been recorded.

created_at string · date-time optional

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/resolve" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsPayoutsResolve({ payoutId: '{payout_id}' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/resolve', {
  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.ROUTEApi(client)

result = api.payouts_payouts_resolve(payout_id='{payout_id}')
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/resolve',
    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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsPayoutsResolve(payout_id: '{payout_id}');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/resolve');
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.RouteApi;

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

var result = api.payoutsPayoutsResolve("{payout_id}");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/resolve"))
    .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 ROUTEApi(config);

var result = api.PayoutsPayoutsResolve(payoutId: "{payout_id}");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/resolve");
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.ROUTEAPI.PayoutsPayoutsResolve(ctx, "{payout_id}").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/resolve", 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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "payout",
  "livemode": false,
  "mocked": true,
  "instruction_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "amount": {
    "amount": 7500000,
    "currency": "NGN"
  },
  "status": "settled",
  "destination": {
    "bank_code": "example",
    "account_number_masked": "******4821",
    "account_name": "example",
    "bank_name": "example",
    "validated": true
  },
  "attempt_count": 0,
  "unknown_since": "2026-09-01T09:00:00Z",
  "settlement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "created_at": "2026-09-01T09:00:00Z"
}
GET/v1/payouts/payouts/{payout_id}/attempts#

Trace how a payment was tried

payouts.attempts.list

An attempt is one try at paying one payout through one rail. These are the tries made for one payout and what each came back with, so your own view can say which rails handled a payment and how many times it was tried.

You name the payout and get back its attempts, each with the rail it went through, its outcome, and when it started and ended.

Read sequence before you take anything from the order: 1 is the try at the primary rail, and anything higher is a try at a fallback. completed_at is null while a try is still open, so handle the null before you time one.

An attempt's id is what you send as attempt_id to GET /v1/payouts/payouts/{payout_id}/attempts/{attempt_id}. A payout_id nothing holds answers not_found.

Path parameters

payout_id string required

The payout's identifier, starting with route_multi_rail_payout_: the id of a payout from GET /v1/payouts/payouts, or the payout_id on a settlement or an attempt.

Query parameters

limit integer optional

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

Returns

The tries made for this payout, each with its rail, its outcome and its timings.

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

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

11 fields of Attempt
id string required

The attempt's identifier. It never changes; pass it as attempt_id to GET /v1/payouts/payouts/{payout_id}/attempts/{attempt_id} to retrieve the attempt on its own.

object always "payout_attempt" required

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

payout_id string required

The payout this attempt was made for: the id of a payout from GET /v1/payouts/payouts, which starts with route_multi_rail_payout_. It is the payout_id you passed in the path to list or retrieve its attempts.

rail_id string required

The rail this attempt went through: the id of a rail from GET /v1/payouts/rails, which starts with route_multi_rail_payout_. Read sequence to see whether it was the primary rail or a failover.

sequence integer · minimum 1 optional

1 is the primary rail. Anything higher is a failover.

outcome string required
acceptedrejectedtimed_outprovider_error
provider_reference string · nullable optional

The reference the provider gave for this attempt, as it appears on their side; quote it when you take a question to them. null when they gave none, such as after a timeout.

provider_message string · nullable optional

What the provider said about the attempt, in their own words, such as the reason for a rejection. null when they said nothing.

started_at string · date-time required

When the attempt was sent to the rail, as an RFC 3339 timestamp in UTC. The gap from the previous attempt's failure or timeout to this moment is failover_latency_ms.

completed_at string · date-time · nullable optional

When the attempt ended, with the rail's answer or a timeout, as an RFC 3339 timestamp in UTC. null while it is still open.

failover_latency_ms integer · nullable optional

Time from the failure or timeout signal on the previous attempt to this one starting. The 95th percentile of this is what the 15 second failover requirement is measured against.

has_more boolean required

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: limit (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/attempts?limit=25" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

// query parameters: limit (optional)
const result = await api.payoutsAttemptsList({ payoutId: '{payout_id}', limit: 25 });
// query parameters: limit (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/attempts?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.ROUTEApi(client)

# query parameters: limit (optional)
result = api.payouts_attempts_list(payout_id='{payout_id}', limit=25)
import os

import requests

# query parameters: limit (optional)
response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/attempts?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\ROUTEApi(new GuzzleHttp\Client(), $config);

# query parameters: limit (optional)
$result = $api->payoutsAttemptsList(payout_id: '{payout_id}', limit: 25);
<?php
// query parameters: limit (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/attempts?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.RouteApi;

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

// query parameters: limit (optional)
var result = api.payoutsAttemptsList("{payout_id}", 25);
// query parameters: limit (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/attempts?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 ROUTEApi(config);

// query parameters: limit (optional)
var result = api.PayoutsAttemptsList(payoutId: "{payout_id}", limit: 25);
// query parameters: limit (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/attempts?limit=25");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: limit (optional)
result, _, err := client.ROUTEAPI.PayoutsAttemptsList(ctx, "{payout_id}").Limit(25).Execute()
// query parameters: limit (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/attempts?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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "payout_attempt",
      "payout_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "rail_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "outcome": "accepted",
      "started_at": "2026-09-01T09:00:00Z",
      "sequence": 1,
      "provider_reference": "paye-2026-09-rivers",
      "provider_message": "example",
      "completed_at": "2026-09-01T09:00:00Z",
      "failover_latency_ms": 1
    }
  ],
  "has_more": true
}
GET/v1/payouts/payouts/{payout_id}/attempts/{attempt_id}#

What one rail came back with

payouts.attempts.retrieve

An attempt is one try at paying one payout through one rail. This hands you one of them whole, so a question about a single try can be taken to the provider on their own reference for it, where they gave one.

You name the payout the try was made for and the attempt itself, and get back that one attempt: its rail, its outcome, its timings, and what the provider said about it in their own words.

provider_reference and provider_message are each null where the provider gave none, so anything you build on them has to read correctly with neither.

This is the whole of one try. Where it sat among the others is on the payout's full list at GET /v1/payouts/payouts/{payout_id}/attempts.

Path parameters

payout_id string required

The payout's identifier, starting with route_multi_rail_payout_: the id of a payout from GET /v1/payouts/payouts, or the payout_id on a settlement or an attempt.

attempt_id string required

The attempt's identifier, from the id of an attempt you listed at GET /v1/payouts/payouts/{payout_id}/attempts. Pair it with the payout_id of the payout it was made for, as payout_id reads on the attempt.

Returns

One try at one rail, with the provider's own reference and message where they gave them.

id string required

The attempt's identifier. It never changes; pass it as attempt_id to GET /v1/payouts/payouts/{payout_id}/attempts/{attempt_id} to retrieve the attempt on its own.

object always "payout_attempt" required

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

payout_id string required

The payout this attempt was made for: the id of a payout from GET /v1/payouts/payouts, which starts with route_multi_rail_payout_. It is the payout_id you passed in the path to list or retrieve its attempts.

rail_id string required

The rail this attempt went through: the id of a rail from GET /v1/payouts/rails, which starts with route_multi_rail_payout_. Read sequence to see whether it was the primary rail or a failover.

sequence integer · minimum 1 optional

1 is the primary rail. Anything higher is a failover.

outcome string required
acceptedrejectedtimed_outprovider_error
provider_reference string · nullable optional

The reference the provider gave for this attempt, as it appears on their side; quote it when you take a question to them. null when they gave none, such as after a timeout.

provider_message string · nullable optional

What the provider said about the attempt, in their own words, such as the reason for a rejection. null when they said nothing.

started_at string · date-time required

When the attempt was sent to the rail, as an RFC 3339 timestamp in UTC. The gap from the previous attempt's failure or timeout to this moment is failover_latency_ms.

completed_at string · date-time · nullable optional

When the attempt ended, with the rail's answer or a timeout, as an RFC 3339 timestamp in UTC. null while it is still open.

failover_latency_ms integer · nullable optional

Time from the failure or timeout signal on the previous attempt to this one starting. The 95th percentile of this is what the 15 second failover requirement is measured against.

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/payouts/payouts/%7Bpayout_id%7D/attempts/%7Battempt_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsAttemptsRetrieve({ payoutId: '{payout_id}', attemptId: '{attempt_id}' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/attempts/%7Battempt_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.ROUTEApi(client)

result = api.payouts_attempts_retrieve(payout_id='{payout_id}', attempt_id='{attempt_id}')
import os

import requests

response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/attempts/%7Battempt_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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsAttemptsRetrieve(payout_id: '{payout_id}', attempt_id: '{attempt_id}');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/attempts/%7Battempt_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.RouteApi;

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

var result = api.payoutsAttemptsRetrieve("{payout_id}", "{attempt_id}");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/attempts/%7Battempt_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 ROUTEApi(config);

var result = api.PayoutsAttemptsRetrieve(payoutId: "{payout_id}", attemptId: "{attempt_id}");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/attempts/%7Battempt_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.ROUTEAPI.PayoutsAttemptsRetrieve(ctx, "{payout_id}", "{attempt_id}").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/payouts/%7Bpayout_id%7D/attempts/%7Battempt_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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "payout_attempt",
  "payout_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "rail_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "outcome": "accepted",
  "started_at": "2026-09-01T09:00:00Z",
  "sequence": 1,
  "provider_reference": "paye-2026-09-rivers",
  "provider_message": "example",
  "completed_at": "2026-09-01T09:00:00Z",
  "failover_latency_ms": 1
}
GET/v1/payouts/account_validations#

What the banks found across your roster

payouts.account_validations.list

Your organisation's validations collect here, so one page answers how a whole roster stands instead of one payee at a time. Nothing in a validation names a payment or a run, so a check is readable whether or not anything is being paid.

You get back the validations your organisation holds, one row for each check that has been run. A validation is one check of one payee's account against the name the bank holds for it.

Read one in full at GET /v1/payouts/account_validations/{account_validation_id}, using the id on its row.

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.

outcome string optional

Restricts the page to one outcome: valid (the name matches), name_mismatch (it doesn't), account_not_found or unresolved (no answer, never a pass). Pass name_mismatch to find the errands before payday; leave it out to get all four.

validname_mismatchaccount_not_foundunresolved

Returns

The validations your organisation holds, with what the bank answered on each.

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

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

9 fields of AccountValidation
id string required

The validation's identifier. It starts with route_multi_rail_payout_ and never changes; pass it as account_validation_id to GET /v1/payouts/account_validations/{account_validation_id} to retrieve this record on its own.

object always "account_validation" required

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

subject_id string required

Who the check was for: one of the subject_ids you sent to POST /v1/payouts/account_validations, the sub_ reference that names a payee in lines[].subject_id at POST /v1/payouts/instructions.

outcome string required
validname_mismatchaccount_not_foundunresolved
expected_name string · nullable optional

The name you expected on the account. Compare it with bank_name_returned when the outcome is name_mismatch; null when there was no name to compare.

bank_name_returned string · nullable optional

What the bank holds. Compared with the expected name, and neither is a full account number.

similarity number · nullable optional

How close the two names are, from 0 to 1. A judgement aid, not a decision.

source string · nullable optional

Named when the outcome is unresolved, because silence is never a pass.

checked_at string · date-time optional

When the bank was asked, as an RFC 3339 timestamp in UTC. Read it to see how old a result is before you rely on it on payday.

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), outcome (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payouts/account_validations?limit=25&outcome=valid" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

// query parameters: limit (optional), starting_after (optional), outcome (optional)
const result = await api.payoutsAccountValidationsList({ limit: 25, outcome: 'valid' });
// query parameters: limit (optional), starting_after (optional), outcome (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/account_validations?limit=25&outcome=valid', {
  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.ROUTEApi(client)

# query parameters: limit (optional), starting_after (optional), outcome (optional)
result = api.payouts_account_validations_list(limit=25, outcome='valid')
import os

import requests

# query parameters: limit (optional), starting_after (optional), outcome (optional)
response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/account_validations?limit=25&outcome=valid',
    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\ROUTEApi(new GuzzleHttp\Client(), $config);

# query parameters: limit (optional), starting_after (optional), outcome (optional)
$result = $api->payoutsAccountValidationsList(limit: 25, outcome: 'valid');
<?php
// query parameters: limit (optional), starting_after (optional), outcome (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/account_validations?limit=25&outcome=valid');
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.RouteApi;
import com.droomwork.sdk.model.*;

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

// query parameters: limit (optional), starting_after (optional), outcome (optional)
var result = api.payoutsAccountValidationsList(25, null, RouteValidationOutcome.fromValue("valid"));
// query parameters: limit (optional), starting_after (optional), outcome (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/account_validations?limit=25&outcome=valid"))
    .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 ROUTEApi(config);

// query parameters: limit (optional), starting_after (optional), outcome (optional)
var result = api.PayoutsAccountValidationsList(limit: 25, outcome: RouteValidationOutcome.Valid);
// query parameters: limit (optional), starting_after (optional), outcome (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/account_validations?limit=25&outcome=valid");
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), outcome (optional)
result, _, err := client.ROUTEAPI.PayoutsAccountValidationsList(ctx).Limit(25).Outcome(droomwork.RouteValidationOutcome("valid")).Execute()
// query parameters: limit (optional), starting_after (optional), outcome (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/account_validations?limit=25&outcome=valid", 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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "account_validation",
      "subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "outcome": "valid",
      "expected_name": "example",
      "bank_name_returned": "example",
      "similarity": 1,
      "source": "example",
      "checked_at": "2026-09-01T09:00:00Z"
    }
  ],
  "has_more": true
}
POST/v1/payouts/account_validations#

Validate destination accounts ahead of payday

payouts.account_validations.create

This asks the bank holding a payee's account what name that account is in. The answer is the bank's own, which is the one thing your records cannot supply, and each check is kept as a record you can read back later.

Send the sub_ references of the payees you want checked, as subject_ids, at least one. No account number goes up and none comes back. No instruction, run or payment has to exist first.

This check belongs to the payment provider. Deciding internally that a name is close enough does not answer it, whatever your own policy allows.

Your validations are read back at GET /v1/payouts/account_validations, and one on its own at GET /v1/payouts/account_validations/{account_validation_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

subject_ids array of string required

A sweep across a roster. Run it days before payday.

Returns

The sweep itself, not the answers, which arrive one per payee afterwards.

object always "account_validation_batch" required

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

id string required

The sweep's identifier, returned by POST /v1/payouts/account_validations when you send a batch of subject_ids. It never changes; quote it when you raise a question about the sweep.

subject_count integer required

How many people this sweep covers: the number of subject_ids you sent. Expect one validation per subject.

status string required

running while subjects are still being checked, completed once every subject has a validation you can read at GET /v1/payouts/account_validations or at results_url.

runningcompleted
results_url string · uri optional

Where to fetch the sweep's results, as an absolute URL. Each subject's validation also appears at GET /v1/payouts/account_validations as it lands.

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/payouts/account_validations" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"subject_ids":["sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"]}'
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsAccountValidationsCreate({
  idempotencyKey: crypto.randomUUID(),
  routeAccountValidationCreateRequest: {"subjectIds":["sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"]},
});
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/account_validations', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    "subject_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.ROUTEApi(client)

result = api.payouts_account_validations_create(body={"subject_ids": ["sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"]})
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/payouts/account_validations',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={"subject_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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsAccountValidationsCreate($idempotencyKey, json_decode('{"subject_ids":["sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"]}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/account_validations');
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_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.RouteApi;

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

var result = api.payoutsAccountValidationsCreate(idempotencyKey, body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/account_validations"))
    .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_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 ROUTEApi(config);

var result = api.PayoutsAccountValidationsCreate(idempotencyKey, body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payouts/account_validations");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
    {
      "subject_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.ROUTEAPI.PayoutsAccountValidationsCreate(ctx).IdempotencyKey(key).RouteAccountValidationCreateRequest(body).Execute()
body := strings.NewReader(`{
  "subject_ids": [
    "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
  ]
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payouts/account_validations", 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": "account_validation_batch",
  "id": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "subject_count": 1,
  "status": "running",
  "results_url": "https://files.sandbox.droomwork.com/example"
}
GET/v1/payouts/account_validations/{account_validation_id}#

One payee's account check in full

payouts.account_validations.retrieve

This hands back a single validation, for when the question is about one person and not about the roster. The identifier never changes, so your own record for that payee can point at the exact check that was run for them.

Pass the validation's id as account_validation_id and you get that one record: who it was for, as subject_id, and what the bank answered. A validation is one check of one payee's account against the name the bank holds for it.

Validations, payouts, rails and instructions all carry identifiers that start route_multi_rail_payout_, so the prefix will not tell you which of them you are holding. Keep an identifier with the record you read it from.

Path parameters

account_validation_id string required

The validation's identifier, from the id of a validation you listed at GET /v1/payouts/account_validations. It starts with route_multi_rail_payout_; a sweep produces one per subject.

Returns

The one check you named, with the payee it covers and the bank's answer.

id string required

The validation's identifier. It starts with route_multi_rail_payout_ and never changes; pass it as account_validation_id to GET /v1/payouts/account_validations/{account_validation_id} to retrieve this record on its own.

object always "account_validation" required

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

subject_id string required

Who the check was for: one of the subject_ids you sent to POST /v1/payouts/account_validations, the sub_ reference that names a payee in lines[].subject_id at POST /v1/payouts/instructions.

outcome string required
validname_mismatchaccount_not_foundunresolved
expected_name string · nullable optional

The name you expected on the account. Compare it with bank_name_returned when the outcome is name_mismatch; null when there was no name to compare.

bank_name_returned string · nullable optional

What the bank holds. Compared with the expected name, and neither is a full account number.

similarity number · nullable optional

How close the two names are, from 0 to 1. A judgement aid, not a decision.

source string · nullable optional

Named when the outcome is unresolved, because silence is never a pass.

checked_at string · date-time optional

When the bank was asked, as an RFC 3339 timestamp in UTC. Read it to see how old a result is before you rely on it on payday.

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/payouts/account_validations/%7Baccount_validation_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsAccountValidationsRetrieve({ accountValidationId: '{account_validation_id}' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/account_validations/%7Baccount_validation_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.ROUTEApi(client)

result = api.payouts_account_validations_retrieve(account_validation_id='{account_validation_id}')
import os

import requests

response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/account_validations/%7Baccount_validation_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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsAccountValidationsRetrieve(account_validation_id: '{account_validation_id}');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/account_validations/%7Baccount_validation_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.RouteApi;

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

var result = api.payoutsAccountValidationsRetrieve("{account_validation_id}");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/account_validations/%7Baccount_validation_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 ROUTEApi(config);

var result = api.PayoutsAccountValidationsRetrieve(accountValidationId: "{account_validation_id}");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/account_validations/%7Baccount_validation_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.ROUTEAPI.PayoutsAccountValidationsRetrieve(ctx, "{account_validation_id}").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/account_validations/%7Baccount_validation_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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "account_validation",
  "subject_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "outcome": "valid",
  "expected_name": "example",
  "bank_name_returned": "example",
  "similarity": 1,
  "source": "example",
  "checked_at": "2026-09-01T09:00:00Z"
}
GET/v1/payouts/settlements#

See every settlement and what it paid

payouts.settlements.list

A settlement is the record that one payout's money arrived, or did not: paid or failed. This reads back the settlements your organisation holds, so your product can take a bank movement and name the payroll line it paid.

Read amount for the figure a settlement is for, in whole minor units with its currency, provider_reference for the rail's own reference for the payment, and origin for the run, payslip and payslip line behind it.

amount is one payout's figure, not an instruction's total, and an instruction split across destinations has one settlement per payout. Any part of origin is null where the payment traces to nothing there, so handle the null.

Carry a settlement's id as settlement_id to GET /v1/payouts/settlements/{settlement_id} to read one in full.

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

Restricts the page to settlements from one payroll run: the run's id from POST /v1/payroll/runs or GET /v1/payroll/runs, starting run_enterprise_, as it reads in origin.run_id. Leave it out for settlements from every run and remittance.

outcome string optional

Restricts the page to one outcome: paid (the money arrived and was independently validated) or failed (it won't). Leave it out to get both.

paidfailed

Returns

The settlements on this page, each with the figure that moved and the trail back to what it paid.

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

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

12 fields of Settlement
id string required

The settlement's identifier. It starts with route_multi_rail_settlement_ and never changes; pass it as settlement_id to GET /v1/payouts/settlements/{settlement_id} or its /receipt, and read it as settlement_id on the payout it closed.

object always "settlement" required

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

payout_id string required

The payout this settlement closes: the id of a payout from GET /v1/payouts/payouts, which starts with route_multi_rail_payout_. When you record a settlement at POST /v1/payouts/settlements, this is the payout_id you sent.

instruction_id string optional

The instruction behind this settlement's payout: the id returned by POST /v1/payouts/instructions, which starts with route_multi_rail_payout_. Read origin for the run, payslip and line it came from.

outcome string required
paidfailed
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.

provider_reference string optional

The rail's own reference for this payment, exactly as it reported it. One of the three things checked against the originating instruction before paid is set, and what you quote to the rail about this payment.

independently_validated boolean required

Amount, destination and reference were checked against the originating instruction. paid is never set on the rail's word alone.

origin object optional

The trail from a bank notification back to a payslip line.

4 fields
run_id string · nullable optional

The payroll run the payment came from: the run_id you sent at POST /v1/payouts/instructions, a run's id from POST /v1/payroll/runs (run_enterprise_). Pass it as run_id to GET /v1/payouts/settlements; null when it traces to no run.

payslip_id string · nullable optional

The payslip within run_id the payment was for: a payslip's id from GET /v1/payroll/payslips, starting run_enterprise_payslip_, one step on the trail from a bank notification to a payslip line. null when it traces to no payslip.

payslip_line_id string · nullable optional

The payslip line this settlement pays: the payslip_line_id you sent on the instruction line at POST /v1/payouts/instructions, a line's id in lines on a payslip from GET /v1/payroll/payslips. null when it traces to no payslip line.

remittance_id string · nullable optional

The remittance the payment was for: the remittance_id you sent at POST /v1/payouts/instructions, a remittance's id from POST /v1/remittance/remittances (remit_authority_rail_remittance_). null when it traces to no remittance.

settled_at string · date-time · nullable optional

When the money arrived, as an RFC 3339 timestamp in UTC. null when there is no such moment, as on a failed settlement.

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), outcome (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payouts/settlements?limit=25&run_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=paid" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

// query parameters: limit (optional), starting_after (optional), run_id (optional), outcome (optional)
const result = await api.payoutsSettlementsList({ limit: 25, runId: 'sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', outcome: 'paid' });
// query parameters: limit (optional), starting_after (optional), run_id (optional), outcome (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/settlements?limit=25&run_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=paid', {
  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.ROUTEApi(client)

# query parameters: limit (optional), starting_after (optional), run_id (optional), outcome (optional)
result = api.payouts_settlements_list(limit=25, run_id='sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', outcome='paid')
import os

import requests

# query parameters: limit (optional), starting_after (optional), run_id (optional), outcome (optional)
response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/settlements?limit=25&run_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=paid',
    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\ROUTEApi(new GuzzleHttp\Client(), $config);

# query parameters: limit (optional), starting_after (optional), run_id (optional), outcome (optional)
$result = $api->payoutsSettlementsList(limit: 25, run_id: 'sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', outcome: 'paid');
<?php
// query parameters: limit (optional), starting_after (optional), run_id (optional), outcome (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/settlements?limit=25&run_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=paid');
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.RouteApi;
import com.droomwork.sdk.model.*;

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

// query parameters: limit (optional), starting_after (optional), run_id (optional), outcome (optional)
var result = api.payoutsSettlementsList(25, null, "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", RouteSettlementOutcome.fromValue("paid"));
// query parameters: limit (optional), starting_after (optional), run_id (optional), outcome (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/settlements?limit=25&run_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=paid"))
    .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 ROUTEApi(config);

// query parameters: limit (optional), starting_after (optional), run_id (optional), outcome (optional)
var result = api.PayoutsSettlementsList(limit: 25, runId: "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", outcome: RouteSettlementOutcome.Paid);
// query parameters: limit (optional), starting_after (optional), run_id (optional), outcome (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/settlements?limit=25&run_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=paid");
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), outcome (optional)
result, _, err := client.ROUTEAPI.PayoutsSettlementsList(ctx).Limit(25).RunId("sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Outcome(droomwork.RouteSettlementOutcome("paid")).Execute()
// query parameters: limit (optional), starting_after (optional), run_id (optional), outcome (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/settlements?limit=25&run_id=sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=paid", 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": "route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "settlement",
      "livemode": false,
      "mocked": true,
      "payout_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "outcome": "paid",
      "amount": {
        "amount": 7500000,
        "currency": "NGN"
      },
      "independently_validated": true,
      "instruction_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "provider_reference": "paye-2026-09-rivers",
      "origin": {
        "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
        "payslip_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
        "payslip_line_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
        "remittance_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
      },
      "settled_at": "2026-09-01T09:00:00Z"
    }
  ],
  "has_more": true
}
POST/v1/payouts/settlements#

Record a confirmation that money arrived

payouts.settlements.create

This records a settlement against a payout: confirmation that its money arrived. So a payment confirmed outside the rail's own reporting reaches the same record your other settlements are read from, with a receipt of its own.

Send the payout_id of the payout that was confirmed. You get the settlement back, with an id starting route_multi_rail_settlement_ to keep.

Take the payout_id from GET /v1/payouts/payouts.

payout_id is what ties a confirmation to the money it is about. Send the payout the statement or the call named, not the instruction behind it.

Read the settlement back at GET /v1/payouts/settlements/{settlement_id}, passing the id as settlement_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 optional

payout_id string optional

The payout this settles: the id of a payout from GET /v1/payouts/payouts or GET /v1/payouts/payouts/{payout_id}, which starts with route_multi_rail_payout_. Send the one the rail, statement or call confirmed.

Returns

The settlement as recorded, with the id to read it back or fetch its receipt.

id string required

The settlement's identifier. It starts with route_multi_rail_settlement_ and never changes; pass it as settlement_id to GET /v1/payouts/settlements/{settlement_id} or its /receipt, and read it as settlement_id on the payout it closed.

object always "settlement" required

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

payout_id string required

The payout this settlement closes: the id of a payout from GET /v1/payouts/payouts, which starts with route_multi_rail_payout_. When you record a settlement at POST /v1/payouts/settlements, this is the payout_id you sent.

instruction_id string optional

The instruction behind this settlement's payout: the id returned by POST /v1/payouts/instructions, which starts with route_multi_rail_payout_. Read origin for the run, payslip and line it came from.

outcome string required
paidfailed
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.

provider_reference string optional

The rail's own reference for this payment, exactly as it reported it. One of the three things checked against the originating instruction before paid is set, and what you quote to the rail about this payment.

independently_validated boolean required

Amount, destination and reference were checked against the originating instruction. paid is never set on the rail's word alone.

origin object optional

The trail from a bank notification back to a payslip line.

4 fields
run_id string · nullable optional

The payroll run the payment came from: the run_id you sent at POST /v1/payouts/instructions, a run's id from POST /v1/payroll/runs (run_enterprise_). Pass it as run_id to GET /v1/payouts/settlements; null when it traces to no run.

payslip_id string · nullable optional

The payslip within run_id the payment was for: a payslip's id from GET /v1/payroll/payslips, starting run_enterprise_payslip_, one step on the trail from a bank notification to a payslip line. null when it traces to no payslip.

payslip_line_id string · nullable optional

The payslip line this settlement pays: the payslip_line_id you sent on the instruction line at POST /v1/payouts/instructions, a line's id in lines on a payslip from GET /v1/payroll/payslips. null when it traces to no payslip line.

remittance_id string · nullable optional

The remittance the payment was for: the remittance_id you sent at POST /v1/payouts/instructions, a remittance's id from POST /v1/remittance/remittances (remit_authority_rail_remittance_). null when it traces to no remittance.

settled_at string · date-time · nullable optional

When the money arrived, as an RFC 3339 timestamp in UTC. null when there is no such moment, as on a failed settlement.

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.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/payouts/settlements" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"payout_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}'
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsSettlementsCreate({});
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/settlements', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    "payout_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.ROUTEApi(client)

result = api.payouts_settlements_create()
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/payouts/settlements',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={"payout_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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsSettlementsCreate();
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/settlements');
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 => '{"payout_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.RouteApi;

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

var result = api.payoutsSettlementsCreate();
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/settlements"))
    .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("""
        {
          "payout_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 ROUTEApi(config);

var result = api.PayoutsSettlementsCreate();
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payouts/settlements");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
    {
      "payout_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.ROUTEAPI.PayoutsSettlementsCreate(ctx).Execute()
body := strings.NewReader(`{
  "payout_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payouts/settlements", 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": "route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "settlement",
  "livemode": false,
  "mocked": true,
  "payout_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "outcome": "paid",
  "amount": {
    "amount": 7500000,
    "currency": "NGN"
  },
  "independently_validated": true,
  "instruction_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "provider_reference": "paye-2026-09-rivers",
  "origin": {
    "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
    "payslip_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
    "payslip_line_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
    "remittance_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
  },
  "settled_at": "2026-09-01T09:00:00Z"
}
GET/v1/payouts/settlements/{settlement_id}#

Check whether one payout's money arrived

payouts.settlements.retrieve

This reads one settlement back: whether one payout's money arrived, the figure that moved, and the rail's own reference for it. So a screen in your product answers a question about one payment weeks later from the identifier it stored, and shows the rail's own reference to quote back.

Send the settlement's id in the path as settlement_id. You get outcome, the amount it is for in whole minor units with its currency, provider_reference and settled_at.

settled_at is null where there is no such moment, as on a failed settlement. Read outcome first; a missing timestamp is not a payment still on its way.

The receipt for this settlement is at GET /v1/payouts/settlements/{settlement_id}/receipt, under the same settlement_id, and there is one whether the outcome was paid or failed. A settlement_id nothing holds answers not_found.

Path parameters

settlement_id string required

The settlement's identifier, from the id of a settlement you listed at GET /v1/payouts/settlements or recorded at POST /v1/payouts/settlements, or from a payout's settlement_id. It starts with route_multi_rail_settlement_.

Returns

One settlement as it stands, with the outcome, the figure that moved and the rail reference for it.

id string required

The settlement's identifier. It starts with route_multi_rail_settlement_ and never changes; pass it as settlement_id to GET /v1/payouts/settlements/{settlement_id} or its /receipt, and read it as settlement_id on the payout it closed.

object always "settlement" required

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

payout_id string required

The payout this settlement closes: the id of a payout from GET /v1/payouts/payouts, which starts with route_multi_rail_payout_. When you record a settlement at POST /v1/payouts/settlements, this is the payout_id you sent.

instruction_id string optional

The instruction behind this settlement's payout: the id returned by POST /v1/payouts/instructions, which starts with route_multi_rail_payout_. Read origin for the run, payslip and line it came from.

outcome string required
paidfailed
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.

provider_reference string optional

The rail's own reference for this payment, exactly as it reported it. One of the three things checked against the originating instruction before paid is set, and what you quote to the rail about this payment.

independently_validated boolean required

Amount, destination and reference were checked against the originating instruction. paid is never set on the rail's word alone.

origin object optional

The trail from a bank notification back to a payslip line.

4 fields
run_id string · nullable optional

The payroll run the payment came from: the run_id you sent at POST /v1/payouts/instructions, a run's id from POST /v1/payroll/runs (run_enterprise_). Pass it as run_id to GET /v1/payouts/settlements; null when it traces to no run.

payslip_id string · nullable optional

The payslip within run_id the payment was for: a payslip's id from GET /v1/payroll/payslips, starting run_enterprise_payslip_, one step on the trail from a bank notification to a payslip line. null when it traces to no payslip.

payslip_line_id string · nullable optional

The payslip line this settlement pays: the payslip_line_id you sent on the instruction line at POST /v1/payouts/instructions, a line's id in lines on a payslip from GET /v1/payroll/payslips. null when it traces to no payslip line.

remittance_id string · nullable optional

The remittance the payment was for: the remittance_id you sent at POST /v1/payouts/instructions, a remittance's id from POST /v1/remittance/remittances (remit_authority_rail_remittance_). null when it traces to no remittance.

settled_at string · date-time · nullable optional

When the money arrived, as an RFC 3339 timestamp in UTC. null when there is no such moment, as on a failed settlement.

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/payouts/settlements/route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsSettlementsRetrieve({ settlementId: 'route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/settlements/route_multi_rail_settlement_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.ROUTEApi(client)

result = api.payouts_settlements_retrieve(settlement_id='route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/settlements/route_multi_rail_settlement_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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsSettlementsRetrieve(settlement_id: 'route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/settlements/route_multi_rail_settlement_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.RouteApi;

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

var result = api.payoutsSettlementsRetrieve("route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/settlements/route_multi_rail_settlement_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 ROUTEApi(config);

var result = api.PayoutsSettlementsRetrieve(settlementId: "route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/settlements/route_multi_rail_settlement_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.ROUTEAPI.PayoutsSettlementsRetrieve(ctx, "route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/settlements/route_multi_rail_settlement_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": "route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "settlement",
  "livemode": false,
  "mocked": true,
  "payout_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "outcome": "paid",
  "amount": {
    "amount": 7500000,
    "currency": "NGN"
  },
  "independently_validated": true,
  "instruction_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "provider_reference": "paye-2026-09-rivers",
  "origin": {
    "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
    "payslip_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
    "payslip_line_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
    "remittance_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
  },
  "settled_at": "2026-09-01T09:00:00Z"
}
GET/v1/payouts/settlements/{settlement_id}/receipt#

Prove one payment after the fact

payouts.receipts.retrieve

A receipt is the record that proves a single settlement, and each settlement has exactly one. Nothing updates or removes a receipt once it exists, so a copy your product keeps never goes stale.

Pass the settlement_id of a settlement you listed or recorded, and you get its receipt, naming the settlement it covers and the amount it attests in whole minor units with its currency.

No call takes a receipt onward, so one payment's trail ends here. Where you need the payout it closed or the run behind it, read the settlement itself at GET /v1/payouts/settlements/{settlement_id}.

Path parameters

settlement_id string required

The settlement's identifier, from the id of a settlement you listed at GET /v1/payouts/settlements or recorded at POST /v1/payouts/settlements, or from a payout's settlement_id. It starts with route_multi_rail_settlement_.

Returns

The receipt for that one settlement, which is what you keep and quote about that payment.

id string required

The receipt's identifier, as returned by GET /v1/payouts/settlements/{settlement_id}/receipt. It never changes, and neither does the receipt: once issued, nothing on it is updated or removed.

object always "payout_receipt" required

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

settlement_id string required

The settlement this receipt proves: the id of a settlement from GET /v1/payouts/settlements, starting route_multi_rail_settlement_, the same settlement_id you passed in the path. There is one receipt per settlement, paid or failed.

outcome string required
paidfailed
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.

hash string required

The receipt's own hash, as the algorithm and the digest, such as sha256:9f2c1e0043a1b8. The next receipt carries it as previous_hash, which is what lets you check the chain.

previous_hash string required

The hash of the receipt issued just before this one. Each receipt carries the last one's hash, so you can walk the chain back and confirm nothing was changed or removed.

trace_references array of string optional

The payroll and remittance traces this receipt is linked to.

issued_at string · date-time optional

When the receipt was issued and chained, as an RFC 3339 timestamp in UTC. From this moment it never changes.

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/payouts/settlements/route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/receipt" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsReceiptsRetrieve({ settlementId: 'route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/settlements/route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/receipt', {
  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.ROUTEApi(client)

result = api.payouts_receipts_retrieve(settlement_id='route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/settlements/route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/receipt',
    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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsReceiptsRetrieve(settlement_id: 'route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/settlements/route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/receipt');
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.RouteApi;

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

var result = api.payoutsReceiptsRetrieve("route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/settlements/route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/receipt"))
    .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 ROUTEApi(config);

var result = api.PayoutsReceiptsRetrieve(settlementId: "route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/settlements/route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/receipt");
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.ROUTEAPI.PayoutsReceiptsRetrieve(ctx, "route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/settlements/route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/receipt", 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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "payout_receipt",
  "settlement_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "outcome": "paid",
  "amount": {
    "amount": 7500000,
    "currency": "NGN"
  },
  "hash": "sha256:9f2c1e0043a1b8",
  "previous_hash": "sha256:9f2c1e0043a1b8",
  "trace_references": [
    "paye-2026-09-rivers"
  ],
  "issued_at": "2026-09-01T09:00:00Z"
}
GET/v1/payouts/reconciliations#

Check which days are still in question

payouts.reconciliations.list

A reconciliation is one day's check: what the provider's statement says went out that day, set against the record of what was paid. Listing them is how a screen in your own product can show a day as agreed or still in question, with nobody comparing a statement by hand.

You send nothing, and get back a page of these days, each one carrying the day it covers, both totals in whole minor units with its currency, and whether it balanced. Keep each id, which starts route_multi_rail_settlement_.

Read one day in full at GET /v1/payouts/reconciliations/{reconciliation_id}, passing the id you kept as reconciliation_id.

Query parameters

limit integer optional

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

on string optional

Restricts the page to the reconciliation for one day, as YYYY-MM-DD, such as 2026-09-01, matching on on the record. One is produced for every day; leave it out to get every day.

Returns

A page of days, each saying whether it balanced and how many breaks it found.

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

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

9 fields of Reconciliation
id string required

The reconciliation's identifier. It starts with route_multi_rail_settlement_ and never changes; pass it as reconciliation_id to GET /v1/payouts/reconciliations/{reconciliation_id} and to /exceptions under it to list the day's breaks.

object always "reconciliation" required

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

on string · date required

The day this reconciliation covers, as YYYY-MM-DD, such as 2026-09-01: that day's provider statement checked against what was paid. Filter the list by it with the on query.

balanced boolean required

true when the day's provider statement and the record of what was paid agree, so break_count is 0. false means at least one break, each raised as an exception for you to answer.

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

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

break_count integer · minimum 0 required

How many breaks the day's check found, from 0 up: where the provider's statement and the record of what was paid disagree. 0 when balanced is true; each break is listed as an exception under /exceptions.

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), on (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payouts/reconciliations?limit=25&on=2026-09-01" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

// query parameters: limit (optional), on (optional)
const result = await api.payoutsReconciliationsList({ limit: 25, on: '2026-09-01' });
// query parameters: limit (optional), on (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/reconciliations?limit=25&on=2026-09-01', {
  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.ROUTEApi(client)

# query parameters: limit (optional), on (optional)
result = api.payouts_reconciliations_list(limit=25, on='2026-09-01')
import os

import requests

# query parameters: limit (optional), on (optional)
response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/reconciliations?limit=25&on=2026-09-01',
    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\ROUTEApi(new GuzzleHttp\Client(), $config);

# query parameters: limit (optional), on (optional)
$result = $api->payoutsReconciliationsList(limit: 25, on: '2026-09-01');
<?php
// query parameters: limit (optional), on (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/reconciliations?limit=25&on=2026-09-01');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.droomwork.sdk.ApiClient;
import com.droomwork.sdk.Configuration;
import com.droomwork.sdk.api.RouteApi;

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

// query parameters: limit (optional), on (optional)
var result = api.payoutsReconciliationsList(25, "2026-09-01");
// query parameters: limit (optional), on (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/reconciliations?limit=25&on=2026-09-01"))
    .header("Droomwork-Api-Key", System.getenv("DROOMWORK_API_KEY"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using 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 ROUTEApi(config);

// query parameters: limit (optional), on (optional)
var result = api.PayoutsReconciliationsList(limit: 25, on: "2026-09-01");
// query parameters: limit (optional), on (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/reconciliations?limit=25&on=2026-09-01");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import 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), on (optional)
result, _, err := client.ROUTEAPI.PayoutsReconciliationsList(ctx).Limit(25).On("2026-09-01").Execute()
// query parameters: limit (optional), on (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/reconciliations?limit=25&on=2026-09-01", nil)
	req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "list",
  "data": [
    {
      "id": "route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "reconciliation",
      "livemode": false,
      "mocked": true,
      "on": "2026-09-01",
      "balanced": true,
      "break_count": 0,
      "provider_total": {
        "amount": 57890000,
        "currency": "NGN"
      },
      "ledger_total": {
        "amount": 57900000,
        "currency": "NGN"
      }
    }
  ],
  "has_more": true
}
GET/v1/payouts/reconciliations/{reconciliation_id}#

What one day's statement check found

payouts.reconciliations.retrieve

This reads one day's check on its own. A break is a payment where the provider's statement and the record of what was paid disagree, and the count on this record is what lets your product leave an agreed day alone and put only the days carrying breaks in front of somebody.

You pass the reconciliation_id you kept from the list, and that day comes back with both totals in whole minor units with its currency, whether it balanced, and how many breaks the check found.

The breaks themselves are not here, only how many there are. Read them at GET /v1/payouts/reconciliations/{reconciliation_id}/exceptions under the same identifier, where each one carries the amount it concerns and whether somebody has answered it.

Path parameters

reconciliation_id string required

The reconciliation's identifier, from the id of a reconciliation you listed at GET /v1/payouts/reconciliations. It starts with route_multi_rail_settlement_ and names one day's check.

Returns

The one day you asked for, with both totals and its count of breaks.

id string required

The reconciliation's identifier. It starts with route_multi_rail_settlement_ and never changes; pass it as reconciliation_id to GET /v1/payouts/reconciliations/{reconciliation_id} and to /exceptions under it to list the day's breaks.

object always "reconciliation" required

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

on string · date required

The day this reconciliation covers, as YYYY-MM-DD, such as 2026-09-01: that day's provider statement checked against what was paid. Filter the list by it with the on query.

balanced boolean required

true when the day's provider statement and the record of what was paid agree, so break_count is 0. false means at least one break, each raised as an exception for you to answer.

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

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

break_count integer · minimum 0 required

How many breaks the day's check found, from 0 up: where the provider's statement and the record of what was paid disagree. 0 when balanced is true; each break is listed as an exception under /exceptions.

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/payouts/reconciliations/%7Breconciliation_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsReconciliationsRetrieve({ reconciliationId: '{reconciliation_id}' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/reconciliations/%7Breconciliation_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.ROUTEApi(client)

result = api.payouts_reconciliations_retrieve(reconciliation_id='{reconciliation_id}')
import os

import requests

response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/reconciliations/%7Breconciliation_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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsReconciliationsRetrieve(reconciliation_id: '{reconciliation_id}');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/reconciliations/%7Breconciliation_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.RouteApi;

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

var result = api.payoutsReconciliationsRetrieve("{reconciliation_id}");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/reconciliations/%7Breconciliation_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 ROUTEApi(config);

var result = api.PayoutsReconciliationsRetrieve(reconciliationId: "{reconciliation_id}");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/reconciliations/%7Breconciliation_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.ROUTEAPI.PayoutsReconciliationsRetrieve(ctx, "{reconciliation_id}").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/reconciliations/%7Breconciliation_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": "route_multi_rail_settlement_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "reconciliation",
  "livemode": false,
  "mocked": true,
  "on": "2026-09-01",
  "balanced": true,
  "break_count": 0,
  "provider_total": {
    "amount": 57890000,
    "currency": "NGN"
  },
  "ledger_total": {
    "amount": 57900000,
    "currency": "NGN"
  }
}
GET/v1/payouts/reconciliations/{reconciliation_id}/exceptions#

Read the breaks a day's check found

payouts.exceptions.list

A reconciliation checks one day's provider statement against the record of what was paid, and every place the two disagreed is raised as a break for somebody to answer. The check is done for you, so what you work through here is the disagreements alone, not a statement to compare line by line.

Name the day's reconciliation in the path with an id from GET /v1/payouts/reconciliations, and you get its breaks, each carrying a code for the kind of break and the amount it concerns, in whole minor units with its currency.

Match on code. detail is the same break in words, written for a person to read, so show it and don't branch on it.

A break is never resolved automatically, so one nobody answers stays open. Nothing here closes one, and a reconciliation_id nothing holds answers not_found.

Path parameters

reconciliation_id string required

The reconciliation's identifier, from the id of a reconciliation you listed at GET /v1/payouts/reconciliations. It starts with route_multi_rail_settlement_ and names one day's check.

Query parameters

limit integer optional

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

Returns

The day's breaks, each with the kind of break, the amount it concerns and the words to read.

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

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

9 fields of ReconciliationException
id string required

The exception's identifier, as id on each break listed at GET /v1/payouts/reconciliations/{reconciliation_id}/exceptions. It never changes; use it to follow one break from open to resolved.

object always "reconciliation_exception" required

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

reconciliation_id string optional

The day's reconciliation this break belongs to: the id of a reconciliation from GET /v1/payouts/reconciliations, starting route_multi_rail_settlement_, the same reconciliation_id you listed the exceptions under.

code string required

Which kind of break: missing_in_ledger (the provider shows a payment we don't), missing_at_provider (we show one the provider doesn't), amount_mismatch (the amounts differ) or duplicate_at_provider (paid twice). detail says it in words.

missing_in_ledgermissing_at_provideramount_mismatchduplicate_at_provider
detail string required

What the break is, in words, written for a person to read. Match on code, not on this.

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.

status string required

open until somebody answers the break, resolved once they have, with resolved_by and reason_code saying who and why. A break is never resolved automatically.

openresolved
resolved_by string · nullable optional

Who resolved the break, as the identifier of the person who did. null while the break is still open: it is never resolved automatically, so this is always somebody's decision.

reason_code string · nullable optional

The reason recorded against the break, as a code you can match on, such as no_payee_destination. null while none has been recorded.

has_more boolean required

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: limit (optional)
curl -X GET "https://sandbox.droomwork.io/v1/payouts/reconciliations/%7Breconciliation_id%7D/exceptions?limit=25" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

// query parameters: limit (optional)
const result = await api.payoutsExceptionsList({ reconciliationId: '{reconciliation_id}', limit: 25 });
// query parameters: limit (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/reconciliations/%7Breconciliation_id%7D/exceptions?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.ROUTEApi(client)

# query parameters: limit (optional)
result = api.payouts_exceptions_list(reconciliation_id='{reconciliation_id}', limit=25)
import os

import requests

# query parameters: limit (optional)
response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/reconciliations/%7Breconciliation_id%7D/exceptions?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\ROUTEApi(new GuzzleHttp\Client(), $config);

# query parameters: limit (optional)
$result = $api->payoutsExceptionsList(reconciliation_id: '{reconciliation_id}', limit: 25);
<?php
// query parameters: limit (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/reconciliations/%7Breconciliation_id%7D/exceptions?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.RouteApi;

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

// query parameters: limit (optional)
var result = api.payoutsExceptionsList("{reconciliation_id}", 25);
// query parameters: limit (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/reconciliations/%7Breconciliation_id%7D/exceptions?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 ROUTEApi(config);

// query parameters: limit (optional)
var result = api.PayoutsExceptionsList(reconciliationId: "{reconciliation_id}", limit: 25);
// query parameters: limit (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/reconciliations/%7Breconciliation_id%7D/exceptions?limit=25");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import droomwork "github.com/fenibofubara/droomwork-sdk-go"

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

// query parameters: limit (optional)
result, _, err := client.ROUTEAPI.PayoutsExceptionsList(ctx, "{reconciliation_id}").Limit(25).Execute()
// query parameters: limit (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/reconciliations/%7Breconciliation_id%7D/exceptions?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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "reconciliation_exception",
      "code": "missing_in_ledger",
      "detail": "The payee has no verified destination, so this line cannot be paid.",
      "amount": {
        "amount": 7500000,
        "currency": "NGN"
      },
      "status": "open",
      "reconciliation_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "resolved_by": "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "reason_code": "no_payee_destination"
    }
  ],
  "has_more": true
}
GET/v1/payouts/readiness#

What ROUTE needs before it moves money

payouts.readiness.retrieve

ROUTE depends on facts other parts of the pipeline own, and this call lists the ones it asks for. So a dependency on another part of the pipeline is something your integration can plan for from the start, and not something you discover at the first run.

Send no parameters. You get one rows entry per fact ROUTE requires, each carrying the module that owns it when you run integrated and the assurance that fact has to reach, plus always_enforced beside them.

ROUTE isn't sold on its own. It moves money, so live access follows the partner bank's own onboarding rules, and that isn't anything you can settle here.

Returns

Every fact ROUTE requires, with the module that owns it, so you can plan the dependencies.

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.

rails_without_credentials array of string optional

The rails you hold no credentials for, each one a rail from GET /v1/payouts/rails whose credentials_held is false. Empty when you hold credentials for every rail.

partner_bank_onboarding string optional

Where you stand in the partner bank's onboarding: not_started, in_progress (under way with the bank) or complete (the bank has finished onboarding you). Live access follows the bank's own rules, and ROUTE isn't sold on its own.

not_startedin_progresscomplete

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/payouts/readiness" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsReadinessRetrieve({});
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/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.ROUTEApi(client)

result = api.payouts_readiness_retrieve()
import os

import requests

response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsReadinessRetrieve();
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/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.RouteApi;

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

var result = api.payoutsReadinessRetrieve();
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/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 ROUTEApi(config);

var result = api.PayoutsReadinessRetrieve();
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/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.ROUTEAPI.PayoutsReadinessRetrieve(ctx).Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/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": "route",
  "mode": "integrated",
  "ready": true,
  "rows": [
    {
      "fact": "signed_payout_instruction",
      "owner": "run",
      "contract_row": "6.3.1",
      "required": "verified",
      "held": "verified",
      "source": "integrated",
      "satisfied": true,
      "missing_because": null
    },
    {
      "fact": "payee_destination",
      "owner": "anchor",
      "contract_row": "6.3.1",
      "required": "verified",
      "held": "verified",
      "source": "integrated",
      "satisfied": true,
      "missing_because": null
    }
  ],
  "always_enforced": [
    "consent_token",
    "bank_account_validation",
    "tax_identifier_validation",
    "duplicate_detection"
  ],
  "rails_without_credentials": [],
  "partner_bank_onboarding": "not_started"
}
POST/v1/payouts/instructions/{instruction_id}/execute#

Execute a planned instruction

payouts.instructions.execute

This is the call that sends the payments out. Everything before it is preparation, so your product has one exact moment to record as the moment a payday went, and a state to show for it.

Send the instruction's id in the path and no request body. You get the instruction back in executing, which is where it stays until an outcome is recorded against it.

The instruction has to be in planned, which POST /v1/payouts/instructions/{instruction_id}/plan leaves it in. One in any other state answers conflict.

Two calls take it on from here: POST /v1/payouts/instructions/{instruction_id}/resolve once the rail has answered, and POST /v1/payouts/instructions/{instruction_id}/mark_unknown while it has not. A payday that cannot be paid at all closes at POST /v1/payouts/instructions/{instruction_id}/fail.

Path parameters

instruction_id string required

The instruction's identifier, starting with route_multi_rail_payout_: the id returned by POST /v1/payouts/instructions when you submitted it, or of one listed at GET /v1/payouts/instructions.

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 instruction in executing, which is the state its payments go out under.

id string required

The instruction's identifier, starting with route_multi_rail_payout_; it never changes. You get it from POST /v1/payouts/instructions when you submit, or from GET /v1/payouts/instructions, and pass it as instruction_id in a path.

object always "payout_instruction" required

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

unknown is a real state and not a failure. An instruction whose rail did not answer is resolved by asking what happened, never by assuming the money did not move. Treating unknown as failed is how someone gets paid twice.

receivedquarantinedfunding_pendingplannedexecutingunknownsettledpartially_settledfailed
source string required

Which of these signed the amounts. You never supply an amount yourself.

runremit
run_id string · nullable optional

The payroll run these payouts came from, when source is run: the run's id from POST /v1/payroll/runs, starting with run_enterprise_, as you sent it in run_id. null when the instruction came from a remittance.

remittance_id string · nullable optional

The remittance these payouts came from, when source is remit: the remittance's id from POST /v1/remittance/remittances, starting with remit_authority_rail_remittance_, as you sent it. null when they came from a payroll run.

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.

line_count integer · minimum 1 required

How many lines the instruction carries, at least 1. Each line names one payee, so this is how many people it pays, whatever splits they carry.

idempotency_key string optional

Derived from the instruction itself, so the same payment always produces the same key. This is what makes a retry after a timeout safe.

signature_verified boolean optional

true when the signature from payroll or remittance checked out, false when it didn't. A failed signature quarantines the instruction with quarantine_reason signature_invalid.

quarantine_reason string · nullable optional

Why the instruction was held, null unless status is quarantined: signature_invalid, duplicate_instruction (already submitted) or integrity_mismatch (a line failed its check). A held instruction is kept as evidence and never executed.

signature_invalidduplicate_instructionintegrity_mismatchnull
route_plan_id string · nullable optional

The plan that will carry this instruction: the id, starting with route_multi_rail_route_plan_, of the plan made by POST /v1/payouts/instructions/{instruction_id}/plan. null until you plan it, and execute is refused while it is null.

created_at string · date-time optional

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

Other responses

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

Errors it can return

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

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

const result = await api.payoutsInstructionsExecute({ instructionId: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_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.ROUTEApi(client)

result = api.payouts_instructions_execute(instruction_id='route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsInstructionsExecute(instruction_id: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_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.RouteApi;

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

var result = api.payoutsInstructionsExecute("route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_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 ROUTEApi(config);

var result = api.PayoutsInstructionsExecute(instructionId: "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_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.ROUTEAPI.PayoutsInstructionsExecute(ctx, "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "payout_instruction",
  "livemode": false,
  "mocked": true,
  "status": "executing",
  "source": "run",
  "total": {
    "amount": 57900000,
    "currency": "NGN"
  },
  "line_count": 1,
  "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "remittance_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "idempotency_key": "example",
  "signature_verified": true,
  "quarantine_reason": "signature_invalid",
  "route_plan_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "created_at": "2026-09-01T09:00:00Z"
}
POST/v1/payouts/instructions/{instruction_id}/fail#

Close an instruction that cannot be paid

payouts.instructions.fail

This closes an instruction that cannot go through at all, as a decision somebody took and recorded. So your product has a final state for a payday that stopped, and the stop is on the record as a stop.

Send the instruction's id in the path and no request body. You get the instruction back in failed, carrying the total and line_count it arrived with.

The instruction has to be in executing or unknown, the same two states resolve accepts. One in received or planned answers conflict, and no call abandons an instruction before it has executed: received accepts only planning, and planned only executing.

Nothing is retried from here and nothing leads out of failed. Where the rail has not answered yet, record that instead at POST /v1/payouts/instructions/{instruction_id}/mark_unknown, and close it at POST /v1/payouts/instructions/{instruction_id}/resolve once you know.

Path parameters

instruction_id string required

The instruction's identifier, starting with route_multi_rail_payout_: the id returned by POST /v1/payouts/instructions when you submitted it, or of one listed at GET /v1/payouts/instructions.

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 instruction in failed, which is the end of it.

id string required

The instruction's identifier, starting with route_multi_rail_payout_; it never changes. You get it from POST /v1/payouts/instructions when you submit, or from GET /v1/payouts/instructions, and pass it as instruction_id in a path.

object always "payout_instruction" required

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

unknown is a real state and not a failure. An instruction whose rail did not answer is resolved by asking what happened, never by assuming the money did not move. Treating unknown as failed is how someone gets paid twice.

receivedquarantinedfunding_pendingplannedexecutingunknownsettledpartially_settledfailed
source string required

Which of these signed the amounts. You never supply an amount yourself.

runremit
run_id string · nullable optional

The payroll run these payouts came from, when source is run: the run's id from POST /v1/payroll/runs, starting with run_enterprise_, as you sent it in run_id. null when the instruction came from a remittance.

remittance_id string · nullable optional

The remittance these payouts came from, when source is remit: the remittance's id from POST /v1/remittance/remittances, starting with remit_authority_rail_remittance_, as you sent it. null when they came from a payroll run.

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.

line_count integer · minimum 1 required

How many lines the instruction carries, at least 1. Each line names one payee, so this is how many people it pays, whatever splits they carry.

idempotency_key string optional

Derived from the instruction itself, so the same payment always produces the same key. This is what makes a retry after a timeout safe.

signature_verified boolean optional

true when the signature from payroll or remittance checked out, false when it didn't. A failed signature quarantines the instruction with quarantine_reason signature_invalid.

quarantine_reason string · nullable optional

Why the instruction was held, null unless status is quarantined: signature_invalid, duplicate_instruction (already submitted) or integrity_mismatch (a line failed its check). A held instruction is kept as evidence and never executed.

signature_invalidduplicate_instructionintegrity_mismatchnull
route_plan_id string · nullable optional

The plan that will carry this instruction: the id, starting with route_multi_rail_route_plan_, of the plan made by POST /v1/payouts/instructions/{instruction_id}/plan. null until you plan it, and execute is refused while it is null.

created_at string · date-time optional

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/fail" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsInstructionsFail({ instructionId: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/fail', {
  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.ROUTEApi(client)

result = api.payouts_instructions_fail(instruction_id='route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/fail',
    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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsInstructionsFail(instruction_id: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/fail');
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.RouteApi;

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

var result = api.payoutsInstructionsFail("route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/fail"))
    .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 ROUTEApi(config);

var result = api.PayoutsInstructionsFail(instructionId: "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/fail");
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.ROUTEAPI.PayoutsInstructionsFail(ctx, "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/fail", 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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "payout_instruction",
  "livemode": false,
  "mocked": true,
  "status": "failed",
  "source": "run",
  "total": {
    "amount": 57900000,
    "currency": "NGN"
  },
  "line_count": 1,
  "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "remittance_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "idempotency_key": "example",
  "signature_verified": true,
  "quarantine_reason": "signature_invalid",
  "route_plan_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "created_at": "2026-09-01T09:00:00Z"
}
POST/v1/payouts/instructions/{instruction_id}/mark_unknown#

Record that a rail has not answered

payouts.instructions.mark_unknown

This separates a payday whose sending is over from one still going out. An instruction left in executing reads as still working; moving it here says the sending finished and the answer is missing, so a screen in your product can show the two apart.

Send the instruction's id in the path and no request body. You get the instruction back in unknown, carrying the total and line_count it arrived with.

The instruction has to be in executing, which POST /v1/payouts/instructions/{instruction_id}/execute leaves it in. One in any other state answers conflict.

Two calls lead out of unknown: POST /v1/payouts/instructions/{instruction_id}/resolve once you hold the rail's answer, and POST /v1/payouts/instructions/{instruction_id}/fail where the money did not move.

Path parameters

instruction_id string required

The instruction's identifier, starting with route_multi_rail_payout_: the id returned by POST /v1/payouts/instructions when you submitted it, or of one listed at GET /v1/payouts/instructions.

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 instruction in unknown, which says its outcome is not established yet.

id string required

The instruction's identifier, starting with route_multi_rail_payout_; it never changes. You get it from POST /v1/payouts/instructions when you submit, or from GET /v1/payouts/instructions, and pass it as instruction_id in a path.

object always "payout_instruction" required

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

unknown is a real state and not a failure. An instruction whose rail did not answer is resolved by asking what happened, never by assuming the money did not move. Treating unknown as failed is how someone gets paid twice.

receivedquarantinedfunding_pendingplannedexecutingunknownsettledpartially_settledfailed
source string required

Which of these signed the amounts. You never supply an amount yourself.

runremit
run_id string · nullable optional

The payroll run these payouts came from, when source is run: the run's id from POST /v1/payroll/runs, starting with run_enterprise_, as you sent it in run_id. null when the instruction came from a remittance.

remittance_id string · nullable optional

The remittance these payouts came from, when source is remit: the remittance's id from POST /v1/remittance/remittances, starting with remit_authority_rail_remittance_, as you sent it. null when they came from a payroll run.

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.

line_count integer · minimum 1 required

How many lines the instruction carries, at least 1. Each line names one payee, so this is how many people it pays, whatever splits they carry.

idempotency_key string optional

Derived from the instruction itself, so the same payment always produces the same key. This is what makes a retry after a timeout safe.

signature_verified boolean optional

true when the signature from payroll or remittance checked out, false when it didn't. A failed signature quarantines the instruction with quarantine_reason signature_invalid.

quarantine_reason string · nullable optional

Why the instruction was held, null unless status is quarantined: signature_invalid, duplicate_instruction (already submitted) or integrity_mismatch (a line failed its check). A held instruction is kept as evidence and never executed.

signature_invalidduplicate_instructionintegrity_mismatchnull
route_plan_id string · nullable optional

The plan that will carry this instruction: the id, starting with route_multi_rail_route_plan_, of the plan made by POST /v1/payouts/instructions/{instruction_id}/plan. null until you plan it, and execute is refused while it is null.

created_at string · date-time optional

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/mark_unknown" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsInstructionsMarkUnknown({ instructionId: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/mark_unknown', {
  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.ROUTEApi(client)

result = api.payouts_instructions_mark_unknown(instruction_id='route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/mark_unknown',
    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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsInstructionsMarkUnknown(instruction_id: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/mark_unknown');
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.RouteApi;

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

var result = api.payoutsInstructionsMarkUnknown("route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/mark_unknown"))
    .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 ROUTEApi(config);

var result = api.PayoutsInstructionsMarkUnknown(instructionId: "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/mark_unknown");
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.ROUTEAPI.PayoutsInstructionsMarkUnknown(ctx, "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/mark_unknown", 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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "payout_instruction",
  "livemode": false,
  "mocked": true,
  "status": "unknown",
  "source": "run",
  "total": {
    "amount": 57900000,
    "currency": "NGN"
  },
  "line_count": 1,
  "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "remittance_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "idempotency_key": "example",
  "signature_verified": true,
  "quarantine_reason": "signature_invalid",
  "route_plan_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "created_at": "2026-09-01T09:00:00Z"
}
POST/v1/payouts/instructions/{instruction_id}/plan#

Plan the route for an instruction

payouts.instructions.plan

Planning is where an instruction is given its rails: which one to try first, and which to fall back to. So your product can see the route a payday will take before any money is sent, and keep that record for afterwards.

Send the instruction's id in the path and no request body. You get the instruction back in planned, and not the route plan itself.

The instruction has to be in received, which is where POST /v1/payouts/instructions leaves it. One in any later state answers conflict.

Planning sends nothing. Read the route it chose at GET /v1/payouts/instructions/{instruction_id}/route_plan, then send the payments at POST /v1/payouts/instructions/{instruction_id}/execute, the only transition planned accepts.

Path parameters

instruction_id string required

The instruction's identifier, starting with route_multi_rail_payout_: the id returned by POST /v1/payouts/instructions when you submitted it, or of one listed at GET /v1/payouts/instructions.

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 instruction in planned, with its rails chosen and nothing sent yet.

id string required

The instruction's identifier, starting with route_multi_rail_payout_; it never changes. You get it from POST /v1/payouts/instructions when you submit, or from GET /v1/payouts/instructions, and pass it as instruction_id in a path.

object always "payout_instruction" required

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

unknown is a real state and not a failure. An instruction whose rail did not answer is resolved by asking what happened, never by assuming the money did not move. Treating unknown as failed is how someone gets paid twice.

receivedquarantinedfunding_pendingplannedexecutingunknownsettledpartially_settledfailed
source string required

Which of these signed the amounts. You never supply an amount yourself.

runremit
run_id string · nullable optional

The payroll run these payouts came from, when source is run: the run's id from POST /v1/payroll/runs, starting with run_enterprise_, as you sent it in run_id. null when the instruction came from a remittance.

remittance_id string · nullable optional

The remittance these payouts came from, when source is remit: the remittance's id from POST /v1/remittance/remittances, starting with remit_authority_rail_remittance_, as you sent it. null when they came from a payroll run.

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.

line_count integer · minimum 1 required

How many lines the instruction carries, at least 1. Each line names one payee, so this is how many people it pays, whatever splits they carry.

idempotency_key string optional

Derived from the instruction itself, so the same payment always produces the same key. This is what makes a retry after a timeout safe.

signature_verified boolean optional

true when the signature from payroll or remittance checked out, false when it didn't. A failed signature quarantines the instruction with quarantine_reason signature_invalid.

quarantine_reason string · nullable optional

Why the instruction was held, null unless status is quarantined: signature_invalid, duplicate_instruction (already submitted) or integrity_mismatch (a line failed its check). A held instruction is kept as evidence and never executed.

signature_invalidduplicate_instructionintegrity_mismatchnull
route_plan_id string · nullable optional

The plan that will carry this instruction: the id, starting with route_multi_rail_route_plan_, of the plan made by POST /v1/payouts/instructions/{instruction_id}/plan. null until you plan it, and execute is refused while it is null.

created_at string · date-time optional

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/plan" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsInstructionsPlan({ instructionId: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/plan', {
  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.ROUTEApi(client)

result = api.payouts_instructions_plan(instruction_id='route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/plan',
    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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsInstructionsPlan(instruction_id: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/plan');
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.RouteApi;

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

var result = api.payoutsInstructionsPlan("route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/plan"))
    .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 ROUTEApi(config);

var result = api.PayoutsInstructionsPlan(instructionId: "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/plan");
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.ROUTEAPI.PayoutsInstructionsPlan(ctx, "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/plan", 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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "payout_instruction",
  "livemode": false,
  "mocked": true,
  "status": "planned",
  "source": "run",
  "total": {
    "amount": 57900000,
    "currency": "NGN"
  },
  "line_count": 1,
  "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "remittance_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "idempotency_key": "example",
  "signature_verified": true,
  "quarantine_reason": "signature_invalid",
  "route_plan_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "created_at": "2026-09-01T09:00:00Z"
}
POST/v1/payouts/instructions/{instruction_id}/resolve#

Settle an instruction once you know the outcome

payouts.instructions.resolve

This is the call that closes a payday as paid. It is the only one that reaches settled, so a period your product shows as complete is one somebody recorded as complete.

Send the instruction's id in the path and no request body. You get the instruction back in settled, carrying the total and line_count it arrived with.

The instruction has to be in executing or unknown. One still in received or planned answers conflict.

Resolve on the answer you hold, never on the time that has passed. Where there is no answer yet, POST /v1/payouts/instructions/{instruction_id}/mark_unknown is the state for it.

Nothing leads out of settled. Where the money did not move, POST /v1/payouts/instructions/{instruction_id}/fail is the close instead.

Path parameters

instruction_id string required

The instruction's identifier, starting with route_multi_rail_payout_: the id returned by POST /v1/payouts/instructions when you submitted it, or of one listed at GET /v1/payouts/instructions.

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 instruction in settled, with the outcome recorded against it.

id string required

The instruction's identifier, starting with route_multi_rail_payout_; it never changes. You get it from POST /v1/payouts/instructions when you submit, or from GET /v1/payouts/instructions, and pass it as instruction_id in a path.

object always "payout_instruction" required

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

unknown is a real state and not a failure. An instruction whose rail did not answer is resolved by asking what happened, never by assuming the money did not move. Treating unknown as failed is how someone gets paid twice.

receivedquarantinedfunding_pendingplannedexecutingunknownsettledpartially_settledfailed
source string required

Which of these signed the amounts. You never supply an amount yourself.

runremit
run_id string · nullable optional

The payroll run these payouts came from, when source is run: the run's id from POST /v1/payroll/runs, starting with run_enterprise_, as you sent it in run_id. null when the instruction came from a remittance.

remittance_id string · nullable optional

The remittance these payouts came from, when source is remit: the remittance's id from POST /v1/remittance/remittances, starting with remit_authority_rail_remittance_, as you sent it. null when they came from a payroll run.

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.

line_count integer · minimum 1 required

How many lines the instruction carries, at least 1. Each line names one payee, so this is how many people it pays, whatever splits they carry.

idempotency_key string optional

Derived from the instruction itself, so the same payment always produces the same key. This is what makes a retry after a timeout safe.

signature_verified boolean optional

true when the signature from payroll or remittance checked out, false when it didn't. A failed signature quarantines the instruction with quarantine_reason signature_invalid.

quarantine_reason string · nullable optional

Why the instruction was held, null unless status is quarantined: signature_invalid, duplicate_instruction (already submitted) or integrity_mismatch (a line failed its check). A held instruction is kept as evidence and never executed.

signature_invalidduplicate_instructionintegrity_mismatchnull
route_plan_id string · nullable optional

The plan that will carry this instruction: the id, starting with route_multi_rail_route_plan_, of the plan made by POST /v1/payouts/instructions/{instruction_id}/plan. null until you plan it, and execute is refused while it is null.

created_at string · date-time optional

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/resolve" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsInstructionsResolve({ instructionId: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/resolve', {
  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.ROUTEApi(client)

result = api.payouts_instructions_resolve(instruction_id='route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/resolve',
    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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsInstructionsResolve(instruction_id: 'route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/resolve');
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.RouteApi;

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

var result = api.payoutsInstructionsResolve("route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/resolve"))
    .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 ROUTEApi(config);

var result = api.PayoutsInstructionsResolve(instructionId: "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/resolve");
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.ROUTEAPI.PayoutsInstructionsResolve(ctx, "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/payouts/instructions/route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/resolve", 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": "route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "payout_instruction",
  "livemode": false,
  "mocked": true,
  "status": "settled",
  "source": "run",
  "total": {
    "amount": 57900000,
    "currency": "NGN"
  },
  "line_count": 1,
  "run_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "remittance_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "idempotency_key": "example",
  "signature_verified": true,
  "quarantine_reason": "signature_invalid",
  "route_plan_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "created_at": "2026-09-01T09:00:00Z"
}
GET/v1/payouts/events#

Replay what happened to one payout instruction

payouts.events.list

ROUTE's own append only record of the payout facts it is the authority for, oldest first. Reading it back is how a consumer that was offline, or one you are writing today against money that moved last month, works through the same facts a webhook delivery carries, in the order they happened.

Pass stream with after and you get that stream's events after the sequence you name, where 0 reads from the start. Leave stream out and you get every stream together, in the order the events were recorded.

A sequence counts inside one stream and never across streams, so store one number per stream and send it with the stream it belongs to. after on its own has nothing to count from.

Read one event in full at GET /v1/payouts/events/{event_id}.

Query parameters

stream string optional

The id of the instruction or payout to replay, from POST /v1/payouts/instructions or GET /v1/payouts/payouts (route_multi_rail_payout_…), with after beside it. Leave it out to read every stream, paged with starting_after.

after integer optional

The sequence of the last event you handled on that stream, as sequence reads on each event; you get the events after it, and 0 reads from the start. Needs stream beside it, because a sequence counts within one stream.

limit integer optional

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

starting_after string optional

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

Returns

A page of events, oldest first; has_more says whether to ask again.

object always "list" required

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

data array of StoredEvent required

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

13 fields of StoredEvent
id string required

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

type string required

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

schema_version integer · minimum 1 required

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

org_id string required

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

sequence integer · minimum 0 required

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

occurred_at string · date-time required

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

request_id string optional

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

livemode boolean required

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

mocked boolean required

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

source string required

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

anchorproofrailflowmatchrunremitroutegatewayiamledgerregistrydeliverydocumentsintelligence
object always "event" required

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

stream string required

The id of the record this event is about, an instruction's or a payout's (route_multi_rail_payout_…); pass it as stream to GET /v1/payouts/events to replay it. sequence counts per organisation and per stream, never across them.

data object required

The record the event is about, in the shape its type names: an instruction, a funding check, a payout, a rail or a reconciliation exception. 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/payouts/events?stream=route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

const api = new ROUTEApi(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.payoutsEventsList({ stream: 'route_multi_rail_payout_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/payouts/events?stream=route_multi_rail_payout_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.ROUTEApi(client)

# query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
result = api.payouts_events_list(stream='route_multi_rail_payout_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/payouts/events?stream=route_multi_rail_payout_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\ROUTEApi(new GuzzleHttp\Client(), $config);

# query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
$result = $api->payoutsEventsList(stream: 'route_multi_rail_payout_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/payouts/events?stream=route_multi_rail_payout_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.RouteApi;

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

// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
var result = api.payoutsEventsList("route_multi_rail_payout_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/payouts/events?stream=route_multi_rail_payout_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 ROUTEApi(config);

// query parameters: stream (optional), after (optional), limit (optional), starting_after (optional)
var result = api.PayoutsEventsList(stream: "route_multi_rail_payout_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/payouts/events?stream=route_multi_rail_payout_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.ROUTEAPI.PayoutsEventsList(ctx).Stream("route_multi_rail_payout_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/payouts/events?stream=route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&after=0&limit=25", nil)
	req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "list",
  "data": [
    {
      "id": "evt_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "type": "run.payslip.calculated",
      "schema_version": 1,
      "org_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "sequence": 0,
      "occurred_at": "2026-09-01T09:00:00Z",
      "livemode": false,
      "mocked": true,
      "source": "anchor",
      "request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "event",
      "stream": "example",
      "data": {}
    }
  ],
  "has_more": true
}
GET/v1/payouts/events/{event_id}#

Confirm a payout fact you were sent

payouts.events.retrieve

One event out of ROUTE's record, read back in full. An event's id never changes and a redelivery carries the same one, so a handler that is not sure whether it has already acted on a payout can settle that here.

Send the identifier from a delivery you received or from GET /v1/payouts/events; it starts with evt_. You get that one event, the stream it belongs to, the sequence it sits at there, and the record the event was about.

Those last two put you back into a replay: send them to GET /v1/payouts/events as stream and after. An event_id nothing holds answers not_found.

Path parameters

event_id string required

The event's identifier, from the id of an event you listed at GET /v1/payouts/events or received on a webhook delivery. It starts with evt_.

Returns

The one event as it was recorded, with the stream and sequence to replay from.

id string required

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

type string required

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

schema_version integer · minimum 1 required

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

org_id string required

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

sequence integer · minimum 0 required

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

occurred_at string · date-time required

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

request_id string optional

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

livemode boolean required

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

mocked boolean required

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

source string required

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

anchorproofrailflowmatchrunremitroutegatewayiamledgerregistrydeliverydocumentsintelligence
object always "event" required

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

stream string required

The id of the record this event is about, an instruction's or a payout's (route_multi_rail_payout_…); pass it as stream to GET /v1/payouts/events to replay it. sequence counts per organisation and per stream, never across them.

data object required

The record the event is about, in the shape its type names: an instruction, a funding check, a payout, a rail or a reconciliation exception. 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/payouts/events/%7Bevent_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsEventsRetrieve({ eventId: '{event_id}' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/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.ROUTEApi(client)

result = api.payouts_events_retrieve(event_id='{event_id}')
import os

import requests

response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsEventsRetrieve(event_id: '{event_id}');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/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.RouteApi;

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

var result = api.payoutsEventsRetrieve("{event_id}");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/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 ROUTEApi(config);

var result = api.PayoutsEventsRetrieve(eventId: "{event_id}");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/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.ROUTEAPI.PayoutsEventsRetrieve(ctx, "{event_id}").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/events/%7Bevent_id%7D", nil)
	req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "evt_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "type": "run.payslip.calculated",
  "schema_version": 1,
  "org_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "sequence": 0,
  "occurred_at": "2026-09-01T09:00:00Z",
  "livemode": false,
  "mocked": true,
  "source": "anchor",
  "request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "event",
  "stream": "example",
  "data": {}
}
GET/v1/payouts/audit_entries#

Who moved money, and who was turned down

payouts.audit_entries.list

Every attempt on your payout records, who made it, and how it ended. So your product can answer who sent an instruction for execution, or who resolved a payout, without keeping a log of its own.

You get entries newest first. Narrow them to one caller with actor_id, to one record with resource_id, to the refusals alone with outcome, or to a window of time.

action is matched exactly against what the entry stored. Something close is not a match: you get an empty page and no refusal, so check the spelling before you believe the log is empty.

A read of yours that worked leaves no row here. What you find is the changes, and the attempts that were turned away.

Read one in full at GET /v1/payouts/audit_entries/{audit_entry_id}.

Query parameters

action string optional

Restricts the list to attempts at one route, as action reads on an entry: the method and route pattern, such as POST /v1/payouts/instructions. Leave it out to get attempts at every route.

actor_id string optional

Only one actor's attempts, matched on the exact actor_id of its entries: the id of the API key (key_…), person (usr_…) or staff account that made them. Add outcome=refused to see repeated refusals; leave it out for every actor.

resource string optional

Only attempts on one kind of record, as resource reads on an entry: the collection segment of the route, such as instructions, payouts or settlements. Pair it with resource_id to narrow to one record; leave it out for every kind.

resource_id string optional

Only attempts on one record, as resource_id reads on an entry: the id the route named, such as an instruction's from POST /v1/payouts/instructions (route_multi_rail_payout_…). Pair it with resource; leave it out for every record.

outcome string optional

Restricts the list to one outcome: succeeded (carried out), refused (you were turned down with a 4xx) or failed (it went wrong on our side, a 5xx). Leave it out to get all three.

succeededrefusedfailed
recorded_after string optional

Only entries recorded after this moment, as an RFC 3339 timestamp in UTC such as 2026-01-01T00:00:00Z; an entry at exactly this instant is left out. Leave it out to go back to the oldest entry.

recorded_before string optional

Only entries recorded before this moment, as an RFC 3339 timestamp in UTC such as 2027-01-01T00:00:00Z; an entry at exactly this instant is left out. Pair it with recorded_after to bound a window, or leave it out to read up to the newest.

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 attempts, newest first, so your audit screen opens on what just happened.

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/payouts/audit_entries lists it. It starts with audit_entry_ and never changes; pass it as audit_entry_id to GET /v1/payouts/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 request and a replayed idempotent request both count as attempts against a mocked route, and the log says so.

at string · date-time required

When the attempt was made, as an RFC 3339 timestamp in UTC. Filter on it with recorded_after and recorded_before when you list.

request_id string required

The identifier of the request that made the attempt. It's the same request_id an error body carries, so you can match a refusal here to the response you were given.

actor_type string required

What kind of caller made the attempt: 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 we acted for you.

actor_id string required

Who made the attempt, as the id of what actor_type names: the API key's (key_…) or client's for client, the person's (usr_…) for user. Pass it as actor_id to GET /v1/payouts/audit_entries to follow one actor.

action string required

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

resource string · nullable optional

The kind of record the attempt was aimed at, as the collection in the route: instructions, payouts, settlements and so on. Null when the route names none.

resource_id string · nullable optional

The id of the one record the route named, such as an instruction's (route_multi_rail_payout_…, from POST /v1/payouts/instructions). null for an attempt on a collection, such as creating an instruction.

outcome string required

How the attempt ended: succeeded means it was carried out, refused means you were turned away with a 4xx, failed means we answered with a 5xx. status carries the exact code.

succeededrefusedfailed
status integer required

The HTTP status the caller was given, such as 201 or 403. Read it with outcome to tell one refusal from another.

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/payouts/audit_entries?action=POST%20%2Fv1%2Fpayouts%2Finstructions&actor_id=usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=instructions&resource_id=route_multi_rail_payout_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, ROUTEApi } from '@droomwork/sdk';

const api = new ROUTEApi(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.payoutsAuditEntriesList({ action: 'POST /v1/payouts/instructions', actorId: 'usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', resource: 'instructions', resourceId: 'route_multi_rail_payout_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/payouts/audit_entries?action=POST%20%2Fv1%2Fpayouts%2Finstructions&actor_id=usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=instructions&resource_id=route_multi_rail_payout_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.ROUTEApi(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.payouts_audit_entries_list(action='POST /v1/payouts/instructions', actor_id='usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', resource='instructions', resource_id='route_multi_rail_payout_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/payouts/audit_entries?action=POST%20%2Fv1%2Fpayouts%2Finstructions&actor_id=usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=instructions&resource_id=route_multi_rail_payout_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\ROUTEApi(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->payoutsAuditEntriesList(action: 'POST /v1/payouts/instructions', actor_id: 'usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', resource: 'instructions', resource_id: 'route_multi_rail_payout_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/payouts/audit_entries?action=POST%20%2Fv1%2Fpayouts%2Finstructions&actor_id=usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=instructions&resource_id=route_multi_rail_payout_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.RouteApi;

ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://sandbox.droomwork.io");
client.setAccessToken(System.getenv("DROOMWORK_API_KEY"));
RouteApi api = new RouteApi(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.payoutsAuditEntriesList("POST /v1/payouts/instructions", "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "instructions", "route_multi_rail_payout_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/payouts/audit_entries?action=POST%20%2Fv1%2Fpayouts%2Finstructions&actor_id=usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=instructions&resource_id=route_multi_rail_payout_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 ROUTEApi(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.PayoutsAuditEntriesList(action: "POST /v1/payouts/instructions", actorId: "usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", resource: "instructions", resourceId: "route_multi_rail_payout_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/payouts/audit_entries?action=POST%20%2Fv1%2Fpayouts%2Finstructions&actor_id=usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=instructions&resource_id=route_multi_rail_payout_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.ROUTEAPI.PayoutsAuditEntriesList(ctx).Action("POST /v1/payouts/instructions").ActorId("usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Resource("instructions").ResourceId("route_multi_rail_payout_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/payouts/audit_entries?action=POST%20%2Fv1%2Fpayouts%2Finstructions&actor_id=usr_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&resource=instructions&resource_id=route_multi_rail_payout_01J8XQ4M7K2N9P3R5T7V9W1Y3Z&outcome=succeeded&recorded_after=2026-01-01T00%3A00%3A00Z&recorded_before=2027-01-01T00%3A00%3A00Z&limit=25", nil)
	req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "list",
  "data": [
    {
      "id": "audit_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "audit_entry",
      "livemode": false,
      "mocked": true,
      "at": "2026-09-01T09:00:00Z",
      "request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "actor_type": "example",
      "actor_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "action": "example",
      "outcome": "succeeded",
      "status": 1,
      "resource": "example",
      "resource_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
    }
  ],
  "has_more": true
}
GET/v1/payouts/audit_entries/{audit_entry_id}#

Tie one attempt to its request

payouts.audit_entries.retrieve

An audit entry is one attempt to change something, or one that was refused, and this reads one back. It names the request that made the attempt, so a refusal your integration recorded and the row we hold for it can be matched to each other.

Put the entry's id in the path. One entry comes back: the moment it happened, who was behind it, the route they aimed at, and the status they were answered with.

An entry stops at the attempt. It does not carry what changed, and it does not carry the reason behind a refusal; for what happened to the instruction itself, read GET /v1/payouts/events.

Where resource_id is set, open that record, such as an instruction at GET /v1/payouts/instructions/{instruction_id}. An audit_entry_id nothing holds answers not_found.

Path parameters

audit_entry_id string required

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

Returns

One attempt in full, with the request behind it and the status it was answered with.

id string required

The entry's identifier, as GET /v1/payouts/audit_entries lists it. It starts with audit_entry_ and never changes; pass it as audit_entry_id to GET /v1/payouts/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 request and a replayed idempotent request both count as attempts against a mocked route, and the log says so.

at string · date-time required

When the attempt was made, as an RFC 3339 timestamp in UTC. Filter on it with recorded_after and recorded_before when you list.

request_id string required

The identifier of the request that made the attempt. It's the same request_id an error body carries, so you can match a refusal here to the response you were given.

actor_type string required

What kind of caller made the attempt: 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 we acted for you.

actor_id string required

Who made the attempt, as the id of what actor_type names: the API key's (key_…) or client's for client, the person's (usr_…) for user. Pass it as actor_id to GET /v1/payouts/audit_entries to follow one actor.

action string required

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

resource string · nullable optional

The kind of record the attempt was aimed at, as the collection in the route: instructions, payouts, settlements and so on. Null when the route names none.

resource_id string · nullable optional

The id of the one record the route named, such as an instruction's (route_multi_rail_payout_…, from POST /v1/payouts/instructions). null for an attempt on a collection, such as creating an instruction.

outcome string required

How the attempt ended: succeeded means it was carried out, refused means you were turned away with a 4xx, failed means we answered with a 5xx. status carries the exact code.

succeededrefusedfailed
status integer required

The HTTP status the caller was given, such as 201 or 403. Read it with outcome to tell one refusal from another.

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/payouts/audit_entries/%7Baudit_entry_id%7D" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY"
import { Configuration, ROUTEApi } from '@droomwork/sdk';

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

const result = await api.payoutsAuditEntriesRetrieve({ auditEntryId: '{audit_entry_id}' });
const response = await fetch('https://sandbox.droomwork.io/v1/payouts/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.ROUTEApi(client)

result = api.payouts_audit_entries_retrieve(audit_entry_id='{audit_entry_id}')
import os

import requests

response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/payouts/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\ROUTEApi(new GuzzleHttp\Client(), $config);

$result = $api->payoutsAuditEntriesRetrieve(audit_entry_id: '{audit_entry_id}');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/payouts/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.RouteApi;

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

var result = api.payoutsAuditEntriesRetrieve("{audit_entry_id}");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/payouts/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 ROUTEApi(config);

var result = api.PayoutsAuditEntriesRetrieve(auditEntryId: "{audit_entry_id}");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/payouts/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.ROUTEAPI.PayoutsAuditEntriesRetrieve(ctx, "{audit_entry_id}").Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/payouts/audit_entries/%7Baudit_entry_id%7D", nil)
	req.Header.Set("Droomwork-Api-Key", os.Getenv("DROOMWORK_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "audit_entry_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "audit_entry",
  "livemode": false,
  "mocked": true,
  "at": "2026-09-01T09:00:00Z",
  "request_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "actor_type": "example",
  "actor_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "action": "example",
  "outcome": "succeeded",
  "status": 1,
  "resource": "example",
  "resource_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}