Docs
API 1.39
Status
Guides

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; for how traffic leaves Kaabist, see egress and proxies.

#What is stored, and where

WhatWhereHow long
A job's inputs, result (data, raw), warnings and timelineKaabist's database, scoped to your organisation; secret inputs encryptedresults readable for 90 days; test-mode results 24 hours
Page snapshots, failure screenshots, attention bundles, files from download and screenshot stepsobject storage, under a key that starts with your organisationyour plan's retention: 7, 30 or 90 days
Ground-truth snapshots of the fields you confirmedobject storageas long as the program version is active: healing works from them
Sessions (signed-in browser state)database, encrypteduntil you delete the session
Credit ledger and audit logdatabase, append-onlynever 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: 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 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 and 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.

#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.

#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:

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. 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.

#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.

#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
// `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
# 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
// 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

Was this page helpful?