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
| Permission | In the dialog | Lets the key |
|---|---|---|
workspace:read | Read the workspace Unavailable for now | Read assets — required for every GET. Not grantable from the console yet, so no key currently holds it. |
collector:write | Manage collectors and integrations | Create hosts, and resume a suspended asset. Pre-selected. |
collector:revoke | Revoke collector tokens | Revoke a host's credential. |
asset:archive | Archive assets | Archive an asset. |
asset:unarchive | Restore assets | Return 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.
| Credential | Looks like | Scope |
|---|---|---|
| Organization key | japi_… | Your whole workspace, limited to the permissions you gave the key. |
| Asset key | jcol_… | Exactly one asset — its own. This is also the agent's ingest credential. |
| Console session | a short-lived token | Whatever 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
| Call | Does | Needs |
|---|---|---|
POST /api/v1/assets | Create a host. Returns its id, its key and its install command. | collector:write |
GET /api/v1/assets | List the estate. | workspace:read — not grantable yet |
GET /api/v1/assets/:id | Read one asset, in any status. | workspace:read — not grantable yet |
PATCH /api/v1/assets/:id/status | Archive 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"}'
| Field | Required | Notes |
|---|---|---|
type | Yes | linux, macos, windows or docker. |
name | Yes | 1–120 characters. The label shown in Data Sources. |
eps_limit | No | Events 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.
| type | allowed_source_types |
|---|---|
linux | linux.auth, syslog.generic |
macos | macos.unified, macos.esf |
windows | windows.security, windows.system, windows.sysmon |
docker | docker.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 for | From active | From archived | From suspended | From revoked |
|---|---|---|---|---|
| archived | Archives it (asset:archive) | No-op, 200 | Archives it | Archives it |
| active | No-op, 200 | Restores it if it was active when archived (asset:unarchive); otherwise 409 invalid_transition | Resumes it (collector:write) | 409 invalid_transition |
| revoked | Revokes the credential (collector:revoke) | 409 invalid_transition | 409 not_a_collector | No-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_reachedif 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_tonaming 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_collectorwhatever 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.
| Status | error | Meaning |
|---|---|---|
| 400 | validation_failed | The body did not match. issues names the fields. |
| 400 | invalid_asset_id | Not a UUID. |
| 401 | invalid_api_key | Unknown, revoked, expired, or a rotated-out asset key. |
| 402 | payment_required | The workspace is awaiting payment. Reads still work. |
| 403 | forbidden | The key lacks a permission. The body names it in required_permission. Every GET answers this today, naming workspace:read. |
| 403 | asset_scope_mismatch | An asset key was aimed at something other than its own asset. |
| 403 | org_suspended | The workspace is suspended. |
| 404 | asset not found | No such asset in this workspace. |
| 404 | org_not_found | The key's workspace no longer exists. |
| 409 | asset_limit_reached | At the plan's asset ceiling. Carries limit and current. |
| 409 | invalid_transition | Not reachable from the current status. status names it, and restores_to names where a restore would land. |
| 409 | not_a_collector | Revocation was asked of an integration. |
| 409 | purge_in_progress | A restore was refused: the asset's data is being purged. |
| 409 | needs_admin_repair | A restore was refused: the archived row does not record what to restore to. |
| 409 | restore_conflict | The asset changed mid-restore. Nothing was changed; read it again and decide. |
| 429 | rate_limited | Over the per-key limit. Retry-After says when to retry. |
| 503 | asset_ceiling_busy | Too many concurrent creates or restores queued for this workspace. Nothing was changed, so retry. |
| 503 | control_plane_unavailable | Ours, 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-Afterheader. 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_busyand 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.