HHeros Developers

Version 1.0.0

Heros IAM

Registration, sessions, API keys and the token grant.

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

Every request you send carries a credential. This is where you get one.

What you should know before you start

You have two kinds of credential, for different jobs. An API key is long lived; send it from your server on every request. An access token is what you get by trading a client identifier and secret: it lasts fifteen minutes and carries only the scopes you asked for. Start with a key. Use tokens when you want to hand a narrower credential to part of your system; the client identifier and secret behind them are issued by Heros on request, and there is no refresh token, so you ask for another when it runs out.

Your credential says which organisation, with one exception. It comes from your credential on every endpoint, and you can't act for one you don't hold a credential for. The exception is a signed-in call working on a product your company owns, where organisation names which of them you mean; GET /v1/me lists the ones you may name, and anything else answers not_found.

Scopes are granular and we check them. run:read can't approve a payroll run: looking at money and moving it are different scopes. There are three actions for each of the eight modules, plus ledger:read, and delivery:read and delivery:write for your webhook endpoints. Ask for what you need and no more.

You see a secret once. An API key is returned when you create it and never again. No endpoint reads one back.

Sandbox keys are real keys. They authenticate for real against real records. What makes one a sandbox key is the realm it reaches, and livemode tells you which.

Getting started

Register. One call gives you an organisation, an account, a session and a first sandbox key. Send the key as Droomwork-Api-Key on any sandbox request. When you want short-lived credentials, ask Heros for a client identifier and secret and exchange them at the token grant.

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?

POST/v1/registrations#

Register an organisation

iam.registrations.create

This registers your company and the first person in it. It hands back both halves of an integration: a key your server authenticates with, and a session that signs that person into your screens with no second sign-in.

Send organisation_name, name, email and password. Copy api_key.key from the 201 first, then send it as Droomwork-Api-Key, session.token as Droomwork-Session.

Don't retry a timed-out call. The second answers conflict and the secret is gone. Recover with POST /v1/sessions, then POST /v1/account/api_keys.

You send no credential here, and nothing has to exist first. If Heros created your account and sent you a password, call POST /v1/account/password instead.

Send a session and the organisation you create becomes a product of the one you signed in as, which is how a company keeps each product's records and keys apart. A product needs its own contact address and cannot register a product itself.

Body

organisation_name string required

The name of the organisation you're registering. It's what organisation.name shows wherever the organisation appears, so give the name you trade under.

name string required

The person registering.

email string · email required

The address the person registering will sign in with. One address belongs to one person: an address that already has an account is refused with conflict.

password string required

Long beats complicated. Refused with a reason you can show somebody, never a rule they have to guess.

Returns

The organisation, the account, a session and the first key.

object always "registration" required

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

livemode boolean required

False for a record in the sandbox realm, true for one in the live realm. This answers which data a record belongs to. mocked answers where its numbers came from, and the two are separate questions. A sandbox payslip computed by the real engine is livemode false and mocked false. A deferred capability in the live realm is livemode true and mocked true.

mocked boolean required

True when this response came from a mock, false when the engine computed the numbers. It stays true for a capability that is still on its documented mock, even in the live realm. It answers a different question from livemode: livemode says which realm you are in, this says where the numbers came from.

organisation RegisteredOrganisation required

The organisation a registration created, and the company that owns it.

3 fields of RegisteredOrganisation
id string required

The new organisation's identifier; it starts with org_ and never changes.

name string required

The organisation_name you sent.

owned_by string · nullable required

The company that owns this organisation, or null when it owns itself. It is null unless you sent a session, and it is the organisation that session belongs to when you did.

account AccountDetail required
2 fields of AccountDetail
email string · email required

The address this account signs in with. It's the email you send to POST /v1/sessions.

name string required

The name of the person this account belongs to, as it was given when the account was created.

session object required

You're signed in from this moment: a token to send as Droomwork-Session, and when it stops being accepted. No need to sign in again with what you just typed.

2 fields
token string required

Send it as the Droomwork-Session header on the endpoints a signed-in person uses, such as GET /v1/me. It acts as this account, so keep it to yourself.

expires_at string · date-time required

When this session stops being accepted, as an RFC 3339 timestamp in UTC. Sign in again at POST /v1/sessions after that.

api_key object required
10 fields
id string required

The key's identifier; it starts with key_ and never changes. Returned by POST /v1/api_keys, POST /v1/account/api_keys and POST /v1/registrations, listed by GET /v1/api_keys and GET /v1/me, and sent as {id} to revoke it.

object always "api_key" required

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

livemode boolean required

False for a record in the sandbox realm, true for one in the live realm. This answers which data a record belongs to. mocked answers where its numbers came from, and the two are separate questions. A sandbox payslip computed by the real engine is livemode false and mocked false. A deferred capability in the live realm is livemode true and mocked true.

mocked boolean required

True when this response came from a mock, false when the engine computed the numbers. It stays true for a capability that is still on its documented mock, even in the live realm. It answers a different question from livemode: livemode says which realm you are in, this says where the numbers came from.

name string required

What this key is for, as named when it was created. Use it to tell your keys apart in a list.

realm string required

Which realm this key reaches: test is the sandbox, where nothing moves, and live is real money and real filings. Sandbox keys are test and carry livemode false; live keys aren't self-serve.

testlive
scopes array of Scope required

What this key may do, as module:action scopes; a request needing one not listed here is refused. You can't issue the wildcard, so it never appears here.

revoked_at string · date-time · nullable optional

Always null on a key you have just been given, which is every response that carries this schema. A key stops authenticating when you revoke it at POST /v1/api_keys/{id}/revoke or POST /v1/account/api_keys/{id}/revoke, and GET /v1/api_keys and GET /v1/account/api_keys are where it then shows the timestamp.

created_at string · date-time required

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

key string required

Shown here and never again. Send it as the Droomwork-Api-Key header.

next string required

What to do with the key you were just given.

Other responses

400The request could not be read, or a value was refused.
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/registrations" \
  -H "Droomwork-Session: $DROOMWORK_SESSION" \
  -H "Content-Type: application/json" \
  -d '{"organisation_name":"Adaeze Foods Ltd","name":"Ada Obi","email":"ada@adaezefoods.example","password":"correct horse battery staple"}'
import { Configuration, IAMApi } from '@heros/sdk';

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

const result = await api.iamRegistrationsCreate({
  iamRegistrationRequest: {"organisationName":"Adaeze Foods Ltd","name":"Ada Obi","email":"ada@adaezefoods.example","password":"correct horse battery staple"},
});
const response = await fetch('https://sandbox.droomwork.io/v1/registrations', {
  method: 'POST',
  headers: {
    'Droomwork-Session': process.env.DROOMWORK_SESSION,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "organisation_name": "Adaeze Foods Ltd",
    "name": "Ada Obi",
    "email": "ada@adaezefoods.example",
    "password": "correct horse battery staple"
  }),
});
const result = await response.json();
import os

import heros_sdk

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

result = api.iam_registrations_create(body={"organisation_name": "Adaeze Foods Ltd", "name": "Ada Obi", "email": "ada@adaezefoods.example", "password": "correct horse battery staple"})
import os

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/registrations',
    headers={
        'Droomwork-Session': os.environ['DROOMWORK_SESSION'],
    },
    json={"organisation_name": "Adaeze Foods Ltd", "name": "Ada Obi", "email": "ada@adaezefoods.example", "password": "correct horse battery staple"},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->iamRegistrationsCreate(json_decode('{"organisation_name":"Adaeze Foods Ltd","name":"Ada Obi","email":"ada@adaezefoods.example","password":"correct horse battery staple"}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/registrations');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Session: ' . getenv('DROOMWORK_SESSION'),
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => '{"organisation_name":"Adaeze Foods Ltd","name":"Ada Obi","email":"ada@adaezefoods.example","password":"correct horse battery staple"}',
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.IamApi;

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

var result = api.iamRegistrationsCreate(body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/registrations"))
    .header("Droomwork-Session", System.getenv("DROOMWORK_SESSION"))
    .header("Content-Type", "application/json")
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "organisation_name": "Adaeze Foods Ltd",
          "name": "Ada Obi",
          "email": "ada@adaezefoods.example",
          "password": "correct horse battery staple"
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Heros.Sdk.Api;
using Heros.Sdk.Client;

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

var result = api.IamRegistrationsCreate(body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/registrations");
request.Headers.Add("Droomwork-Session", Environment.GetEnvironmentVariable("DROOMWORK_SESSION"));
request.Content = new StringContent("""
    {
      "organisation_name": "Adaeze Foods Ltd",
      "name": "Ada Obi",
      "email": "ada@adaezefoods.example",
      "password": "correct horse battery staple"
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

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

result, _, err := client.IAMAPI.IamRegistrationsCreate(ctx).IamRegistrationRequest(body).Execute()
body := strings.NewReader(`{
  "organisation_name": "Adaeze Foods Ltd",
  "name": "Ada Obi",
  "email": "ada@adaezefoods.example",
  "password": "correct horse battery staple"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/registrations", body)
	req.Header.Set("Droomwork-Session", os.Getenv("DROOMWORK_SESSION"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "registration",
  "livemode": false,
  "mocked": false,
  "organisation": {
    "id": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
    "name": "Ada Obi",
    "owned_by": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
  },
  "account": {
    "email": "ada@adaezefoods.example",
    "name": "Ada Obi"
  },
  "session": {
    "token": "example",
    "expires_at": "2026-09-01T09:00:00Z"
  },
  "api_key": {
    "id": "key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
    "object": "api_key",
    "livemode": false,
    "mocked": false,
    "name": "Ada Obi",
    "realm": "test",
    "scopes": [
      "run:read"
    ],
    "created_at": "2026-09-01T09:00:00Z",
    "revoked_at": null,
    "key": "dw_test_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
  },
  "next": "example"
}
POST/v1/sessions#

Sign in and get a session

iam.sessions.create

It signs one person in. It's what lets your front end act for that person: you take a password and hold a token, so no password sits in a browser and your API key stays on your server.

Send the account's email and password. Keep token for the Droomwork-Session header and expires_at for when to sign them in again; the organisation's own id comes back too, starting org_.

A password_reset_required refusal isn't a wrong password. The account still holds the password we sent, so send the person to POST /v1/account/password.

You send no credential here. Use it when you hold no session: after expires_at passes, after POST /v1/sessions/revoke, or on a new device. POST /v1/registrations and POST /v1/account/password each hand you a session, so you don't need it there. After this, GET /v1/me and POST /v1/account/api_keys.

Body

email string · email required

The address the account signs in with. An address that was never registered is refused the same way as a wrong password.

password string required

The account's own password. If it's still the one Heros sent, you get password_reset_required: replace it at POST /v1/account/password instead.

Returns

One session, with the signed-in person's name and their organisation, so your first screen needs no second call.

object always "session" required

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

token string required

Send it as the Droomwork-Session header on the endpoints a signed-in person uses, such as GET /v1/me. It isn't an API key, and nothing outside those endpoints accepts it.

expires_at string · date-time required

When this session stops being accepted, as an RFC 3339 timestamp in UTC. Sign in again after that.

account AccountDetail required
2 fields of AccountDetail
email string · email required

The address this account signs in with. It's the email you send to POST /v1/sessions.

name string required

The name of the person this account belongs to, as it was given when the account was created.

organisation OrganisationDetail required
2 fields of OrganisationDetail
id string required

Your organisation's identifier; it starts with org_ and never changes. It reads the same wherever organisation appears, from POST /v1/registrations to GET /v1/me. Your key names your organisation on every request, so you never send this with a key. The one place you may send it is a signed-in call that works on one of several organisations you own, such as GET /v1/account/api_keys, and there you can only name one GET /v1/me already lists.

name string required

The organisation's name, as it was given when the organisation was registered.

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/sessions" \
  -H "Content-Type: application/json" \
  -d '{"email":"ada@adaezefoods.example","password":"correct horse battery staple"}'
import { Configuration, IAMApi } from '@heros/sdk';

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

const result = await api.iamSessionsCreate({
  iamSignInRequest: {"email":"ada@adaezefoods.example","password":"correct horse battery staple"},
});
const response = await fetch('https://sandbox.droomwork.io/v1/sessions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "email": "ada@adaezefoods.example",
    "password": "correct horse battery staple"
  }),
});
const result = await response.json();
import os

import heros_sdk

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

result = api.iam_sessions_create(body={"email": "ada@adaezefoods.example", "password": "correct horse battery staple"})
import os

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/sessions',
    headers={
    },
    json={"email": "ada@adaezefoods.example", "password": "correct horse battery staple"},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->iamSessionsCreate(json_decode('{"email":"ada@adaezefoods.example","password":"correct horse battery staple"}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/sessions');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => '{"email":"ada@adaezefoods.example","password":"correct horse battery staple"}',
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.IamApi;

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

var result = api.iamSessionsCreate(body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/sessions"))
    .header("Content-Type", "application/json")
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "email": "ada@adaezefoods.example",
          "password": "correct horse battery staple"
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Heros.Sdk.Api;
using Heros.Sdk.Client;

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

var result = api.IamSessionsCreate(body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/sessions");
request.Content = new StringContent("""
    {
      "email": "ada@adaezefoods.example",
      "password": "correct horse battery staple"
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

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

result, _, err := client.IAMAPI.IamSessionsCreate(ctx).IamSignInRequest(body).Execute()
body := strings.NewReader(`{
  "email": "ada@adaezefoods.example",
  "password": "correct horse battery staple"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/sessions", body)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "session",
  "token": "example",
  "expires_at": "2026-09-01T09:00:00Z",
  "account": {
    "email": "ada@adaezefoods.example",
    "name": "Ada Obi"
  },
  "organisation": {
    "id": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
    "name": "Ada Obi"
  }
}
POST/v1/sessions/revoke#

Sign a person out

iam.sessions.revoke

This ends one sign-in session. It gives your app a Sign out button that ends the session right away: with no call here the token works until expires_at passes, and someone at a shared computer stays signed in.

Send no request body. The session token goes in the Droomwork-Session header, not an API key. Drop the token you held and show your sign-in screen.

This ends only the session you called with. Only POST /v1/account/password ends every session an account holds, and it replaces the password as well. Any API key you issued keeps working.

Call POST /v1/registrations, POST /v1/account/password or POST /v1/sessions first; any of the three gives you the session. After this, signing in again means POST /v1/sessions.

Returns

The session is ended.

object always "session" required

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

revoked always true required

Always true when this answers; a session that could not be ended is refused instead.

Other responses

401No valid credential was presented.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/sessions/revoke" \
  -H "Droomwork-Session: $DROOMWORK_SESSION"
import { Configuration, IAMApi } from '@heros/sdk';

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

const result = await api.iamSessionsRevoke({});
const response = await fetch('https://sandbox.droomwork.io/v1/sessions/revoke', {
  method: 'POST',
  headers: {
    'Droomwork-Session': process.env.DROOMWORK_SESSION,
  },
});
const result = await response.json();
import os

import heros_sdk

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

result = api.iam_sessions_revoke()
import os

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/sessions/revoke',
    headers={
        'Droomwork-Session': os.environ['DROOMWORK_SESSION'],
    },
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->iamSessionsRevoke();
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/sessions/revoke');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Session: ' . getenv('DROOMWORK_SESSION'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.IamApi;

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

var result = api.iamSessionsRevoke();
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/sessions/revoke"))
    .header("Droomwork-Session", System.getenv("DROOMWORK_SESSION"))
    .method("POST", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Heros.Sdk.Api;
using Heros.Sdk.Client;

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

var result = api.IamSessionsRevoke();
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/sessions/revoke");
request.Headers.Add("Droomwork-Session", Environment.GetEnvironmentVariable("DROOMWORK_SESSION"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

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

result, _, err := client.IAMAPI.IamSessionsRevoke(ctx).Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/sessions/revoke", nil)
	req.Header.Set("Droomwork-Session", os.Getenv("DROOMWORK_SESSION"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "session",
  "revoked": true
}
POST/v1/oauth/token#

Trade client credentials for a short-lived token

iam.oauth.token

This gets you a credential to hand to something that should not hold your API key: a build script, a reporting service, a job you don't fully trust. What you hand over expires by itself, so you never pass your API key around and you never have to revoke what you handed out.

Send grant_type as client_credentials with your client_id and client_secret, and access_token comes back. Ask for scopes the client lacks and you get forbidden.

You send no credential here, and the client in the body is the one thing that must already exist. After this you can call POST /v1/api_keys and GET /v1/api_keys, and the rest of IAM a token reaches.

Body

grant_type always "client_credentials" required

Always client_credentials, the one grant this endpoint supports. Anything else, or leaving it out, is refused with invalid_request.

client_id string required

The identifier of a client Heros issued you on request, sent with its client_secret; no endpoint creates or lists clients, and the sandbox holds none until one is issued. An unknown identifier and a wrong secret are refused identically, so the response can't tell you which clients exist.

client_secret string required

The secret issued with client_id. Keep it on your server; a wrong secret is refused the same way as an unknown client.

scopes array of Scope optional

A subset of what the client holds. Omitted means all of them.

Returns

The token.

object always "token" required

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

access_token string required

The bearer token. Send it as Authorization: Bearer <token> on every request until expires_in runs out, then ask for another with the same client identifier and secret; there is no refresh token.

token_type always "Bearer" required

Always Bearer. Send access_token in the Authorization header with that prefix.

expires_in integer required

Seconds. Fifteen minutes.

scopes array of Scope required

What this token may do, as module:action scopes. Exactly what you asked for, or everything the client holds if you asked for none.

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/oauth/token" \
  -H "Content-Type: application/json" \
  -d '{"grant_type":"client_credentials","client_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","client_secret":"example","scopes":["run:read"]}'
import { Configuration, IAMApi } from '@heros/sdk';

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

const result = await api.iamOauthToken({
  iamTokenRequest: {"grantType":"client_credentials","clientId":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","clientSecret":"example","scopes":["run:read"]},
});
const response = await fetch('https://sandbox.droomwork.io/v1/oauth/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "grant_type": "client_credentials",
    "client_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
    "client_secret": "example",
    "scopes": [
      "run:read"
    ]
  }),
});
const result = await response.json();
import os

import heros_sdk

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

result = api.iam_oauth_token(body={"grant_type": "client_credentials", "client_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "client_secret": "example", "scopes": ["run:read"]})
import os

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/oauth/token',
    headers={
    },
    json={"grant_type": "client_credentials", "client_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", "client_secret": "example", "scopes": ["run:read"]},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->iamOauthToken(json_decode('{"grant_type":"client_credentials","client_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","client_secret":"example","scopes":["run:read"]}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/oauth/token');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => '{"grant_type":"client_credentials","client_id":"sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z","client_secret":"example","scopes":["run:read"]}',
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.IamApi;

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

var result = api.iamOauthToken(body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/oauth/token"))
    .header("Content-Type", "application/json")
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "grant_type": "client_credentials",
          "client_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
          "client_secret": "example",
          "scopes": [
            "run:read"
          ]
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Heros.Sdk.Api;
using Heros.Sdk.Client;

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

var result = api.IamOauthToken(body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/oauth/token");
request.Content = new StringContent("""
    {
      "grant_type": "client_credentials",
      "client_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "client_secret": "example",
      "scopes": [
        "run:read"
      ]
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

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

result, _, err := client.IAMAPI.IamOauthToken(ctx).IamTokenRequest(body).Execute()
body := strings.NewReader(`{
  "grant_type": "client_credentials",
  "client_id": "sub_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "client_secret": "example",
  "scopes": [
    "run:read"
  ]
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/oauth/token", body)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "token",
  "access_token": "example",
  "token_type": "Bearer",
  "expires_in": 1,
  "scopes": [
    "run:read"
  ]
}
GET/v1/api_keys#

See every key your organisation holds

iam.api_keys.list

This lists every API key your organisation holds: what each is called, what it may do in scopes, and whether it has been stopped. It's the only list of your keys a server can read on its own, with no person signed in.

You get one record for each key in data, newest first, and no secret among them: a key's secret comes back once, from POST /v1/api_keys. Keep each id for POST /v1/api_keys/{id}/revoke.

Check revoked_at on every row: a stopped key still looks like a working one until you do.

Call POST /v1/registrations, POST /v1/oauth/token or POST /v1/account/api_keys first; any of them gives you the credential this call needs. Nothing else waits on this call.

Returns

The key records, each with a created_at, so you can find the ones older than your rotation window.

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

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

9 fields of ApiKey
id string required

The key's identifier; it starts with key_ and never changes. Returned by POST /v1/api_keys, POST /v1/account/api_keys and POST /v1/registrations, listed by GET /v1/api_keys and GET /v1/me, and sent as {id} to revoke it.

object always "api_key" required

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

livemode boolean required

False for a record in the sandbox realm, true for one in the live realm. This answers which data a record belongs to. mocked answers where its numbers came from, and the two are separate questions. A sandbox payslip computed by the real engine is livemode false and mocked false. A deferred capability in the live realm is livemode true and mocked true.

mocked boolean required

True when this response came from a mock, false when the engine computed the numbers. It stays true for a capability that is still on its documented mock, even in the live realm. It answers a different question from livemode: livemode says which realm you are in, this says where the numbers came from.

name string required

What this key is for, as named when it was created. Use it to tell your keys apart in a list.

realm string required

Which realm this key reaches: test is the sandbox, where nothing moves, and live is real money and real filings. Sandbox keys are test and carry livemode false; live keys aren't self-serve.

testlive
scopes array of Scope required

What this key may do, as module:action scopes; a request needing one not listed here is refused. You can't issue the wildcard, so it never appears here.

revoked_at string · date-time · nullable optional

When this key stopped authenticating, as an RFC 3339 timestamp in UTC, or null while it still works. A revoked key stays listed with this set, so its history stays with it.

created_at string · date-time required

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.

organisation string optional

Which organisation these keys belong to. GET /v1/account/api_keys sets it, so a page you fetched for one of your organisations can be told from a page you fetched for another without remembering which request produced which. GET /v1/api_keys leaves it out: that call answers for the organisation on your key and there is only ever one.

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

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

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

import heros_sdk

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

result = api.iam_api_keys_list()
import os

import requests

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

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

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

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

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

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

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

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

result, _, err := client.IAMAPI.IamApiKeysList(ctx).Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/api_keys", 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": "key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "api_key",
      "livemode": false,
      "mocked": false,
      "name": "Ada Obi",
      "realm": "test",
      "scopes": [
        "run:read"
      ],
      "created_at": "2026-09-01T09:00:00Z",
      "revoked_at": "2026-09-01T09:00:00Z"
    }
  ],
  "has_more": true,
  "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
POST/v1/api_keys#

Give part of your system its own key

iam.api_keys.create

This gives one part of your system its own API key, holding only the powers you decide. When a key leaks you revoke that one, and the rest keep working.

Send name and scopes, and copy key from the 201. The id beside it is what you send to revoke this key later.

Leave scopes out or empty and the key holds run:read only. org_id or organisation_id in the body gets forbidden.

Call POST /v1/registrations, POST /v1/oauth/token or POST /v1/account/api_keys first; any of them gives you the credential this call needs. After this you can call POST /v1/api_keys/{id}/revoke with the id that came back, and the rest of IAM your credential reaches.

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

name string optional

What this key is for, so a list of them can be read later.

scopes array of Scope optional

The scopes the key should carry, each module:action; name only what it needs. A value that isn't a scope refuses the whole request, and you can't issue the wildcard.

organisation string optional

Which of your organisations the key is for, when you hold more than one. Leave it out and the key is for the one you signed in as. Only POST /v1/account/api_keys reads this, because only a session knows a person; POST /v1/api_keys takes its organisation from your key and refuses to be pointed anywhere else. Name an organisation you don't own and you get not_found, the same answer as an organisation that doesn't exist. GET /v1/me lists the ones you can name.

Returns

The key record, created_at included, so you can store when it was issued without listing your keys.

id string required

The key's identifier; it starts with key_ and never changes. Returned by POST /v1/api_keys, POST /v1/account/api_keys and POST /v1/registrations, listed by GET /v1/api_keys and GET /v1/me, and sent as {id} to revoke it.

object always "api_key" required

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

livemode boolean required

False for a record in the sandbox realm, true for one in the live realm. This answers which data a record belongs to. mocked answers where its numbers came from, and the two are separate questions. A sandbox payslip computed by the real engine is livemode false and mocked false. A deferred capability in the live realm is livemode true and mocked true.

mocked boolean required

True when this response came from a mock, false when the engine computed the numbers. It stays true for a capability that is still on its documented mock, even in the live realm. It answers a different question from livemode: livemode says which realm you are in, this says where the numbers came from.

name string required

What this key is for, as named when it was created. Use it to tell your keys apart in a list.

realm string required

Which realm this key reaches: test is the sandbox, where nothing moves, and live is real money and real filings. Sandbox keys are test and carry livemode false; live keys aren't self-serve.

testlive
scopes array of Scope required

What this key may do, as module:action scopes; a request needing one not listed here is refused. You can't issue the wildcard, so it never appears here.

revoked_at string · date-time · nullable optional

Always null on a key you have just been given, which is every response that carries this schema. A key stops authenticating when you revoke it at POST /v1/api_keys/{id}/revoke or POST /v1/account/api_keys/{id}/revoke, and GET /v1/api_keys and GET /v1/account/api_keys are where it then shows the timestamp.

created_at string · date-time required

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

key string required

Shown here and never again. Send it as the Droomwork-Api-Key header.

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/api_keys" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"name":"CI","scopes":["run:read"],"organisation":"org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}'
import { Configuration, IAMApi } from '@heros/sdk';

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

const result = await api.iamApiKeysCreate({
  idempotencyKey: crypto.randomUUID(),
  iamApiKeyCreateRequest: {"name":"CI","scopes":["run:read"],"organisation":"org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"},
});
const response = await fetch('https://sandbox.droomwork.io/v1/api_keys', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    "name": "CI",
    "scopes": [
      "run:read"
    ],
    "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
  }),
});
const result = await response.json();
import os

import heros_sdk

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

result = api.iam_api_keys_create(body={"name": "CI", "scopes": ["run:read"], "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"})
import os
import uuid

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/api_keys',
    headers={
        'Droomwork-Api-Key': os.environ['DROOMWORK_API_KEY'],
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={"name": "CI", "scopes": ["run:read"], "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->iamApiKeysCreate($idempotencyKey, json_decode('{"name":"CI","scopes":["run:read"],"organisation":"org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/api_keys');
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 => '{"name":"CI","scopes":["run:read"],"organisation":"org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}',
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.IamApi;

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

var result = api.iamApiKeysCreate(idempotencyKey, body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/api_keys"))
    .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("""
        {
          "name": "CI",
          "scopes": [
            "run:read"
          ],
          "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Heros.Sdk.Api;
using Heros.Sdk.Client;

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

var result = api.IamApiKeysCreate(idempotencyKey, body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/api_keys");
request.Headers.Add("Droomwork-Api-Key", Environment.GetEnvironmentVariable("DROOMWORK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
    {
      "name": "CI",
      "scopes": [
        "run:read"
      ],
      "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

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

result, _, err := client.IAMAPI.IamApiKeysCreate(ctx).IdempotencyKey(key).IamApiKeyCreateRequest(body).Execute()
body := strings.NewReader(`{
  "name": "CI",
  "scopes": [
    "run:read"
  ],
  "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/api_keys", 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": "key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "api_key",
  "livemode": false,
  "mocked": false,
  "name": "Ada Obi",
  "realm": "test",
  "scopes": [
    "run:read"
  ],
  "created_at": "2026-09-01T09:00:00Z",
  "revoked_at": null,
  "key": "dw_test_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
POST/v1/api_keys/{id}/revoke#

Stop a key from working

iam.api_keys.revoke

This stops one API key working, straight away. An API key lasts until you end it, and your own code can end it with no person signed in.

Send the key's id in the path, not the secret you stored. Send no request body. The one field to keep is revoked_at.

Revoking the same key twice answers not_found, so retry a timed-out call with its original Idempotency-Key.

With a session, POST /v1/account/api_keys/{id}/revoke does the same and takes no Idempotency-Key.

Call POST /v1/registrations, POST /v1/api_keys or POST /v1/account/api_keys first; any of the three issues a key and hands you the id you send here. Nothing else waits on this call.

Path parameters

id string required

The key's id, which starts with key_, as returned by POST /v1/api_keys or POST /v1/account/api_keys when you made it and listed by GET /v1/api_keys and GET /v1/me. A key belonging to another organisation answers not_found.

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 key's whole record, not just a confirmation, so your own keys list can replace that row.

id string required

The key's identifier; it starts with key_ and never changes. Returned by POST /v1/api_keys, POST /v1/account/api_keys and POST /v1/registrations, listed by GET /v1/api_keys and GET /v1/me, and sent as {id} to revoke it.

object always "api_key" required

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

livemode boolean required

False for a record in the sandbox realm, true for one in the live realm. This answers which data a record belongs to. mocked answers where its numbers came from, and the two are separate questions. A sandbox payslip computed by the real engine is livemode false and mocked false. A deferred capability in the live realm is livemode true and mocked true.

mocked boolean required

True when this response came from a mock, false when the engine computed the numbers. It stays true for a capability that is still on its documented mock, even in the live realm. It answers a different question from livemode: livemode says which realm you are in, this says where the numbers came from.

name string required

What this key is for, as named when it was created. Use it to tell your keys apart in a list.

realm string required

Which realm this key reaches: test is the sandbox, where nothing moves, and live is real money and real filings. Sandbox keys are test and carry livemode false; live keys aren't self-serve.

testlive
scopes array of Scope required

What this key may do, as module:action scopes; a request needing one not listed here is refused. You can't issue the wildcard, so it never appears here.

revoked_at string · date-time · nullable optional

When this key stopped authenticating, as an RFC 3339 timestamp in UTC, or null while it still works. A revoked key stays listed with this set, so its history stays with it.

created_at string · date-time required

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

Other responses

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

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/api_keys/key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/revoke" \
  -H "Droomwork-Api-Key: $DROOMWORK_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
import { Configuration, IAMApi } from '@heros/sdk';

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

const result = await api.iamApiKeysRevoke({ id: 'key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/api_keys/key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/revoke', {
  method: 'POST',
  headers: {
    'Droomwork-Api-Key': process.env.DROOMWORK_API_KEY,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const result = await response.json();
import os

import heros_sdk

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

result = api.iam_api_keys_revoke(id='key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os
import uuid

import requests

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

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

$result = $api->iamApiKeysRevoke(id: 'key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/api_keys/key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/revoke');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Api-Key: ' . getenv('DROOMWORK_API_KEY'),
    'Idempotency-Key: ' . bin2hex(random_bytes(16)),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.IamApi;

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

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

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

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

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

result, _, err := client.IAMAPI.IamApiKeysRevoke(ctx, "key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/api_keys/key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/revoke", 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": "key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "api_key",
  "livemode": false,
  "mocked": false,
  "name": "Ada Obi",
  "realm": "test",
  "scopes": [
    "run:read"
  ],
  "created_at": "2026-09-01T09:00:00Z",
  "revoked_at": "2026-09-01T09:00:00Z"
}
GET/v1/support_access#

When Heros looked at your data

iam.support_access.list

You see every time a Heros staff member read your organisation's data, newest first. It lets you line up every look against your own audit trail by at, and show what each one was for.

You get one row for each look, and each row's id starts with sta_. No row names who looked; keep that id to quote back to us.

Raise limit to see further back. Every read is written before it's answered, so an empty list means nobody looked.

Call POST /v1/registrations, POST /v1/oauth/token or POST /v1/account/api_keys first; any of them gives you the credential this call needs. Nothing else waits on this call.

Query parameters

limit integer optional

How many records to return, from 1 to 200, newest first. Leave it out to get 50.

Returns

When Heros looked, and what for.

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

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

5 fields of SupportAccess
id string required

This look's identifier; it starts with sta_ and is returned only by GET /v1/support_access. The record never names who looked, so if you need to take something further, quote this id to us and we can identify the person internally.

object always "support_access" required

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

at string · date-time required

When the read happened, as an RFC 3339 timestamp in UTC. Line it up with the entry marked actor_type: staff in your own audit trail.

action string required

What was read, as the method and the path. The same shape your audit trail uses, so you can line the two up.

purpose one of optional

Null for a look recorded before purposes were kept.

SupportAccessPurposeor
has_more boolean required

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

Other responses

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

Errors it can return

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

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

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

import heros_sdk

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

# query parameters: limit (optional)
result = api.iam_support_access_list(limit=25)
import os

import requests

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

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

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

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

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

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

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

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

// query parameters: limit (optional)
result, _, err := client.IAMAPI.IamSupportAccessList(ctx).Limit(25).Execute()
// query parameters: limit (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/support_access?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": "sta_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "support_access",
      "at": "2026-09-01T09:00:00Z",
      "action": "GET /v1/payroll/runs",
      "purpose": "support_ticket"
    }
  ],
  "has_more": true
}
GET/v1/me#

Who this credential belongs to

iam.me.retrieve

This answers who a session token belongs to: the account that signed in, the organisation it belongs to, that organisation's API keys, and every organisation this sign-in can work on. So your account screen draws from this one response.

Send the session token in the Droomwork-Session header. An API key, or an expired or revoked session, gets unauthenticated. Each api_keys[].id is the {id} for revoking that key.

Read organisations if you own more than one. Those id values are the only ones a signed-in call will accept as organisation, so name anything else and you get not_found.

api_keys holds the keys of the organisation you signed in as, not one person's. For a product's keys, call GET /v1/account/api_keys and name it.

Call POST /v1/registrations, POST /v1/account/password or POST /v1/sessions first for the session.

Returns

The account, and a created_at on each entry in organisations, so you can show a product's registration date.

object always "account" required

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

account AccountDetail required
2 fields of AccountDetail
email string · email required

The address this account signs in with. It's the email you send to POST /v1/sessions.

name string required

The name of the person this account belongs to, as it was given when the account was created.

organisation OrganisationDetail required
2 fields of OrganisationDetail
id string required

Your organisation's identifier; it starts with org_ and never changes. It reads the same wherever organisation appears, from POST /v1/registrations to GET /v1/me. Your key names your organisation on every request, so you never send this with a key. The one place you may send it is a signed-in call that works on one of several organisations you own, such as GET /v1/account/api_keys, and there you can only name one GET /v1/me already lists.

name string required

The organisation's name, as it was given when the organisation was registered.

api_keys array of ApiKey required

Every API key the organisation holds, without their secrets; revoked keys stay listed with revoked_at set. No endpoint returns a secret again. These are the keys of the organisation in organisation only. For a product's keys, call GET /v1/account/api_keys and name it.

9 fields of ApiKey
id string required

The key's identifier; it starts with key_ and never changes. Returned by POST /v1/api_keys, POST /v1/account/api_keys and POST /v1/registrations, listed by GET /v1/api_keys and GET /v1/me, and sent as {id} to revoke it.

object always "api_key" required

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

livemode boolean required

False for a record in the sandbox realm, true for one in the live realm. This answers which data a record belongs to. mocked answers where its numbers came from, and the two are separate questions. A sandbox payslip computed by the real engine is livemode false and mocked false. A deferred capability in the live realm is livemode true and mocked true.

mocked boolean required

True when this response came from a mock, false when the engine computed the numbers. It stays true for a capability that is still on its documented mock, even in the live realm. It answers a different question from livemode: livemode says which realm you are in, this says where the numbers came from.

name string required

What this key is for, as named when it was created. Use it to tell your keys apart in a list.

realm string required

Which realm this key reaches: test is the sandbox, where nothing moves, and live is real money and real filings. Sandbox keys are test and carry livemode false; live keys aren't self-serve.

testlive
scopes array of Scope required

What this key may do, as module:action scopes; a request needing one not listed here is refused. You can't issue the wildcard, so it never appears here.

revoked_at string · date-time · nullable optional

When this key stopped authenticating, as an RFC 3339 timestamp in UTC, or null while it still works. A revoked key stays listed with this set, so its history stays with it.

created_at string · date-time required

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

organisations array of ReachableOrganisation required

Every organisation this sign-in can work on: the company in organisation first, then each product it owns, oldest first. Always at least one entry, so you have one list to read through and no special case for the company itself. Signing in to a product gives you one entry, that product, because a product never reaches the company above it or a product beside it. Pass any id here as organisation on a signed-in call.

6 fields of ReachableOrganisation
id string required

The organisation's identifier. Send it as organisation on a signed-in call to work on this one instead of the company you signed in as.

object always "organisation" required

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

name string required

The name this organisation was registered with.

realm string required

Always test while the sandbox is what we serve. A product is always in the same realm as the company that owns it, so this is never a mixture.

owned_by string · nullable required

The company that owns this organisation, or null when it owns itself. The entry you signed in as is the one with null, and every other entry names it here.

created_at string · date-time required

When this organisation was registered, as an RFC 3339 timestamp in UTC.

Other responses

401No valid credential was presented.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X GET "https://sandbox.droomwork.io/v1/me" \
  -H "Droomwork-Session: $DROOMWORK_SESSION"
import { Configuration, IAMApi } from '@heros/sdk';

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

const result = await api.iamMeRetrieve({});
const response = await fetch('https://sandbox.droomwork.io/v1/me', {
  method: 'GET',
  headers: {
    'Droomwork-Session': process.env.DROOMWORK_SESSION,
  },
});
const result = await response.json();
import os

import heros_sdk

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

result = api.iam_me_retrieve()
import os

import requests

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

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

$result = $api->iamMeRetrieve();
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/me');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Session: ' . getenv('DROOMWORK_SESSION'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.IamApi;

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

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

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

var result = api.IamMeRetrieve();
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/me");
request.Headers.Add("Droomwork-Session", Environment.GetEnvironmentVariable("DROOMWORK_SESSION"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

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

result, _, err := client.IAMAPI.IamMeRetrieve(ctx).Execute()
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/me", nil)
	req.Header.Set("Droomwork-Session", os.Getenv("DROOMWORK_SESSION"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "account",
  "account": {
    "email": "ada@adaezefoods.example",
    "name": "Ada Obi"
  },
  "organisation": {
    "id": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
    "name": "Ada Obi"
  },
  "api_keys": [
    {
      "id": "key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "api_key",
      "livemode": false,
      "mocked": false,
      "name": "Ada Obi",
      "realm": "test",
      "scopes": [
        "run:read"
      ],
      "created_at": "2026-09-01T09:00:00Z",
      "revoked_at": "2026-09-01T09:00:00Z"
    }
  ],
  "organisations": [
    {
      "id": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "organisation",
      "name": "Ada Obi",
      "realm": "example",
      "owned_by": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "created_at": "2026-09-01T09:00:00Z"
    }
  ]
}
POST/v1/account/password#

Replace the password you were sent

iam.account.password.replace

It swaps the password we sent for one only you know, and it signs you in too. So your setup code can finish the start-up alone: set the real password, then take the account's first API key, with no sign-in call between.

Send email, current_password and new_password. Keep token as Droomwork-Session. Reusing the sent password is invalid_request, an expired one unauthenticated. All sessions end.

You send no credential here. You do need an account we created for you and the password we sent with it, which won't sign you in. After this you can call POST /v1/account/api_keys and GET /v1/me. GET /v1/support_access isn't one of them, so make a key first.

Body

email string · email required

The address of the account whose password you're replacing: the one the starting password was sent for.

current_password string required

The password Heros sent you.

new_password string required

One only you know. Twelve characters at least.

Returns

The password is yours, and this is the session for it.

object always "session" required

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

token string required

Send it as the Droomwork-Session header on the endpoints a signed-in person uses, such as GET /v1/me. It isn't an API key, and nothing outside those endpoints accepts it.

expires_at string · date-time required

When this session stops being accepted, as an RFC 3339 timestamp in UTC. Sign in again after that.

account AccountDetail required
2 fields of AccountDetail
email string · email required

The address this account signs in with. It's the email you send to POST /v1/sessions.

name string required

The name of the person this account belongs to, as it was given when the account was created.

organisation OrganisationDetail required
2 fields of OrganisationDetail
id string required

Your organisation's identifier; it starts with org_ and never changes. It reads the same wherever organisation appears, from POST /v1/registrations to GET /v1/me. Your key names your organisation on every request, so you never send this with a key. The one place you may send it is a signed-in call that works on one of several organisations you own, such as GET /v1/account/api_keys, and there you can only name one GET /v1/me already lists.

name string required

The organisation's name, as it was given when the organisation was registered.

Other responses

400The request could not be read, or a value was refused.
401No valid credential was presented.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/account/password" \
  -H "Content-Type: application/json" \
  -d '{"email":"ada@adaezefoods.example","current_password":"example","new_password":"example correct"}'
import { Configuration, IAMApi } from '@heros/sdk';

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

const result = await api.iamAccountPasswordReplace({
  iamAccountPasswordRequest: {"email":"ada@adaezefoods.example","currentPassword":"example","newPassword":"example correct"},
});
const response = await fetch('https://sandbox.droomwork.io/v1/account/password', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "email": "ada@adaezefoods.example",
    "current_password": "example",
    "new_password": "example correct"
  }),
});
const result = await response.json();
import os

import heros_sdk

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

result = api.iam_account_password_replace(body={"email": "ada@adaezefoods.example", "current_password": "example", "new_password": "example correct"})
import os

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/account/password',
    headers={
    },
    json={"email": "ada@adaezefoods.example", "current_password": "example", "new_password": "example correct"},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->iamAccountPasswordReplace(json_decode('{"email":"ada@adaezefoods.example","current_password":"example","new_password":"example correct"}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/account/password');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => '{"email":"ada@adaezefoods.example","current_password":"example","new_password":"example correct"}',
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.IamApi;

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

var result = api.iamAccountPasswordReplace(body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/account/password"))
    .header("Content-Type", "application/json")
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "email": "ada@adaezefoods.example",
          "current_password": "example",
          "new_password": "example correct"
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Heros.Sdk.Api;
using Heros.Sdk.Client;

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

var result = api.IamAccountPasswordReplace(body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/account/password");
request.Content = new StringContent("""
    {
      "email": "ada@adaezefoods.example",
      "current_password": "example",
      "new_password": "example correct"
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

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

result, _, err := client.IAMAPI.IamAccountPasswordReplace(ctx).IamAccountPasswordRequest(body).Execute()
body := strings.NewReader(`{
  "email": "ada@adaezefoods.example",
  "current_password": "example",
  "new_password": "example correct"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/account/password", body)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "object": "session",
  "token": "example",
  "expires_at": "2026-09-01T09:00:00Z",
  "account": {
    "email": "ada@adaezefoods.example",
    "name": "Ada Obi"
  },
  "organisation": {
    "id": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
    "name": "Ada Obi"
  }
}
GET/v1/account/api_keys#

Your keys, without holding one

iam.account.api_keys.list

This lists the API keys of one of your organisations, from a signed in person's session. So the keys page in your own product can show what exists, which scopes each key carries and which have been revoked, without anybody holding a key to ask with.

Send your session token as Droomwork-Session, not an API key. Add organisation to list the keys of a product you own; leave it out for the organisation you signed in as.

GET /v1/api_keys lists the same records for whoever holds a key of that organisation. This one needs a person instead, which is the only way in for a product whose key you have lost.

GET /v1/me lists the organisations you can name here.

Query parameters

organisation string optional

Which of your organisations to list. Leave it out for the one you signed in as. Name an organisation you don't own and you get not_found, the same answer as one that doesn't exist, because an identifier you cannot reach is not an identifier you should be able to confirm.

Returns

The organisation's key records, each carrying a created_at, so you can show when a key was issued.

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

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

9 fields of ApiKey
id string required

The key's identifier; it starts with key_ and never changes. Returned by POST /v1/api_keys, POST /v1/account/api_keys and POST /v1/registrations, listed by GET /v1/api_keys and GET /v1/me, and sent as {id} to revoke it.

object always "api_key" required

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

livemode boolean required

False for a record in the sandbox realm, true for one in the live realm. This answers which data a record belongs to. mocked answers where its numbers came from, and the two are separate questions. A sandbox payslip computed by the real engine is livemode false and mocked false. A deferred capability in the live realm is livemode true and mocked true.

mocked boolean required

True when this response came from a mock, false when the engine computed the numbers. It stays true for a capability that is still on its documented mock, even in the live realm. It answers a different question from livemode: livemode says which realm you are in, this says where the numbers came from.

name string required

What this key is for, as named when it was created. Use it to tell your keys apart in a list.

realm string required

Which realm this key reaches: test is the sandbox, where nothing moves, and live is real money and real filings. Sandbox keys are test and carry livemode false; live keys aren't self-serve.

testlive
scopes array of Scope required

What this key may do, as module:action scopes; a request needing one not listed here is refused. You can't issue the wildcard, so it never appears here.

revoked_at string · date-time · nullable optional

When this key stopped authenticating, as an RFC 3339 timestamp in UTC, or null while it still works. A revoked key stays listed with this set, so its history stays with it.

created_at string · date-time required

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.

organisation string optional

Which organisation these keys belong to. GET /v1/account/api_keys sets it, so a page you fetched for one of your organisations can be told from a page you fetched for another without remembering which request produced which. GET /v1/api_keys leaves it out: that call answers for the organisation on your key and there is only ever one.

Other responses

401No valid credential was presented.
404No record with that identifier.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
# query parameters: organisation (optional)
curl -X GET "https://sandbox.droomwork.io/v1/account/api_keys?organisation=org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z" \
  -H "Droomwork-Session: $DROOMWORK_SESSION"
import { Configuration, IAMApi } from '@heros/sdk';

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

// query parameters: organisation (optional)
const result = await api.iamAccountApiKeysList({ organisation: 'org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
// query parameters: organisation (optional)
const response = await fetch('https://sandbox.droomwork.io/v1/account/api_keys?organisation=org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z', {
  method: 'GET',
  headers: {
    'Droomwork-Session': process.env.DROOMWORK_SESSION,
  },
});
const result = await response.json();
import os

import heros_sdk

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

# query parameters: organisation (optional)
result = api.iam_account_api_keys_list(organisation='org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

# query parameters: organisation (optional)
response = requests.request(
    'GET',
    'https://sandbox.droomwork.io/v1/account/api_keys?organisation=org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z',
    headers={
        'Droomwork-Session': os.environ['DROOMWORK_SESSION'],
    },
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

# query parameters: organisation (optional)
$result = $api->iamAccountApiKeysList(organisation: 'org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
// query parameters: organisation (optional)
$ch = curl_init('https://sandbox.droomwork.io/v1/account/api_keys?organisation=org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Session: ' . getenv('DROOMWORK_SESSION'),
  ],
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.IamApi;

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

// query parameters: organisation (optional)
var result = api.iamAccountApiKeysList("org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
// query parameters: organisation (optional)
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/account/api_keys?organisation=org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"))
    .header("Droomwork-Session", System.getenv("DROOMWORK_SESSION"))
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Heros.Sdk.Api;
using Heros.Sdk.Client;

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

// query parameters: organisation (optional)
var result = api.IamAccountApiKeysList(organisation: "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
// query parameters: organisation (optional)
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://sandbox.droomwork.io/v1/account/api_keys?organisation=org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
request.Headers.Add("Droomwork-Session", Environment.GetEnvironmentVariable("DROOMWORK_SESSION"));
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

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

// query parameters: organisation (optional)
result, _, err := client.IAMAPI.IamAccountApiKeysList(ctx).Organisation("org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
// query parameters: organisation (optional)
req, _ := http.NewRequest("GET", "https://sandbox.droomwork.io/v1/account/api_keys?organisation=org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z", nil)
	req.Header.Set("Droomwork-Session", os.Getenv("DROOMWORK_SESSION"))
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": "key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
      "object": "api_key",
      "livemode": false,
      "mocked": false,
      "name": "Ada Obi",
      "realm": "test",
      "scopes": [
        "run:read"
      ],
      "created_at": "2026-09-01T09:00:00Z",
      "revoked_at": "2026-09-01T09:00:00Z"
    }
  ],
  "has_more": true,
  "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
POST/v1/account/api_keys#

Make an API key while signed in

iam.account.api_keys.create

Make an API key from a signed in person's session. No other call issues a key from a session for an organisation you already hold, so you can build a keys page in your own product: each server or job gets its own key, named and scoped, and revoked there when it leaks.

Use name to tell keys apart. Leave scopes out, or empty, and the key carries every scope we issue. POST /v1/api_keys defaults to run:read, so set scopes on both. Copy key from the 201.

A key acts as the organisation it was issued for and no other, so two of your organisations take two keys.

Stop one at POST /v1/account/api_keys/{id}/revoke.

Body optional

name string optional

What this key is for, so a list of them can be read later.

scopes array of Scope optional

The scopes the key should carry, each module:action; name only what it needs. A value that isn't a scope refuses the whole request, and you can't issue the wildcard.

organisation string optional

Which of your organisations the key is for, when you hold more than one. Leave it out and the key is for the one you signed in as. Only POST /v1/account/api_keys reads this, because only a session knows a person; POST /v1/api_keys takes its organisation from your key and refuses to be pointed anywhere else. Name an organisation you don't own and you get not_found, the same answer as an organisation that doesn't exist. GET /v1/me lists the ones you can name.

Returns

The key record, with a created_at you never sent, so you can store when this key was issued.

id string required

The key's identifier; it starts with key_ and never changes. Returned by POST /v1/api_keys, POST /v1/account/api_keys and POST /v1/registrations, listed by GET /v1/api_keys and GET /v1/me, and sent as {id} to revoke it.

object always "api_key" required

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

livemode boolean required

False for a record in the sandbox realm, true for one in the live realm. This answers which data a record belongs to. mocked answers where its numbers came from, and the two are separate questions. A sandbox payslip computed by the real engine is livemode false and mocked false. A deferred capability in the live realm is livemode true and mocked true.

mocked boolean required

True when this response came from a mock, false when the engine computed the numbers. It stays true for a capability that is still on its documented mock, even in the live realm. It answers a different question from livemode: livemode says which realm you are in, this says where the numbers came from.

name string required

What this key is for, as named when it was created. Use it to tell your keys apart in a list.

realm string required

Which realm this key reaches: test is the sandbox, where nothing moves, and live is real money and real filings. Sandbox keys are test and carry livemode false; live keys aren't self-serve.

testlive
scopes array of Scope required

What this key may do, as module:action scopes; a request needing one not listed here is refused. You can't issue the wildcard, so it never appears here.

revoked_at string · date-time · nullable optional

Always null on a key you have just been given, which is every response that carries this schema. A key stops authenticating when you revoke it at POST /v1/api_keys/{id}/revoke or POST /v1/account/api_keys/{id}/revoke, and GET /v1/api_keys and GET /v1/account/api_keys are where it then shows the timestamp.

created_at string · date-time required

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

key string required

Shown here and never again. Send it as the Droomwork-Api-Key header.

organisation string required

Which organisation this key acts as, which is not always the one you signed in to. POST /v1/account/api_keys takes an organisation naming a product your company owns, and the key it answers with acts as that product and no other, so read this to confirm which one you just minted for. POST /v1/api_keys leaves it out: that call issues for the organisation on your credential and there is only ever one.

Other responses

400The request could not be read, or a value was refused.
401No valid credential was presented.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/account/api_keys" \
  -H "Droomwork-Session: $DROOMWORK_SESSION" \
  -H "Content-Type: application/json" \
  -d '{"name":"CI","scopes":["run:read"],"organisation":"org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}'
import { Configuration, IAMApi } from '@heros/sdk';

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

const result = await api.iamAccountApiKeysCreate({
  iamApiKeyCreateRequest: {"name":"CI","scopes":["run:read"],"organisation":"org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"},
});
const response = await fetch('https://sandbox.droomwork.io/v1/account/api_keys', {
  method: 'POST',
  headers: {
    'Droomwork-Session': process.env.DROOMWORK_SESSION,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "name": "CI",
    "scopes": [
      "run:read"
    ],
    "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
  }),
});
const result = await response.json();
import os

import heros_sdk

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

result = api.iam_account_api_keys_create(body={"name": "CI", "scopes": ["run:read"], "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"})
import os

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/account/api_keys',
    headers={
        'Droomwork-Session': os.environ['DROOMWORK_SESSION'],
    },
    json={"name": "CI", "scopes": ["run:read"], "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->iamAccountApiKeysCreate(json_decode('{"name":"CI","scopes":["run:read"],"organisation":"org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}', true));
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/account/api_keys');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Session: ' . getenv('DROOMWORK_SESSION'),
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => '{"name":"CI","scopes":["run:read"],"organisation":"org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}',
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.IamApi;

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

var result = api.iamAccountApiKeysCreate(body);
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/account/api_keys"))
    .header("Droomwork-Session", System.getenv("DROOMWORK_SESSION"))
    .header("Content-Type", "application/json")
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "name": "CI",
          "scopes": [
            "run:read"
          ],
          "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Heros.Sdk.Api;
using Heros.Sdk.Client;

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

var result = api.IamAccountApiKeysCreate(body);
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/account/api_keys");
request.Headers.Add("Droomwork-Session", Environment.GetEnvironmentVariable("DROOMWORK_SESSION"));
request.Content = new StringContent("""
    {
      "name": "CI",
      "scopes": [
        "run:read"
      ],
      "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

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

result, _, err := client.IAMAPI.IamAccountApiKeysCreate(ctx).IamApiKeyCreateRequest(body).Execute()
body := strings.NewReader(`{
  "name": "CI",
  "scopes": [
    "run:read"
  ],
  "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/account/api_keys", body)
	req.Header.Set("Droomwork-Session", os.Getenv("DROOMWORK_SESSION"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "api_key",
  "livemode": false,
  "mocked": false,
  "name": "Ada Obi",
  "realm": "test",
  "scopes": [
    "run:read"
  ],
  "created_at": "2026-09-01T09:00:00Z",
  "revoked_at": null,
  "key": "dw_test_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}
POST/v1/account/api_keys/{id}/revoke#

Stop a key from working, while signed in

iam.account.api_keys.revoke

This stops one of your API keys working, straight away. So you can put a Revoke button on your settings screen, turn off a colleague's key, or replace one by making the new key first and stopping the old one after.

Send the key's id in the path, the one listed at GET /v1/account/api_keys, and your session token as Droomwork-Session, not an API key.

If the key belongs to a product you own, and not to the organisation you signed in as, send that product as organisation. The key has to belong to the organisation you name, so a product's key sent without one answers not_found.

Read revoked_at in the answer: the moment the key stopped. Revoking isn't deleting.

Path parameters

id string required

The key's id, which starts with key_, as returned by POST /v1/api_keys or POST /v1/account/api_keys when you made it and listed by GET /v1/api_keys and GET /v1/me. A key belonging to another organisation answers not_found.

Body optional

organisation string optional

Which of your organisations the key belongs to. Leave it out for the one you signed in as. GET /v1/me lists the ones you can name.

Returns

The key, with the moment it stopped working.

id string required

The key's identifier; it starts with key_ and never changes. Returned by POST /v1/api_keys, POST /v1/account/api_keys and POST /v1/registrations, listed by GET /v1/api_keys and GET /v1/me, and sent as {id} to revoke it.

object always "api_key" required

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

livemode boolean required

False for a record in the sandbox realm, true for one in the live realm. This answers which data a record belongs to. mocked answers where its numbers came from, and the two are separate questions. A sandbox payslip computed by the real engine is livemode false and mocked false. A deferred capability in the live realm is livemode true and mocked true.

mocked boolean required

True when this response came from a mock, false when the engine computed the numbers. It stays true for a capability that is still on its documented mock, even in the live realm. It answers a different question from livemode: livemode says which realm you are in, this says where the numbers came from.

name string required

What this key is for, as named when it was created. Use it to tell your keys apart in a list.

realm string required

Which realm this key reaches: test is the sandbox, where nothing moves, and live is real money and real filings. Sandbox keys are test and carry livemode false; live keys aren't self-serve.

testlive
scopes array of Scope required

What this key may do, as module:action scopes; a request needing one not listed here is refused. You can't issue the wildcard, so it never appears here.

revoked_at string · date-time · nullable optional

When this key stopped authenticating, as an RFC 3339 timestamp in UTC, or null while it still works. A revoked key stays listed with this set, so its history stays with it.

created_at string · date-time required

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

Other responses

401No valid credential was presented.
404No record with that identifier.

Errors it can return

Sends against https://sandbox.droomwork.io
Request
which?
curl -X POST "https://sandbox.droomwork.io/v1/account/api_keys/key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/revoke" \
  -H "Droomwork-Session: $DROOMWORK_SESSION" \
  -H "Content-Type: application/json" \
  -d '{"organisation":"org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}'
import { Configuration, IAMApi } from '@heros/sdk';

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

const result = await api.iamAccountApiKeysRevoke({ id: 'key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z' });
const response = await fetch('https://sandbox.droomwork.io/v1/account/api_keys/key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/revoke', {
  method: 'POST',
  headers: {
    'Droomwork-Session': process.env.DROOMWORK_SESSION,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
  }),
});
const result = await response.json();
import os

import heros_sdk

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

result = api.iam_account_api_keys_revoke(id='key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z')
import os

import requests

response = requests.request(
    'POST',
    'https://sandbox.droomwork.io/v1/account/api_keys/key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/revoke',
    headers={
        'Droomwork-Session': os.environ['DROOMWORK_SESSION'],
    },
    json={"organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"},
)
result = response.json()
<?php
require_once __DIR__ . '/vendor/autoload.php';

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

$result = $api->iamAccountApiKeysRevoke(id: 'key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z');
<?php
$ch = curl_init('https://sandbox.droomwork.io/v1/account/api_keys/key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/revoke');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_HTTPHEADER => [
    'Droomwork-Session: ' . getenv('DROOMWORK_SESSION'),
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => '{"organisation":"org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"}',
]);
$result = json_decode(curl_exec($ch), true);
import com.useheros.sdk.ApiClient;
import com.useheros.sdk.Configuration;
import com.useheros.sdk.api.IamApi;

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

var result = api.iamAccountApiKeysRevoke("key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://sandbox.droomwork.io/v1/account/api_keys/key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/revoke"))
    .header("Droomwork-Session", System.getenv("DROOMWORK_SESSION"))
    .header("Content-Type", "application/json")
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
        }
        """))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();
using Heros.Sdk.Api;
using Heros.Sdk.Client;

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

var result = api.IamAccountApiKeysRevoke(id: "key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z");
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://sandbox.droomwork.io/v1/account/api_keys/key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/revoke");
request.Headers.Add("Droomwork-Session", Environment.GetEnvironmentVariable("DROOMWORK_SESSION"));
request.Content = new StringContent("""
    {
      "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
    }
    """, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
import heros "github.com/fenibofubara/heros-sdk-go"

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

result, _, err := client.IAMAPI.IamAccountApiKeysRevoke(ctx, "key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z").Execute()
body := strings.NewReader(`{
  "organisation": "org_01J8XQ4M7K2N9P3R5T7V9W1Y3Z"
}`)
req, _ := http.NewRequest("POST", "https://sandbox.droomwork.io/v1/account/api_keys/key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z/revoke", body)
	req.Header.Set("Droomwork-Session", os.Getenv("DROOMWORK_SESSION"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
	return err
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
Response
{
  "id": "key_01J8XQ4M7K2N9P3R5T7V9W1Y3Z",
  "object": "api_key",
  "livemode": false,
  "mocked": false,
  "name": "Ada Obi",
  "realm": "test",
  "scopes": [
    "run:read"
  ],
  "created_at": "2026-09-01T09:00:00Z",
  "revoked_at": "2026-09-01T09:00:00Z"
}