The Backstory API

Run Backstory investigations from your own code: submit an indicator, poll the job, and fetch the verdict, report, recommendations, and graph state over REST.

The Backstory API runs investigations from your own code. You submit an indicator, poll the job until it finishes, and fetch the verdict, the report, the remediation recommendations, or the raw graph state. The endpoint-by-endpoint reference is published as Stairwell Backstory (Beta) in the API Reference; this page is the shape of the workflow it belongs to.

Requests go to https://app.stairwell.com/backstory/v1beta1/... and authenticate with a Stairwell API key as a bearer token.

This is a beta surface. Routes and response shapes may change, and when they do it is announced in the changelog rather than discovered by your integration.

What can I do with it?

Everything an investigation needs from the outside, and nothing that requires a person.

  • Submit an investigation on a file hash, a hostname, or an IP address.
  • Poll its scheduling state until it reaches a terminal status.
  • Fetch the result, the recommendations on their own, or the graph state.
  • List investigations across environments, including ones run by hand in the app.
  • Search for the investigations that mention a given indicator.
  • Cancel, pause, and resume runs you submitted.

How do I submit an investigation?

Post the environment, a name, and the seed indicator.

curl -H "Authorization: Bearer $KEY" -X POST \
  https://app.stairwell.com/backstory/v1beta1/investigations:submit \
  -d '{
        "environmentId": "<env>",
        "name": "alert-4417-triage",
        "seed": {"iocType": "IOC_TYPE_FILE", "iocValue": "<sha256>"},
        "tier": "JOB_TIER_BATCH",
        "idempotencyKey": "alert-4417"
      }'

Three fields deserve a decision rather than a default:

  • idempotencyKey makes retries safe. Replaying the same key returns the original job instead of enqueuing a second investigation of the same thing. Set it from something stable in your own system, such as the alert id. This is the single most valuable field on the request.
  • tier is JOB_TIER_INTERACTIVE when someone is waiting on the answer, and JOB_TIER_BATCH, the default, for bulk work that is fair-shared with everyone else's.
  • intelEnvironmentIds adds environments whose intelligence the run may consult, without making their contents part of your blast radius.

You can also pass a batchId to group a bulk submission, which makes a morning's worth of vendor indicators one identifiable set rather than forty unrelated runs.

If you are over your organization's pending-investigation cap or its submission-window quota, submit returns a resource-exhausted error. Treat that as backpressure and retry later with the same idempotency key, not as a failure.

How do I know when it is finished?

Poll the job, then fetch the result.

curl -H "Authorization: Bearer $KEY" \
  https://app.stairwell.com/backstory/v1beta1/investigations/<id>/job

The job carries the status, the attempt count, a step count that advances as the run works, and the create, start, and finish times. Poll it until the status is terminal. Asking for the result before then returns a failed-precondition error rather than a partial answer, which is deliberate: there is no half-finished report to misread.

An unknown investigation and one you do not have access to both answer not-found, identically and on purpose. It is not a bug in your integration, and it is not something to work around.

What comes back?

curl -H "Authorization: Bearer $KEY" \
  https://app.stairwell.com/backstory/v1beta1/investigations/<id>/result \
  | jq -r .reportJson | base64 -d | jq .

The result carries the seed, the verdict, the summary, the immediate actions as a flat list of strings, the full report, and the classified remediation export. The report and the export are serialized JSON delivered as bytes, so they arrive base64 encoded over REST and you decode them to get the document. That indirection is what lets their schemas evolve without breaking this contract.

Two narrower endpoints exist for the two common cases:

  • /recommendations returns the remediation section on its own: the immediate actions and the classified export, without the rest of the payload. This is what an automation usually wants.
  • /state returns the gzipped graph state, the same artifact the app renders. Use it when you want the investigation itself rather than its conclusions.

The artifacts these endpoints return carry no analyst identities. They are the investigation's findings, not its authorship.

Which investigations can I see?

Every investigation in the environments you can read, whether it was submitted through the API or run by hand in the app.

curl -H "Authorization: Bearer $KEY" \
  "https://app.stairwell.com/backstory/v1beta1/investigations?environmentIds=<env>&statusFilter=STATUS_COMPLETED&pageSize=25"

Listing filters by status, by creation-time range, and by a substring of the name or the seed value. One cursor spans the whole environment set, so you page by passing nextPageToken back until it comes back empty. Environments you cannot read are skipped rather than refused; if none of the requested environments are readable, the call is denied.

There is one asymmetry worth designing around: scheduling state exists only for runs you submitted through the API. An investigation someone ran in the app never entered the queue, so asking for its job, or trying to cancel, pause, or resume it, answers not-found. Its status and its results are available through listing and the result endpoints like any other.

How do I find past investigations by indicator?

Search the recorded indicators.

curl -H "Authorization: Bearer $KEY" \
  "https://app.stairwell.com/backstory/v1beta1/investigations/iocs:search?environmentIds=<env>&iocType=IOC_TYPE_HOSTNAME&iocValue=evil.example"

Each hit names the investigation, the environment, the matched value, the disposition it was given, and when that was decided. The returned investigation ids resolve against the result and state endpoints, so a search is a usable entry point rather than a dead end.

Hostname searches match subdomains by default, so a search for a domain finds investigations that recorded a name underneath it. Pass exact=true to restrict that. File hashes and IP addresses always match exactly.

What should I read next?


Did this page help you?