Automate Jutsu — the Jutsu API

Everything you do in Data Sources can be done from your own tooling instead. The Jutsu API lets a provisioning script, a CI job, a Terraform local-exec block or an internal portal create and manage assets without anyone opening a browser. One call mints a host, returns its credential, and hands back the install command for that platform.

Onboarding fifty servers used to mean fifty trips through the Data Sources dialog. It is now a loop.

Path: Organization → API keys in the left sidebar, to mint the credential. The API itself lives at /api/v1/assets on your Jutsu host.

What the API covers today: host assets — Linux, macOS, Windows and Docker collectors. Cloud and SaaS integrations such as AWS CloudTrail, Google Cloud Platform and Google Workspace still connect through the console. They will join this surface through the same type field.

Reads are not available to console-minted keys yet. Creating, archiving and revoking work today. The two GET endpoints need the workspace:read permission, and the console cannot grant it yet — the create dialog lists it as Unavailable for now. A key you mint today is therefore write-only, and a GET with it answers 403 forbidden naming workspace:read. The read reference below is included so it is ready when that opens up.

Create an API key

Minting a key is a console action and only a console action. A key can never create another key, and the page is gated on org:manage, so an analyst who opens it would see nothing but errors.

  • Open Organization → API keys in the left sidebar.
  • Click Create key.
  • Give it a Name. Use something you would recognise in an audit log a year from now — "deploy pipeline", not "test". The audit log names the key, never a person, so it is only as readable as the name you choose.
  • Choose an Expiry: Never expires, 30 days, 90 days or 1 year. Never is the default, because most keys run unattended in CI.
  • Select the Permissions the key needs. Manage collectors and integrations is the only one pre-selected, and Read the workspace is greyed out as Unavailable for now — see the table below.
  • Click Create key.
  • Copy the key from the confirmation dialog before closing it. The dialog also shows a ready-to-run example call. [IMAGE_1]

SHOWN ONCE: Jutsu stores only a hash of the key. The secret exists in that dialog and nowhere else. If you close it without copying, the key cannot be recovered — revoke it and create another. Store it in your secret manager, not in a repository. [IMAGE_2]

The five permissions

PermissionIn the dialogLets the key
workspace:readRead the workspace Unavailable for nowRead assets — required for every GET. Not grantable from the console yet, so no key currently holds it.
collector:writeManage collectors and integrationsCreate hosts, and resume a suspended asset. Pre-selected.
collector:revokeRevoke collector tokensRevoke a host's credential.
asset:archiveArchive assetsArchive an asset.
asset:unarchiveRestore assetsReturn an archived asset to the active inventory.

Two rules to plan around

  • You cannot grant a permission you do not hold. The scope you can give a key is capped by your own. A permission you lack renders greyed out with the reason shown rather than hidden, so it is clear why it is unavailable. A key is never a route to authority you do not already have.
  • A key cannot mint another key. Key management is deliberately console-only. There is no API endpoint that creates, lists or revokes keys.

Managing keys you already have

The API keys page lists every key in the workspace with its status badge — Active, Expired or Revoked — its prefix, how many permissions it carries, and four dates: Created, Last used, Expires, and who created it. [IMAGE_3]

Last used is the column that matters for hygiene. A key that has never been used, or has not been used in months, is a live credential doing no work. Revoke it.

To revoke a key, click the bin icon on its row and confirm. Anything using that key stops working within about a minute. Revocation cannot be undone — you would create a new key and update whatever uses it.

A key outlives the person who made it. That is the point of a service key: your deploy pipeline keeps running after its author leaves. It also means an offboarding checklist that only removes people leaves their keys running. The creator column shows "a removed user" once the account is gone.

The three credentials

Three kinds of credential reach Jutsu, and they are not interchangeable.

CredentialLooks likeScope
Organization keyjapi_…Your whole workspace, limited to the permissions you gave the key.
Asset keyjcol_…Exactly one asset — its own. This is also the agent's ingest credential.
Console sessiona short-lived tokenWhatever the signed-in person can do.

All three are bearer tokens:

Authorization: Bearer japi_your_key_here

The credential decides the workspace. There is no tenant header to set, and no way for a key to act on another organization.

Keys work on the asset API only. An organization key is accepted under /api/v1/assets and nowhere else. Point it at any other endpoint and it is read as a console session, so it answers 401 invalid or expired token even though the key itself is perfectly valid. A leaked organization key cannot read your alerts or your events.

The four endpoints

CallDoesNeeds
POST /api/v1/assetsCreate a host. Returns its id, its key and its install command.collector:write
GET /api/v1/assetsList the estate.workspace:read — not grantable yet
GET /api/v1/assets/:idRead one asset, in any status.workspace:read — not grantable yet
PATCH /api/v1/assets/:id/statusArchive it, restore it, or revoke its credential.depends on the change

Create a host

curl -X POST https://<your-jutsu-host>/api/v1/assets \  -H "Authorization: Bearer $JUTSU_API_KEY" \  -H 'Content-Type: application/json' \  -d '{"type":"linux","name":"web-01"}'
FieldRequiredNotes
typeYeslinux, macos, windows or docker.
nameYes1–120 characters. The label shown in Data Sources.
eps_limitNoEvents per second for this host. Defaults to 1000.

A 201 response carries three things: the asset, its key, and its install commands.

{  "asset": {    "id": "6f1e…",                     // the id used everywhere below    "kind": "collector",    "type": "linux",    "name": "web-01",    "status": "active",    "eps_limit": 1000,    "allowed_source_types": ["linux.auth", "syslog.generic"],    "token_prefix": "jcol_A1b2C3d",    // the display handle, safe to log    "criticality": "medium",    "created_at": "2026-09-10T09:00:00.000Z"  },  "api_key": "jcol_…",                 // shown ONCE  "install": {    "platform": "linux",    "shell": "bash",    "install": "curl -fsSL https://…/install.sh | JUTSU_TOKEN=jcol_… sh",    "verify":  "…   the same install, checksum verified first",    "vector":  "…   the sink stanza the agent will run",    "uninstall": { "shell": "bash", "uninstall": "…", "dry_run": "…" },    "script_sha256": "…",    "script_version": "1.8.0",    "no_curl": null  }}

install.install is the one-liner to run on the host. install.verify is the same install with the script's published SHA-256 checked first — prefer it in automation, where nobody is reading the output.

Docker differs in two ways: no_curl carries wget and via_docker variants for minimal images, and script_sha256 is null because the Docker bootstrap is rendered per host, with its hash published at /install-docker.sh.sha256 instead.

The install command is built from the host you called. Call api-beta.example.com and the agent is told to report to api-beta.example.com. That is deliberate, because one ingress serves several names — but it means calling the wrong hostname produces a host that ships to the wrong place, with no error.

Which source types a host may ship

Each platform's allow-list comes from the same catalog the console's tiles use.

typeallowed_source_types
linuxlinux.auth, syslog.generic
macosmacos.unified, macos.esf
windowswindows.security, windows.system, windows.sysmon
dockerdocker.logs

A 200 from ingest does not confirm delivery. Events outside a host's allow-list are dropped, and so are events sent with a revoked or unknown key — but the ingest endpoint still answers HTTP 200 in both cases. Confirm arrival by checking the host's activity in Assets, never by reading the ingest status code.

Read your assets

Not usable with a console-minted key yet. Both calls below require workspace:read, which the create dialog cannot currently grant, so today they answer 403 forbidden with required_permission: workspace:read. Everything else in this tab works.

GET /api/v1/assets lists the estate. Archived rows appear only with ?status=archived.

GET /api/v1/assets/:id returns one asset in any status, which is how you confirm that an archive landed.

Both return the list item shape, which is not the flat asset object that POST returns. Kind-specific fields sit under a collector or integration object, and a host's real status is the one inside collector:

{  "asset": {    "id": "6f1e…",    "kind": "collector",    "collector": {      "token_id": "6f1e…",          // the same id      "name": "web-01",      "token_prefix": "jcol_A1b2C3d",      "allowed_source_types": ["linux.auth", "syslog.generic"],      "status": "active",           // read a host's status here      "eps_limit": 1000,      "created_at": "2026-09-10 09:00:00.000",      "health": { … }               // null until the agent first reports    },    "criticality": "medium",    "host_platform": "linux"        // GET /:id only  }}

host_platform is linux, macos, windows, docker, or null when the token names no platform. A health of null means the agent has not reported yet, not that it is unhealthy.

Change an asset's status

curl -X PATCH https://<your-jutsu-host>/api/v1/assets/$ASSET_ID/status \  -H "Authorization: Bearer $JUTSU_API_KEY" \  -H 'Content-Type: application/json' \  -d '{"status":"archived"}'

status is active, archived or revoked. What each one does depends on where the asset is now.

Asking forFrom activeFrom archivedFrom suspendedFrom revoked
archivedArchives it (asset:archive)No-op, 200Archives itArchives it
activeNo-op, 200Restores it if it was active when archived (asset:unarchive); otherwise 409 invalid_transitionResumes it (collector:write)409 invalid_transition
revokedRevokes the credential (collector:revoke)409 invalid_transition409 not_a_collectorNo-op, 200

Notes that save a debugging session

  • Archiving frees a plan slot; restoring re-claims one. A restore can therefore fail with 409 asset_limit_reached if the workspace filled up while the asset was archived.
  • Asking for active only succeeds if the asset really becomes active. A host that was revoked before it was archived would come back revoked, so the request is refused with restores_to naming where a restore would actually land, and nothing is changed.
  • Archiving already revokes a host's credential, which is why re-revoking an archived asset is refused rather than pretended.
  • Revoked is final. There is no way back, because the credential is gone. Create a new host instead.
  • Revoked applies to hosts only. An integration answers 409 not_a_collector whatever its status: its credential lives at the provider and is yours to revoke there. A suspended asset is always an integration.
  • Asking for the status something already has succeeds, so a script that retries converges instead of having to interpret a 409.
  • A key holding no permission that could make the requested change gets a 403 before the asset is looked up at all, so it learns nothing about whether the asset exists.

Asset keys — a host that manages itself

The api_key in a create response is the host's own jcol_… token. It is both what the installed agent ships logs with and what manages that one asset:

curl -X PATCH https://<your-jutsu-host>/api/v1/assets/$ASSET_ID/status \  -H "Authorization: Bearer $ASSET_KEY" \  -H 'Content-Type: application/json' \  -d '{"status":"archived"}'

On the asset API, an asset key reaches only its own asset. It cannot create assets, cannot list them, and cannot touch a sibling; those requests answer 403 asset_scope_mismatch. That confinement is what makes it safe to leave the key sitting on the host it belongs to.

Archiving or revoking an asset with its own key succeeds, and then correctly stops that key from working. A host can retire itself, which is what makes decommissioning scriptable — and it is also why the next point exists.

A host that deregisters itself raises a high-severity alert. "Host deregistered itself with its own key" is mapped to Defense Evasion (T1562.001), because a monitored host going dark by its own hand looks exactly like an attacker switching monitoring off. Close the alert if the decommission was planned. The alert is best-effort — it is raised after the change commits, so an interruption at that moment can lose it. The audit log entry is written separately and is the durable record.

Errors

Every error is a flat JSON object with an error code.

StatuserrorMeaning
400validation_failedThe body did not match. issues names the fields.
400invalid_asset_idNot a UUID.
401invalid_api_keyUnknown, revoked, expired, or a rotated-out asset key.
402payment_requiredThe workspace is awaiting payment. Reads still work.
403forbiddenThe key lacks a permission. The body names it in required_permission. Every GET answers this today, naming workspace:read.
403asset_scope_mismatchAn asset key was aimed at something other than its own asset.
403org_suspendedThe workspace is suspended.
404asset not foundNo such asset in this workspace.
404org_not_foundThe key's workspace no longer exists.
409asset_limit_reachedAt the plan's asset ceiling. Carries limit and current.
409invalid_transitionNot reachable from the current status. status names it, and restores_to names where a restore would land.
409not_a_collectorRevocation was asked of an integration.
409purge_in_progressA restore was refused: the asset's data is being purged.
409needs_admin_repairA restore was refused: the archived row does not record what to restore to.
409restore_conflictThe asset changed mid-restore. Nothing was changed; read it again and decide.
429rate_limitedOver the per-key limit. Retry-After says when to retry.
503asset_ceiling_busyToo many concurrent creates or restores queued for this workspace. Nothing was changed, so retry.
503control_plane_unavailableOurs, not yours. Retry.

401 and 503 are never used for each other. A 401 means the credential is wrong; a 503 means Jutsu is unavailable. So it is always safe to retry a 503 with the same key, and never useful to retry a 401. A client that re-mints on every failure is reacting to the wrong signal.

Rate limits and concurrency

  • About 300 requests per minute per key. Over that is a 429 with a Retry-After header. Asset keys and organization keys have separate budgets.
  • When your plan has an asset ceiling, creates and restores for one workspace are processed one at a time, so the ceiling holds even when a script fires them in parallel. Parallel creates therefore take about as long as sequential ones. A request that waits too long answers 503 asset_ceiling_busy and can be retried. Plans without a ceiling are not queued.

Revoking a key

Revoking in the console takes effect immediately on the replica that handled it, and within about a minute everywhere else. If you are cutting off a credential you believe has leaked and you want certainty rather than an estimate, revoke it and then make a call that should now fail. A 401 is your confirmation.

What is recorded

Every write reaches the workspace audit log naming the credential, not a person: apikey:japi_A1b2C3d for an organization key, asset_key:… for an asset key. No individual is blamed for a machine's write.

That is also why key names matter. The audit log is only as readable as the names you chose when you minted the keys.