# Security & data handling

What Kaabist stores about your jobs, how secrets and sessions are protected, how long data is kept, and how to delete it.

This page describes what Kaabist keeps when it runs your programs, how it protects credentials and
signed-in sessions, and what you can do to keep exposure small. Read it before you automate a flow
that signs in, handles personal data or runs on behalf of your own customers.

## When to use it

Use this page when you design a program that handles credentials, when you answer a security
review about Kaabist, or when you need to delete what Kaabist holds for your organisation. For the
exact lifetimes of each record, see [data retention](https://docs.kaab.ist/concepts/data-retention); for how traffic
leaves Kaabist, see [egress and proxies](https://docs.kaab.ist/concepts/egress-and-proxies).

## What is stored, and where

| What | Where | How long |
|---|---|---|
| A job's inputs, result (`data`, `raw`), warnings and timeline | Kaabist's database, scoped to your organisation; secret inputs encrypted | results readable for 90 days; test-mode results 24 hours |
| Page snapshots, failure screenshots, attention bundles, files from `download` and `screenshot` steps | object storage, under a key that starts with your organisation | your plan's retention: 7, 30 or 90 days |
| Ground-truth snapshots of the fields you confirmed | object storage | as long as the program version is active: healing works from them |
| Sessions (signed-in browser state) | database, encrypted | until you delete the session |
| Credit ledger and audit log | database, append-only | never rewritten |

Data is processed in the EU region by default. Kaabist runs on Hetzner infrastructure, and payments
go through Paddle as merchant of record; the full sub-processor list accompanies the data processing
agreement.

Retention per plan comes from [plans](https://docs.kaab.ist/reference/plans): `free` keeps 7 days, `developer` and `team`
30, `scale` and `enterprise` 90.

## Encryption of secrets

Every organisation has its own data key. Secrets (session storage state, stored passwords, secret
job inputs, bring-your-own provider credentials) are encrypted with that key before they are
written, and the data key is itself encrypted by a master key held in a separate key-management
service. Plaintext exists only in the memory of the worker running your job: it is never written to
a log, a snapshot, a screenshot or a webhook.

API keys are stored only as a keyed hash. The key is shown once, when you create it, and cannot be
shown again.

## Secret inputs and `store`

Declare every password, token or one-time code as an input of type `secret`. A secret input is:

- masked in logs and in the job's `meta`, and never returned by any API call;
- redacted in screenshots and snapshots: the field it was typed into (and every `type=password`
  field) is blanked before the capture;
- refused where it would leak: lint error [E013](https://docs.kaab.ist/reference/dsl/lint/e013) rejects a secret used in
  an `extract` or rendered into a URL.

By default a secret is supplied with each job and kept only for that job. `store: true` on a
`secret` input lets Kaabist save the value on a session so the session's refresh program can sign in
again without you. It works only when your organisation has enabled secret storage; otherwise the
request fails with `secret_storage_disabled`. Storing passwords is optional and not recommended: the
safer default is a session plus a hosted page that asks a person to sign in again when it expires
(see [authenticated flows](https://docs.kaab.ist/guides/authenticated-flows) and [hosted resolve](https://docs.kaab.ist/guides/hosted-resolve)).

Programs that fill a password field are rate limited per organisation (60 runs an hour by default)
and need a verified payment method.

## Storage states and sessions

A session holds a browser's cookies and local storage in Playwright's `storageState` format. It is
encrypted like any other secret, never included in a snapshot, and never shown back: the panel lets
you replace or delete a session, not read it. Jobs that use a session or a secret input run in a
browser process of their own organisation, never one shared with other customers. See
[sessions](https://docs.kaab.ist/concepts/sessions).

## Snapshots and redaction

A snapshot keeps the page's HTML after it settled, its URL and headers, a summary of the network
log, and JSON response bodies up to 512 KB each (for JSON and network locators). Other response
bodies are dropped, and storage state is never part of a snapshot. Screenshots are kept for failed
steps and attention bundles, and when a program takes one with a `screenshot` step. Password
fields and the targets of secret inputs are redacted before any capture.

When you pick on a stored page in the panel, scripts are removed from the copy and it is shown in a
sandbox. See [snapshots](https://docs.kaab.ist/concepts/snapshots).

## Live views and hosted pages

A live view (a takeover, a recording session) and a hosted resolve page use short-lived tokens
scoped to one job: 15 minutes, refreshed while the page is open. The frames of a live view are
streamed and never stored.

## Deleting your data

An `admin` key (or an owner or admin in the panel) can ask for everything to be deleted:

```http
POST /v1/org/data-deletion
Authorization: Bearer sk_live_…
Content-Type: application/json

{ "confirm": "<your organisation's slug>" }
```

The answer is `202` with the request's `id` and `scheduled_for`. Snapshots, results and sessions are
purged within 24 hours; copies already in backups expire within 35 days. One request can be outstanding per organisation: asking again returns the
first. The credit ledger and the audit log are kept, because they are the record of what was billed
and who approved what.

## Webhook signing

Every webhook delivery carries `Kaab-Signature: t=<unix seconds>,v1=<hex>`, an HMAC-SHA256 of
`<t>.<raw body>` with your endpoint's `whsec_…` secret. Verify it on the raw body, and reject a
delivery whose `t` is more than 300 seconds from your clock. After a rotation the previous secret
keeps signing alongside the new one for 24 hours, so accept a delivery when any `v1` matches.

Payloads are minimal: the event, the job, the reason and a link. Extracted data is never in a
webhook; fetch the job with your key. See [webhooks](https://docs.kaab.ist/guides/webhooks).

## Key hygiene

- **Separate keys per use.** `sk_test_` keys run in test mode; `sk_live_` keys do real work. Give a
  service the narrowest scope it needs: `read`, `run` or `admin`. Only `admin` manages keys,
  webhooks, BYO providers and the organisation.
- **Keep keys server-side.** Never ship a key in a browser bundle or a mobile app.
- **Rotate with a grace period.** `POST /v1/api-keys/{id}/rotate` with `grace_seconds` (at most one
  day) issues a new key and keeps the old one working until the grace ends, so you can deploy
  without downtime. Revoking is immediate.
- **Set an expiry.** Responses to a key with less than 7 days left carry `X-Kaab-Key-Expires`.
- **Use two-factor authentication.** Owners and admins need an authenticator app to manage keys,
  members and billing in the panel.

## SSRF protection and egress

Every request a job makes leaves through Kaabist's egress gateway; workers have no other route to
the internet. The gateway resolves the hostname itself, checks the address, and connects to that
checked address, again on every redirect. Private networks (RFC 1918), loopback, link-local
addresses, the cloud metadata address `169.254.169.254`, their IPv6 equivalents and `.internal` and
`.local` names are refused, as are targets on Kaabist's blocklist. A refused request fails the job
with `ssrf_blocked` (or `target_disallowed` for a blocked target).

The gateway also paces requests per hostname across all customers, so a burst of your jobs does not
hammer a site. See [egress and proxies](https://docs.kaab.ist/concepts/egress-and-proxies).

## Worked example

Take a customer portal at `https://portal.example.com` that signs in with an e-mail, a password and
a one-time code. The program
declares the password as a `secret` input, so it is typed into the form but never logged, captured
or returned:

### TypeScript

```ts
// `password` is declared `{ "type": "secret" }` in the program: it is encrypted at rest, never
// returned, and redacted from logs, step records and snapshots.
const job = await kaab.jobs.create({
  program: name,
  inputs: {
    base_url: `${site}`,
    password: process.env.LOGIN_PASSWORD ?? "secret",
  },
});
```

### Python

```python
# password is declared {"type": "secret"} in the program: encrypted at rest, never returned,
# and redacted from logs, step records and snapshots.
password = os.environ.get("LOGIN_PASSWORD", "secret")
job = kaab.jobs.create(program=name, inputs={"base_url": f"{site}", "password": password})
```

### Go

```go
// password is declared {"type": "secret"} in the program: encrypted at rest, never returned,
// and redacted from logs, step records and snapshots.
password := os.Getenv("LOGIN_PASSWORD")
if password == "" {
	password = "secret"
}
body, err := kaab.Body[client.CreateJobJSONBody](map[string]any{
	"program": name,
	"inputs":  map[string]any{"base_url": site + "", "password": password},
})
if err != nil {
	log.Fatal(err)
}
job, err := c.Jobs.Create(ctx, body)
if err != nil {
	log.Fatal(err)
}
```

Read the finished job back: the envelope identifies the inputs only by `inputs_hash`, `meta` holds
no password, and the password field is blank in the step's stored page. The other half is the
network guard: a job pointed at a page that redirects to `169.254.169.254` fails with
`ssrf_blocked` instead of following it.

## Pitfalls

- **A secret as a plain `string` input.** It is then logged and returned like any other input.
  Declare it `secret`.
- **Extracting a value that contains personal data you do not need.** Kaabist stores what your
  program extracts for the retention period. Extract only what you use.
- **`store: true` by habit.** It keeps a password at rest. Prefer a session and a hosted re-login.
- **Trusting a webhook without checking the signature**, or verifying a re-serialized body instead
  of the raw one.
- **One `admin` key everywhere.** A leaked `run` key cannot create keys or change webhooks.

## Reference

- [Request data deletion](https://docs.kaab.ist/reference/api/org/request-data-deletion)
- [Create an API key](https://docs.kaab.ist/reference/api/api-keys/create-api-key), [rotate](https://docs.kaab.ist/reference/api/api-keys/rotate-api-key),
  [revoke](https://docs.kaab.ist/reference/api/api-keys/revoke-api-key)
- [Inputs](https://docs.kaab.ist/reference/dsl/inputs) and lint [E013](https://docs.kaab.ist/reference/dsl/lint/e013)
- [Error codes](https://docs.kaab.ist/reference/codes#errors) and [key scopes](https://docs.kaab.ist/reference/codes#key-scopes)
- [Plans](https://docs.kaab.ist/reference/plans)
