# Create an API key Source: https://docs.kalligator.com/account/api-keys Make a kal_ API key so that your own agent or script can use the Kalligator API for you. An API key lets your own agent or script act for you without a browser. The key has your access. It never has the access of the Kalligator team. ## What a key can do A key can use all hacker API routes: programs, reports, files, messages, and account status. A key cannot: * Create, list, or delete API keys * Start Stripe onboarding * Use any route of the Kalligator team These limits stop a leaked key from making more keys or changing where your rewards go. ## Create a key You must verify your email first. See [Create your account](/account/create-account#verify-your-email). On your account page, open **API keys**. * **Name**: a label for the key, for example `My triage agent`. At most 80 characters. * **Access**: **Read and write** (default) or **Read only**. * **Expires**: **In 30 days**, **In 90 days** (default), **In 1 year**, or **Never**. Select **Create key**. Select **Copy** to copy the key. Then select **Done**. The page shows the key only one time. If you lose it, delete it and create a new key. ## Store the key safely * Put the key in a secret manager or an environment variable, for example `KALLIGATOR_API_KEY`. * Do not put the key in source code, in a report, or in a message. * Use one key for each agent, so that you can delete one key without an effect on the others. * Use a **Read only** key for an agent that only monitors your reports. ## Use the key Send the key in the `Authorization` header. ```bash Test your key theme={"dark"} curl https://kalligator.com/api/me \ -H "Authorization: Bearer $KALLIGATOR_API_KEY" ``` See [Authentication](/api-reference/authentication) for the full rules. ## Delete a key In **API keys**, select **Delete** next to the key. Agents that use the key stop working immediately. A key also stops working when: * It expires. * You change your password. This ends all keys that you made before the change. * Your account is disabled. ## Limits | Limit | Value | | - | - | | Keys for each account | 100 | | Expiry | 1 to 365 days, or no expiry. Default 90 days. | | Requests | 120 each minute for your account, shared by all your keys | The key list shows the first eight characters of each key, its access, its creation date, its last use, and its expiry date. # Create your account Source: https://docs.kalligator.com/account/create-account Sign up, verify your email, reset your password, and turn on two-step verification. You need a Kalligator account to save drafts and submit reports. A person must do the sign-up and the email verification in a browser. An agent cannot do these steps with an API key. ## Sign up Go to [kalligator.com/account](https://kalligator.com/account) and select **Sign up**. The password must have: * At least 12 characters, and at most 256 * One uppercase letter * One lowercase letter * One number * One special character The form shows a checklist of these rules as you type. Select **Create account**. Kalligator sends a verification email to your address. ## Verify your email Open the link in the verification email. The link opens the page `/auth/action`. When the page shows **Your email is verified**, select **Continue**. You must verify your email before you can: * Save a draft or submit a report * Set up payouts with Stripe * Create an API key * Turn on two-step verification If you did not get the email, open **Profile** on your account page and select **Resend verification email**. You can send one email each minute and five each hour. If your account page still shows **Not verified** after you open the link, select **Refresh** in **Profile**. ## Reset your password 1. Go to [kalligator.com/account](https://kalligator.com/account) and select **Sign in**. 2. Type your email, then select **Forgot password?**. 3. Open the link in the email. Type a new password and select **Save new password**. For your privacy, the page shows the same message if no account has that email. A password change ends all API keys that you created before the change. Create new keys after you change your password. ## Turn on two-step verification Two-step verification is optional for hackers. We recommend it. 1. On your account page, open **Security**. 2. Select **Set up authenticator app**. If the page asks for your password, type it. 3. Scan the QR code with an authenticator app, such as 1Password or Authy. 4. Type the 6-digit code from the app and select **Turn on**. After this, sign-in asks for a code from your authenticator app. ## Account page sections | Section | URL | Use it to | | - | - | - | | **Profile** | `/account?section=profile` | See your email and verification status. | | **Payouts** | `/account?section=payouts` | Set up and check your Stripe payout account. | | **Reports** | `/account?section=reports` | See your active-report slots and recent reports. | | **Security** | `/account?section=security` | Turn two-step verification on or off. | | **API keys** | `/account?section=api-keys` | Create and delete API keys. | ## Next step [Set up payouts](/account/payouts) so that you can submit reports. # Set up payouts Source: https://docs.kalligator.com/account/payouts Connect a Stripe account to receive rewards. You must do this before you submit a report. Kalligator pays rewards through Stripe. You connect a Stripe account one time. You cannot submit a report until your Stripe status is **Ready**. Payout setup needs a website sign-in. An API key cannot start Stripe onboarding. This rule stops a leaked key from changing where your rewards go. ## Connect Stripe The **Payouts** section does not show the Stripe button until your email is verified. See [Create your account](/account/create-account#verify-your-email). On your account page, open **Payouts**. Select **Complete Stripe setup**. Stripe opens its onboarding form. Complete all the required information. When you finish, Stripe sends you back to your account page. Kalligator checks your Stripe status again and shows the result. ## Stripe statuses | Status | What it means | What to do | | - | - | - | | **Not started** | You did not start onboarding. | Select **Complete Stripe setup**. | | **More information needed** | Stripe needs more information. | Select **Continue Stripe setup**. | | **Pending** | Stripe is checking your information. | Wait, then check again. | | **Ready** | You can submit reports and receive rewards. | No action. | | **Unavailable** | Kalligator cannot get your status from Stripe now. | Try again later. | | **Not configured** | Stripe is not set up on the server. | Send an email to [nathan@kalligator.com](mailto:nathan@kalligator.com). | To check the status again, select the refresh icon in **Payouts**. ## Check the status with the API Your agent can read the status with an API key. ```bash Check payout status theme={"dark"} curl https://kalligator.com/api/stripe/status \ -H "Authorization: Bearer $KALLIGATOR_API_KEY" ``` ```json Response theme={"dark"} { "configured": true, "status": "ready", "ready": true, "checked_at": "2026-10-01T14:03:12Z", "detail": "You can submit reports." } ``` Submission needs `ready: true`. `GET /api/me` returns the last stored status and does not contact Stripe. ## How rewards reach you When the Kalligator team approves a reward, Stripe moves it to your connected account. Stripe then pays it to your bank on your payout schedule. See [Rewards](/policies/rewards). Kalligator is in early beta. Payouts currently run in Stripe test mode. # End-to-end example Source: https://docs.kalligator.com/agents/end-to-end-example A Python script that checks the account, writes a draft, uploads evidence, submits after approval, and answers triage questions. This script shows the full API workflow in one file. Use it as a base for your own agent. It uses the [`requests`](https://pypi.org/project/requests/) library. ## Before you run it * Complete the [one-time setup](/agents/overview#what-a-person-must-do). * Set `KALLIGATOR_API_KEY` to a **Read and write** key. * Choose a program with open intake. Read its scope. Confirm that your asset is in scope. * Put your evidence file next to the script, for example `request.txt`. ```bash theme={"dark"} pip install requests export KALLIGATOR_API_KEY="kal_..." python submit_report.py acme-web request.txt ``` ## The script ```python submit_report.py expandable theme={"dark"} import os import sys import time import uuid import mimetypes import pathlib import requests API = "https://kalligator.com/api" FINAL = {"accepted", "rejected", "duplicate", "insufficient_info", "withdrawn"} session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['KALLIGATOR_API_KEY']}" class KalligatorError(Exception): def __init__(self, status, code, detail): super().__init__(f"{status} {code}: {detail}") self.code = code def call(method, path, **kwargs): """Send a request. Wait and retry when the API sends Retry-After.""" while True: response = session.request(method, f"{API}{path}", timeout=150, **kwargs) retry_after = response.headers.get("Retry-After") if response.status_code in (429, 503) and retry_after: time.sleep(int(retry_after)) continue if response.status_code >= 400: error = response.json() raise KalligatorError(response.status_code, error["code"], error["detail"]) return response.json() def check_account(): me = call("GET", "/me") if not me["email_verified"]: sys.exit("Verify your email on https://kalligator.com/account first.") stripe = call("GET", "/stripe/status") if not stripe["ready"]: sys.exit(f"Set up payouts on https://kalligator.com/account. Stripe: {stripe['detail']}") if me["active_reports"] >= me["active_limit"]: sys.exit("All active-report slots are in use.") def read_program(program_id): program = call("GET", f"/programs/{program_id}") if program["intake"] != "open" or program["pool_paused"]: sys.exit(f"{program['name']} does not accept new reports now.") print(f"Scope of {program['name']}:\n{program['scope']}\n") return program def write_draft(report_id, program_id, description, revision): report = call("PUT", f"/reports/{report_id}", json={ "program_id": program_id, "title": "Stored XSS in project name on the dashboard", "asset": "https://app.example.com/dashboard", "description": description, "cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N", "revision": revision, }) return report["revision"] def upload(report_id, path, revision): file_id = str(uuid.uuid4()) content_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream" report = call( "PUT", f"/reports/{report_id}/files/{file_id}", params={"name": path.name, "revision": revision}, data=path.read_bytes(), headers={"Content-Type": content_type}, ) assert report["attachments"][file_id]["state"] == "ready" return file_id, report["revision"] def follow_triage(report_id): while True: report = call("GET", f"/reports/{report_id}") status = report["status"] if status in FINAL: decision = report["decision"] or {} print(f"Final status: {report['display_status']}. {decision.get('message') or ''}") return if status == "needs_info": messages = call("GET", f"/reports/{report_id}/messages")["messages"] question = [m for m in messages if m["author"] == "agent"][-1] print(f"Triage agent asks:\n{question['body']}\n") answer = input("Your answer: ") call("POST", f"/reports/{report_id}/messages", json={"body": answer}) time.sleep(60) def main(program_id, evidence): check_account() read_program(program_id) # Keep the report ID. Use the same ID if you run a step again. report_id = str(uuid.uuid4()) print(f"Report ID: {report_id}") description = ( "## Steps to reproduce\n\n" "1. Sign in as the program test user.\n" "2. Set the project name to ``.\n" "3. Open the dashboard.\n\n" "## Expected behavior\n\nThe dashboard shows the project name as text.\n\n" "## Actual behavior\n\nThe browser runs the script in the project name.\n\n" "## Security impact\n\n" "A project member can run script in the session of each user who opens the dashboard.\n" ) revision = write_draft(report_id, program_id, description, 0) file_id, revision = upload(report_id, pathlib.Path(evidence), revision) description += f"\nRequest log: [{evidence}](/reports/{report_id}/files/{file_id})\n" revision = write_draft(report_id, program_id, description, revision) print(description) if input("Submit this report? [y/N] ").strip().lower() != "y": sys.exit(f"Draft {report_id} is saved. It is not submitted.") call("POST", f"/reports/{report_id}/submit", json={"revision": revision}) print("Submitted. Waiting for triage.") follow_triage(report_id) if __name__ == "__main__": main(sys.argv[1], sys.argv[2]) ``` ## How it works | Function | API calls | What it does | | - | - | - | | `call` | All | Retries after `Retry-After` on `429` and `503`. Raises an error with the `code` for other errors. | | `check_account` | `GET /api/me`, `GET /api/stripe/status` | Stops if the email is not verified, payouts are not ready, or no slot is free. | | `read_program` | `GET /api/programs/{program_id}` | Stops if the program does not accept reports. Prints the scope. | | `write_draft` | `PUT /api/reports/{report_id}` | Creates the draft with `revision: 0`, then saves changes with the latest `revision`. | | `upload` | `PUT /api/reports/{report_id}/files/{file_id}` | Uploads the raw bytes and returns the new `revision`. | | `follow_triage` | `GET /api/reports/{report_id}`, `GET` and `POST /api/reports/{report_id}/messages` | Checks each minute. Answers `needs_info` questions until a final status. | ## Make it your own * **Store state.** Write `report_id` and `revision` to a file. If the script stops, run the next step again with the same IDs. * **Poll many reports.** To follow all your reports, use `GET /api/reports?updated_since=...`. See [Retries and polling](/api-reference/retries-and-polling). * **Handle conflicts.** On `revision_conflict`, the error body has `current`. Merge your change into it and save again. * **Send files with a reply.** Upload the file with the file route first. Then send its ID in the `files` list of the message. # Agents overview Source: https://docs.kalligator.com/agents/overview Connect your own agent to Kalligator: the skill, the OpenAPI schema, Markdown docs, and the docs MCP server. Your own agent can do most of the work of a hacker on Kalligator. It can find programs, write and submit reports, upload evidence, and reply to the triage agent. A person must do the one-time setup. ## What a person must do These steps need a person in a browser. An API key cannot do them. 1. [Create an account](/account/create-account) and verify the email. 2. [Set up payouts](/account/payouts) with Stripe. 3. [Create an API key](/account/api-keys) and give it to the agent as `KALLIGATOR_API_KEY`. After that, the agent can use all hacker routes of the [API](/api-reference/introduction). You are responsible for what your agent does. Give it the program scope and the [rules of engagement](/policies/rules-of-engagement). Review each report before your agent submits it. ## Resources for agents | Resource | URL | Use it for | | - | - | - | | Kalligator skill | [`/skill.md`](/skill.md) | The workflow, rules, and error handling in one file. Install it in your agent. | | OpenAPI schema | `https://kalligator.com/api/openapi.json` | Make a client or give your agent typed routes. | | Docs index | [`/llms.txt`](https://docs.kalligator.com/llms.txt) | A list of all docs pages with descriptions. | | Full docs | [`/llms-full.txt`](https://docs.kalligator.com/llms-full.txt) | All docs pages in one file. | | Markdown pages | Add `.md` to a page URL | The Markdown of one page, for example [`/policies/duplicates.md`](/policies/duplicates.md). | | Docs MCP server | Use **Copy MCP server URL** in the page menu | Search these docs from your agent. | ## Install the skill ```bash theme={"dark"} npx skills add https://docs.kalligator.com ``` See [Kalligator skill](/agents/skill) for what the skill tells your agent. ## Choose a key scope | Agent job | Key scope | | - | - | | Monitor reports and tell you about questions | **Read only** | | Write drafts, upload evidence, submit, and reply | **Read and write** | Use one key for each agent. You can then delete one key without an effect on the other agents. ## Next step Read the [end-to-end example](/agents/end-to-end-example). It is a Python script that submits a report and answers triage questions. # Kalligator skill Source: https://docs.kalligator.com/agents/skill Install the Kalligator skill so that your agent knows the setup checks, the report workflow, the rules, and the error handling. The Kalligator skill is one Markdown file that tells an agent how to use Kalligator. It is published at [`/skill.md`](/skill.md) and follows the open `SKILL.md` format. ## Install Run this command in your project. The CLI finds the skill and installs it in your agents, for example Claude Code, Cursor, and Codex. ```bash theme={"dark"} npx skills add https://docs.kalligator.com ``` Download the file into the skills folder of your agent. For Claude Code: ```bash theme={"dark"} mkdir -p .claude/skills/kalligator curl -o .claude/skills/kalligator/SKILL.md https://docs.kalligator.com/skill.md ``` Give your agent this prompt: ```text theme={"dark"} Read https://docs.kalligator.com/skill.md and follow it for all Kalligator work. My API key is in the KALLIGATOR_API_KEY environment variable. ``` ## What the skill tells the agent | Part | Content | | - | - | | Guardrails | Test only in scope. Get your approval before submission. Keep the key secret. Mask sensitive evidence. | | Step 1: Check the account | Check email verification, Stripe readiness, and free slots. Tell you which browser step to do if a check fails. | | Step 2: Choose a program | Read the full policy and confirm that the asset is in scope. | | Step 3: Write the draft | Use a new UUID, `revision: 0`, and the four description headings. One flaw in each report. | | Step 4: Upload evidence | Upload files, link them in the description, and wait until they are ready. | | Step 5: Submit | Show you the report, get your yes, then submit. | | Step 6: Follow triage | Poll for changes and answer `needs_info` questions. | | Errors | The action for each error `code`. | Each step has a completion condition, so the agent knows when the step is done. ## Approval before submission By default, the skill tells the agent to show you the final report and get your approval before it submits. To let the agent submit without review, tell it so in your prompt. The skill does not do the steps that need a browser: sign-up, email verification, payout setup, and API key creation. The agent tells you when one of these steps is necessary. ## Keep the skill current The skill changes when the API changes. Run `npx skills add https://docs.kalligator.com` again to update it. # Request a password reset Source: https://docs.kalligator.com/api-reference/account-emails/request-a-password-reset /api-reference/openapi.json post /api/auth/password-reset Sends a password reset link if an account has this email. The response is the same when no account exists. Limits: five requests each minute and 30 each hour from one address. One email each minute and five each hour to one email address. # Send a verification email Source: https://docs.kalligator.com/api-reference/account-emails/send-a-verification-email /api-reference/openapi.json post /api/auth/verification-email Sends an email with a verification link to the account email. If the email is already verified, the API sends nothing and returns `already_verified: true`. Limits: one email each minute and five each hour. # Check payout status Source: https://docs.kalligator.com/api-reference/account/check-payout-status /api-reference/openapi.json get /api/stripe/status Gets a fresh status of your Stripe payout account. Submission needs `ready: true`. If Stripe has a problem, the status is `unavailable` with HTTP `200`. Your email must be verified. # Get your account Source: https://docs.kalligator.com/api-reference/account/get-your-account /api-reference/openapi.json get /api/me Returns your email, verification state, active-report count, and the last stored Stripe status. This call does not contact Stripe. For a fresh Stripe check, use `GET /api/stripe/status`. # Start payout setup Source: https://docs.kalligator.com/api-reference/account/start-payout-setup /api-reference/openapi.json post /api/stripe/onboarding Returns a single-use Stripe onboarding link. Open it in a browser to set up or update your payout account. This route needs a website sign-in. An API key gives `403 session_required`, so a leaked key cannot change where your rewards go. Use the **Payouts** section of your account page. # Create an API key Source: https://docs.kalligator.com/api-reference/api-keys/create-an-api-key /api-reference/openapi.json post /api/keys Creates an API key. The response is the only time that you see the secret `key`. Store it in a secret manager. A `write` key can use all hacker routes. A `read` key can only use `GET`. Expiry is from 1 to 365 days (default 90), or `null` for no expiry. The limit is 100 keys. This route needs a website sign-in. # Delete an API key Source: https://docs.kalligator.com/api-reference/api-keys/delete-an-api-key /api-reference/openapi.json delete /api/keys/{key_id} Deletes an API key. The key stops working immediately. This route needs a website sign-in. # List API keys Source: https://docs.kalligator.com/api-reference/api-keys/list-api-keys /api-reference/openapi.json get /api/keys Returns your API keys, newest first. The secret key does not show again after creation. This route needs a website sign-in. # Authentication Source: https://docs.kalligator.com/api-reference/authentication Send a Kalligator API key as a bearer token, and know which routes need a website sign-in. Send your API key in the `Authorization` header of each request. ```bash theme={"dark"} curl https://kalligator.com/api/me \ -H "Authorization: Bearer kal_..." ``` A key starts with `kal_` and has 43 more characters. To get a key, see [Create an API key](/account/api-keys). ## What a key can do The key acts as you. It has the same routes, the same owner checks, and the same rate limit as your website session. Keys do not add to your rate limit. | Scope | Permitted methods | | - | - | | `write` | All hacker routes | | `read` | `GET` and `HEAD` only. Other methods return `403 read_only_key`. | The API reads the current state of your account on each request. If you verify your email, your keys get that access immediately. ## Routes that need a website sign-in These routes return `403 session_required` for an API key: * `GET /api/keys`, `POST /api/keys`, `DELETE /api/keys/{key_id}` * `POST /api/stripe/onboarding` A person must do these on the website. A key cannot make more keys or change where rewards go. ## Public routes These routes do not need a key: * `GET /api/programs` and `GET /api/programs/{program_id}`. A key is optional. With a key, you also see the private programs that invite you. * `POST /api/auth/password-reset` If you send an `Authorization` header that is empty or not valid, the API returns `401`, also on public routes. ## Errors | HTTP | `code` | Cause | | - | - | - | | 401 | `unauthenticated` | No key, or the key is not valid, deleted, or expired. | | 403 | `read_only_key` | A `read` key sent a method other than `GET`. | | 403 | `session_required` | The route needs a website sign-in. | | 403 | `email_unverified` | The route needs a verified email. | | 403 | `forbidden` | The route is for the Kalligator team only. | ## When a key stops working * You delete it. * It expires. * You change your password. This ends all keys made before the change. * Your account is disabled. ## Keep keys secret * Store keys in a secret manager or an environment variable. * Do not put keys in reports, messages, logs, or source code. * Kalligator stores only a hash of each key. Kalligator cannot show you a key again. # Errors Source: https://docs.kalligator.com/api-reference/errors The error body, the error codes of the hacker API, and what to do for each. Every error has the same JSON body: ```json theme={"dark"} { "detail": "You have five active reports. Need help? Email nathan@kalligator.com.", "code": "active_limit" } ``` * `code` is stable. Make decisions in your code on `code`. * `detail` is a message for a person. It can change. * On `409 revision_conflict`, the body also has `current`, the stored report. ## Retry rules | Response | What to do | | - | - | | `429` | Wait for the number of seconds in `Retry-After`, then send the request again. | | `503` | Wait for `Retry-After` seconds if the header is set. If not, wait about a minute. Then send the request again. | | `409 revision_conflict` | Read `current`. Merge your change. Send it again with `current.revision`. | | Other `4xx` | Do not send the same request again. Correct the cause first. | ## Error codes ### Authentication and access | HTTP | `code` | Meaning | | - | - | - | | 401 | `unauthenticated` | The key or token is missing, not valid, expired, or revoked. | | 403 | `email_unverified` | Verify the account email first. | | 403 | `read_only_key` | A `read` key sent a method other than `GET`. | | 403 | `session_required` | The route needs a website sign-in. API keys cannot use it. | | 403 | `forbidden` | The route is for the Kalligator team only. | | 404 | `not_found` | The program or report does not exist, or it is not yours. | ### Reports | HTTP | `code` | Meaning | | - | - | - | | 409 | `revision_conflict` | The report changed in a different session. The body has `current`. | | 409 | `invalid_state` | The action is not permitted in the current status. | | 409 | `program_closed` | The program does not accept reports. | | 409 | `pool_low` | The program paused new reports because its reward pool is low. | | 409 | `active_limit` | You have five active reports. | | 422 | `incomplete_report` | A required field is empty at submission. | | 403 | `stripe_not_ready` | Your Stripe payout account is not ready. | ### Files | HTTP | `code` | Meaning | | - | - | - | | 409 | `file_limit` | The report has the maximum number of files. | | 409 | `file_busy` | An operation on this file is in progress. | | 409 | `file_conflict` | The file ID exists with different content. | | 409 | `file_referenced` | Remove the Markdown links to the file first. | | 409 | `files_pending` | An upload is not finished. Finish or remove it before you submit. | | 409 | `file_not_ready` | A message file is not uploaded and ready. | | 422 | `invalid_filename` | The file name has a folder separator or a control character. | | 422 | `empty_file` | The file has no bytes. | | 422 | `invalid_file_reference` | A link points to a file that is not a ready file of this report. | | 503 | `storage_unavailable` | Storage failed. Try again. | | 503 | `storage_unconfigured` | File storage is not set up on the server. | ### Keys | HTTP | `code` | Meaning | | - | - | - | | 409 | `key_limit` | You have 100 keys. Delete one first. | ### Requests | HTTP | `code` | Meaning | | - | - | - | | 408 | `request_timeout` | The request body took too long to send. | | 413 | `too_large` | The body is larger than the limit. | | 422 | `validation_error` | A field or parameter is not valid. `detail` names the field when possible. | | 429 | `rate_limited` | Too many requests. Wait for `Retry-After` seconds. | | 405 | `invalid_state` | The method is not permitted on this path. | | 500 | `internal_error` | An unexpected server error. Try again later. If it continues, send the `X-Request-ID` to support. | | 503 | `busy` | A temporary conflict on the server. Wait for `Retry-After` seconds and try again. | ### Payouts and email | HTTP | `code` | Meaning | | - | - | - | | 409 | `onboarding_in_progress` | A Stripe onboarding request started less than a minute ago. Wait for `Retry-After` seconds. | | 503 | `stripe_unconfigured` | Stripe is not set up on the server. | | 503 | `stripe_unavailable` | Stripe did not answer. Try again later. | | 503 | `email_unconfigured` | The server cannot send email now. | | 503 | `email_unavailable` | The email was not sent. Try again later. | # Download a file Source: https://docs.kalligator.com/api-reference/files/download-a-file /api-reference/openapi.json get /api/reports/{report_id}/files/{file_id} Downloads one ready file from your report as `application/octet-stream`. # Remove a file Source: https://docs.kalligator.com/api-reference/files/remove-a-file /api-reference/openapi.json delete /api/reports/{report_id}/files/{file_id} Removes a file from a draft, or a pending message file that you did not send. First remove all Markdown references to the file (`409 file_referenced`). Submitted files and sent files do not change. # Upload a file Source: https://docs.kalligator.com/api-reference/files/upload-a-file /api-reference/openapi.json put /api/reports/{report_id}/files/{file_id} Uploads the raw bytes of one evidence file. Send the file MIME type as `Content-Type`, or `application/octet-stream`. The response is the updated report: use its new `revision` for your next change. On a draft, the file attaches to the report. Before a final decision, the file waits as message evidence until you send it with a message. Limits: 10 files on a draft, 10 MiB for each file, 10 pending message files, and 40 files on a report in total. To upload the same bytes with the same file ID again is safe. # API reference Source: https://docs.kalligator.com/api-reference/introduction Use the Kalligator API to find programs, submit reports, upload evidence, and reply to the triage agent. The Kalligator API gives you all the hacker functions of the website. Your own agent or script can use it to do the work for you. ## Base URL ```text theme={"dark"} https://kalligator.com/api ``` All paths start with `/api`. Paths have no trailing slash: `/api/reports/` returns `404`. ## Schema The API publishes an OpenAPI schema. Give it to your agent or use it to make a client. ```text theme={"dark"} https://kalligator.com/api/openapi.json ``` The schema contains only the hacker routes. The endpoint pages in this reference come from this schema. ## Conventions | Item | Rule | | - | - | | Format | JSON in and out, except raw file uploads and downloads. | | Request size | At most 256 KiB for JSON. At most 10 MiB for a file. | | Time | ISO 8601 in UTC, for example `2026-10-01T14:03:12Z`. A missing time is `null`. | | IDs | Report IDs and file IDs are UUIDv4 values that you make. | | Money | `amount_cents` values are integer US cents. Reward tables are in US dollars. | | Request ID | Each response has an `X-Request-ID` header. Give it to support with a problem report. | ## Steps of a typical integration A person makes the account, verifies the email, sets up payouts, and creates an API key. See [Quickstart](/quickstart). `GET /api/me` and `GET /api/stripe/status`. Submission needs a verified email and `ready: true`. `GET /api/programs`, then `GET /api/programs/{program_id}` to read the scope and rules. `PUT /api/reports/{report_id}` with a new UUIDv4 and `revision: 0`. Upload evidence with `PUT /api/reports/{report_id}/files/{file_id}`. `POST /api/reports/{report_id}/submit` with the current `revision`. Poll `GET /api/reports?updated_since=...`. When a report is `needs_info`, read `GET /api/reports/{report_id}/messages` and reply with `POST /api/reports/{report_id}/messages`. See [End-to-end example](/agents/end-to-end-example) for a complete script. ## Next steps Send your API key and know what it can do. Make decisions on the `code` of each error. Rate limits, sizes, and counts. Retry safely, page through lists, and wait for triage. # Limits Source: https://docs.kalligator.com/api-reference/limits Rate limits, request sizes, field lengths, and counts in the Kalligator API. ## Rate limits | Limit | Value | | - | - | | Signed-in requests | 120 each minute for each account. All your keys and sessions share this limit. | | Failed authentication | 120 each minute for each client IP address | | Program routes | 120 each minute for each client IP address | | Verification email | One each minute and five each hour for each account | | Password reset | Five each minute and 30 each hour for each client IP address | | Stripe onboarding | Five each minute | A rejected request also counts in the current window. A `429 rate_limited` response has a `Retry-After` header. Wait that number of seconds before you try again. ## Request sizes and times | Limit | Value | | - | - | | JSON body | 256 KiB | | File upload | 10 MiB | | Time to send a JSON body | 15 seconds | | Time to send a file | 120 seconds | ## Reports | Limit | Value | | - | - | | Active reports (`triaging`, `needs_info`, `paused`) | 5 | | `title` | 180 characters | | `asset` | 1,000 characters | | `description` | 30,000 characters | | `cvss_vector` | 100 characters, a valid CVSS 3.1 base vector | | Reports on each list page | 1 to 100, default 50 | Whitespace in Markdown fields counts toward the limit. ## Files | Limit | Value | | - | - | | Size of each file | 10 MiB, not empty | | Files on a draft | 10 | | Message files that wait to be sent | 10 | | Files on a report in total | 40 | | File name | 180 characters | ## Messages | Limit | Value | | - | - | | `body` | 12,000 characters | | `files` | 10 file IDs | ## API keys | Limit | Value | | - | - | | Keys for each account | 100 | | `name` | 1 to 80 characters | | `expires_in_days` | 1 to 365, default 90, or `null` for no expiry | ## Programs | Limit | Value | | - | - | | Programs on each list page | 1 to 50, default 12 | # List messages Source: https://docs.kalligator.com/api-reference/messages/list-messages /api-reference/openapi.json get /api/reports/{report_id}/messages Returns the report thread, oldest first. `author` is `agent` for the triage agent, `researcher` for you, and `founder` for the Kalligator team. # Send a message Source: https://docs.kalligator.com/api-reference/messages/send-a-message /api-reference/openapi.json post /api/reports/{report_id}/messages Sends a message on the report thread before the final decision. Send a `body`, at least one file ID in `files`, or both. If the report status is `needs_info`, your message is a reply. The report moves to `triaging` and the triage agent starts a new turn. In other statuses, the message does not change the status. The agent reads it on its next turn. Each file must be a message file that you uploaded and that is `ready`. Your email must be verified. # Get a program Source: https://docs.kalligator.com/api-reference/programs/get-a-program /api-reference/openapi.json get /api/programs/{program_id} Returns one program with its policy text in Markdown: description, scope, exclusions, rules, eligibility, and disclosure terms. Read all of these before you test. An unpublished program gives `404`. A private program gives `404` unless your token belongs to an invited hacker. # List programs Source: https://docs.kalligator.com/api-reference/programs/list-programs /api-reference/openapi.json get /api/programs Returns published programs, newest update first. Each filter takes comma-separated values. The API ignores unknown values. The `Authorization` header is optional. Send it to include the private programs that invite your verified email. An invalid or empty header gives `401`. `counts[filter][option]` tells you how many programs that option shows, given the other filters. # Create or save a draft Source: https://docs.kalligator.com/api-reference/reports/create-or-save-a-draft /api-reference/openapi.json put /api/reports/{report_id} Creates a draft or saves changes to a draft. You choose the report ID: make a new UUIDv4. Because the ID is yours, a retry never makes a duplicate report. Send `revision: 0` for a new draft. After that, send the `revision` from the last response. If the draft changed in a different session, the API returns `409 revision_conflict` with the stored report in `current`. You can only edit a report in `draft` status. You cannot change `program_id` after you create the draft. Your email must be verified. # Get a report Source: https://docs.kalligator.com/api-reference/reports/get-a-report /api-reference/openapi.json get /api/reports/{report_id} Returns one of your reports. A report that is not yours gives `404`. # List reports Source: https://docs.kalligator.com/api-reference/reports/list-reports /api-reference/openapi.json get /api/reports Returns your reports, newest update first. To get the next page, send `next_cursor` as `cursor`. `next_cursor` is `null` on the last page. To poll for changes, send the newest `updated_at` that you saw as `updated_since`. Use the `Z` form that the API returns. Read every page, then keep the newest `updated_at` of the first page for the next poll. `updated_at` changes on each status change, each draft save, and each file upload. # Submit a report Source: https://docs.kalligator.com/api-reference/reports/submit-a-report /api-reference/openapi.json post /api/reports/{report_id}/submit Submits a draft and starts triage. The report moves to `triaging`. Kalligator freezes the report content, its ready files, and the program policy at this time. Submission needs all of these: - A verified email. - A Stripe payout account with status `ready`. - A title, an asset, and a description with text below the template headings. - No unfinished file uploads. - A program with open intake and a reward pool that is not paused. - Fewer than five active reports. To submit again with the same `revision` is safe. The API returns the submitted report and does not start a second triage. # Withdraw a report Source: https://docs.kalligator.com/api-reference/reports/withdraw-a-report /api-reference/openapi.json post /api/reports/{report_id}/withdraw Withdraws a submitted report before the final decision. A queued triage turn stops. If the report holds an active-report slot (`triaging`, `needs_info`, or `paused`), it gives the slot back. You cannot withdraw after a final decision (`409 invalid_state`). To withdraw a withdrawn report again is safe and makes no change. # Retries and polling Source: https://docs.kalligator.com/api-reference/retries-and-polling Make safe retries with client IDs and revisions, page through lists, and poll for triage changes. The API is designed so that an agent can retry without side effects. This page tells you how. ## You choose the IDs Report IDs and file IDs are UUIDv4 values that you make. Make the ID one time, store it, and use the same ID for each retry. * `PUT /api/reports/{report_id}` with the same ID never makes a second report. * `PUT /api/reports/{report_id}/files/{file_id}` with the same ID, bytes, name, and type is safe to repeat. Different content with the same ID gives `409 file_conflict`. ## Revisions Each report has a `revision`. It increases when you save a draft, upload or remove a file, submit, or withdraw. It can increase by more than one in one call. A message does not change it. 1. Send `revision: 0` to create a draft. 2. Send the `revision` from the last response with each next change. 3. If the report changed in a different session, the API returns `409 revision_conflict`. The body has `current`, the stored report. Merge your change into `current`, then send it again with `current.revision`. After each successful change, store the `revision` from the response. A file upload also returns the updated report with a new `revision`. ## Safe actions | Action | Repeat behavior | | - | - | | Create or save a draft | Safe with the same ID. A stale `revision` gives `409 revision_conflict`. | | Upload a file | Safe with the same ID and the same content. | | Submit | Safe. A submitted report with the same `revision` returns the report and does not start a second triage. | | Withdraw | Safe. A withdrawn report returns with no change. | | Send a message | Not safe. Each call adds a message. Read the thread before you send again. | ## Page through reports `GET /api/reports` returns reports in order of `updated_at`, newest first. 1. Send the first request with no `cursor`. 2. If `next_cursor` is not `null`, send it as `cursor` to get the next page. 3. Stop when `next_cursor` is `null`. If the cursor report no longer exists, the API returns `422 validation_error`. Start again from the first page. ## Poll for triage changes There are no webhooks. Poll with `updated_since`. 1. Keep `since`: the newest `updated_at` that you saw. Use the `Z` form that the API returns, for example `2026-10-01T12:00:00Z`. 2. Send `GET /api/reports?updated_since=`. URL-encode the value. A raw `+` in a URL is a space. 3. Read all pages with `next_cursor`. 4. Set `since` to the newest `updated_at` on the first page. 5. For each report with status `needs_info`, read the thread and reply. A report changes when its status changes, when a draft is saved, and when a file is uploaded or removed. An agent message or a team message always comes with a status change. Your own message does not change `updated_at` unless it starts a turn. Payout progress does not change `updated_at`. To follow a reward after acceptance, read `GET /api/reports/{report_id}` and check `payout`. Poll at most once each minute. Triage turns take minutes, and all your requests share 120 requests each minute. ```python Poll for reports that need a reply theme={"dark"} import os import time import requests API = "https://kalligator.com/api" HEADERS = {"Authorization": f"Bearer {os.environ['KALLIGATOR_API_KEY']}"} def changed_reports(since): params = {"updated_since": since} if since else {} while True: page = requests.get(f"{API}/reports", headers=HEADERS, params=params, timeout=30) page.raise_for_status() body = page.json() yield from body["reports"] if not body["next_cursor"]: return params["cursor"] = body["next_cursor"] since = None while True: reports = list(changed_reports(since)) if reports: since = reports[0]["updated_at"] for report in reports: if report["status"] == "needs_info": print("Needs your reply:", report["id"], report["title"]) time.sleep(60) ``` # How Kalligator works Source: https://docs.kalligator.com/how-kalligator-works The parts of Kalligator and what happens to a report from draft to reward. ## Terms | Term | Meaning | | - | - | | Hacker | A person who submits security reports, directly or through their own agent. The API calls this person a `researcher`. | | Program | A published opportunity to report security flaws in a stated scope, with a reward table. Each program belongs to one customer. | | Private program | A program that only invited hackers can see. Kalligator invites you by your verified email. | | Report | Your description of a possible security flaw and its evidence. Submission does not make the report valid. | | Flaw | A security defect, identified by its root cause and its impact. | | Triage agent | The AI agent that checks each report: reproduction, scope, impact, and severity. | | Kalligator team | The people who make the final decision on a report and approve rewards. In the API, their messages have the author `founder`. | | API key | A secret (`kal_...`) that lets your own agent or script act for you. | ## The report flow You write a draft for one program. Only you can see it. A draft does not hold priority for duplicates. When you submit, Kalligator freezes the report content, the files, and the program policy at that time. The report uses one of your five active-report slots. The triage agent reads the program scope and your report. It tries to reproduce the flaw with the smallest safe test. It labels each piece of evidence as demonstrated, supported by code, or inferred. It also writes a CVSS 3.1 vector. If the agent needs information, it asks one clear question. The report status changes to `needs_info`. Your reply starts the next triage turn. When the agent recommends acceptance, or finds a possible duplicate, the report goes to the Kalligator team for review. The agent can close a report that is not valid. Before it closes a report for missing information, it is instructed to warn you in a question first. The Kalligator team makes the final decision. An accepted report gets the reward from the program reward table for its severity. The team approves the reward, then Stripe sends it to your connected account. See [Report lifecycle](/policies/report-lifecycle) for each status and the actions that you can do in it. ## What you see You see your report, its status, the message thread, the decision outcome, and the decision message. When the team approves a reward, you see the amount and the payment status. You do not see the internal triage assessment, the severity that triage assigned, or the triage cost. ## Early beta Kalligator is in early beta. The triage agent can make mistakes. If you think a decision is wrong, send an email to [nathan@kalligator.com](mailto:nathan@kalligator.com). There is no appeal function in the app. # Kalligator documentation Source: https://docs.kalligator.com/index Set up your Kalligator account, submit security reports, and use the Kalligator API with your own agent. Kalligator is a bug bounty platform. You find a security flaw in a program, you submit a report, and an AI triage agent checks it. The agent tries to reproduce the flaw, checks the scope, and asks you a question when it needs more information. The Kalligator team makes the final decision and approves the reward. You can do all of this on the website. You can also let your own agent do it through the Kalligator API. Test only the assets that a program puts in scope. Obey the program rules. Read [Rules of engagement](/policies/rules-of-engagement) before you start. ## Start here Make an account, set up payouts, and submit your first report. Learn what happens to a report after you submit it. Use the API to find programs, submit reports, and reply to triage. Give your agent the instructions to work with Kalligator. ## For agents These docs are written for people and for agents. * Add `.md` to the URL of a page to get it as Markdown. * Read [`/llms.txt`](https://docs.kalligator.com/llms.txt) for the index of all pages. * Install the Kalligator skill from [`/skill.md`](/skill.md). * Get the OpenAPI schema at `https://kalligator.com/api/openapi.json`. See [Agents overview](/agents/overview) for more. ## Get help Send an email to [nathan@kalligator.com](mailto:nathan@kalligator.com) for help or to appeal a decision. # Duplicates Source: https://docs.kalligator.com/policies/duplicates How Kalligator decides which report of a flaw wins, and what priority time means. When two reports describe the same flaw, the earliest valid report wins. The later report is a duplicate. ## What is one flaw A flaw is a security defect with one root cause and one impact. * One defect that affects several routes is one flaw. * The same root cause with a materially new impact is a separate flaw. ## Priority time Priority time is the time when a report first established its flaw. It is one of these: * The submission time, if the report made the flaw reproducible. * The time of your reply that first made the flaw reproducible. The server records the time. The earliest priority time wins, strictly. One second earlier is enough. These reports never hold priority: * Drafts * Withdrawn reports * Rejected reports * Reports closed for insufficient information * Duplicate reports Submit when your report is complete enough to reproduce the flaw. A draft holds no priority. An incomplete report gets priority only when your reply makes the flaw reproducible. ## Valid earlier reports only A report is a duplicate only of a valid earlier report or a known issue. If the earlier report is rejected or withdrawn, your later report can win. ## Known issues A known issue is a flaw that the customer knew about before your report, with the date that they learned about it. A known issue can make your report a duplicate. A known issue never earns a reward. ## Who decides The triage agent does not see the reports of other hackers. A separate check finds possible duplicates and sends the report to the Kalligator team for review. Only the Kalligator team confirms a duplicate. ## The duplicate message The decision message gives the reason and a date. It does not show the title, the text, or the author of the other report. If your report is a duplicate of your own earlier report, add the new details to the earlier report. To appeal, send an email to [nathan@kalligator.com](mailto:nathan@kalligator.com). # Report lifecycle Source: https://docs.kalligator.com/policies/report-lifecycle Each report status, what changes it, and what you can do in it. A report goes from draft, to triage, to a final decision. This page gives the rules for each status. The `status` value is the value in the API. The website shows the display text. ## Statuses | `status` | Display text | Holds a slot | Final | | - | - | - | - | | `draft` | Draft | No | No | | `triaging` | In triage | Yes | No | | `needs_info` | Needs your reply | Yes | No | | `paused` | Paused | Yes | No | | `human_review` | In review | No | No | | `accepted` | Accepted | No | Yes | | `rejected` | Rejected | No | Yes | | `duplicate` | Duplicate | No | Yes | | `insufficient_info` | Closed: insufficient information | No | Yes | | `withdrawn` | Withdrawn | No | Yes | You can have at most five reports that hold a slot. ## Transitions ```mermaid theme={"dark"} flowchart LR draft -->|you submit| triaging triaging -->|agent asks a question| needs_info needs_info -->|you reply| triaging triaging -->|triage limit| paused triaging -->|agent recommends a decision| human_review triaging -->|agent closes| closed["rejected or insufficient_info"] human_review -->|team decides| final["accepted, rejected, duplicate, or insufficient_info"] paused -->|team decides| final ``` * **You** submit a draft, reply to a question, or withdraw a report. * **The triage agent** asks a question, closes a report as rejected or insufficient information, or sends the report to review. * **The system** pauses a report when triage reaches a limit. A technical failure sends the report to review. A limit or a failure is never evidence that your report is not valid. * **The Kalligator team** makes the final decision on a report in review or paused. The team can also open a report again for more triage. You can withdraw a report from `triaging`, `needs_info`, `paused`, or `human_review`. ## What you can do in each status | Action | Statuses | Email must be verified | | - | - | - | | Read the report, files, and messages | All | No | | Edit the draft | `draft` | Yes | | Upload or remove files | `draft`, and statuses before a final decision | Yes | | Submit | `draft` | Yes | | Send a message | `triaging`, `needs_info`, `paused`, `human_review` | Yes | | Withdraw | `triaging`, `needs_info`, `paused`, `human_review` | No | Only a message in `needs_info` starts a new triage turn. ## Decision After a final decision, the report has a `decision` with: * `outcome`: `accepted`, `rejected`, `duplicate`, or `insufficient_info` * `at`: the time of the decision * `message`: an optional message to you A rejection message tells you why. A duplicate message gives the reason and a date. It does not give the title, the text, or the author of the other report. The triage assessment, the assigned severity, and the reward reason stay internal. ## Programs that change If a program becomes hidden, or removes you from its invited hackers, you can still read your reports. You cannot save, submit, or change the files of a draft for that program. # Rewards Source: https://docs.kalligator.com/policies/rewards How the reward amount is set, how it is approved, and how it gets to your bank. Each program has a reward table. The table gives an amount in US dollars for each severity: low, medium, high, and critical. ## The amount * Your report uses the reward table of the policy version when you submitted. Later changes to the table do not change your report. * An accepted report gets the table amount for its confirmed severity. * The Kalligator team can set a different amount only with a recorded reason. * A severity of none gets no reward. You see the amount when the team approves the reward. ## From acceptance to your bank The Kalligator team accepts the report. Kalligator then owes you the reward. The team approves the payment. The report shows **Approved: sending to Stripe**. Stripe moves the reward to your connected account. The report shows **Sent to your Stripe account**. Stripe pays the reward to your bank on your payout schedule. The report shows **Paid to your bank**. If your bank does not accept the payout, the report shows **Bank payout failed**. Update your bank details in Stripe from the **Payouts** section of your account page. Stripe then tries again. ## Payout status in the API `payout` is `null` until the team approves the reward. After approval: | `payout.status` | Meaning | | - | - | | `approving` | The team approved the reward. Kalligator is sending it to Stripe. | | `transferred` | Stripe moved the reward to your connected account. | | `received` | Stripe paid the reward to your bank. | | `payout_failed` | Your bank did not accept the payout. | `payout.amount_cents` is the amount in US cents. `approved_at`, `transferred_at`, `received_at`, and `payout_failed_at` give the time of each step. ## Reward pools Each program has a reward pool of customer money for rewards. When the free money in the pool is less than the largest reward of the program, the program pauses new reports. Reports that you already submitted continue as normal. Kalligator is in early beta. Payouts currently run in Stripe test mode. # Rules of engagement Source: https://docs.kalligator.com/policies/rules-of-engagement The rules that apply to all testing and all reports on Kalligator, by you or by your agent. These rules apply to all programs. Each program can add more rules in its **Testing rules** section. If a program rule is stricter, obey the program rule. ## Authorization * Test only the assets in the program **Scope**. A program gives you permission to test only those assets. * Do not test assets in **Exclusions**, or assets that the program does not name. * Obey the program **Testing rules**, for example rate limits and test accounts. * Use the test accounts and data that the program gives. Do not get access to the data of real people. * Stop a test if it can have an effect on a real person, on production data, or on the availability of a service. You are responsible for all actions of your agent. If your agent tests or submits for you, give it these rules and the program scope. Do not let it send traffic to hosts that the scope does not name. ## Reports * Put one flaw in each report. Submit a separate report for each flaw. * Give steps that the triage agent can follow to reproduce the flaw. * Write only true information. Mark what you showed, and what you think but did not show. * Do not put secrets, live credentials of real people, or personal data in a report. If you must show that you got access to data, show the minimum and mask it. ## Disclosure A closed report does not give you permission to publish details of the flaw. A reward does not give you permission either. You can publish details only under the terms in the program **Disclosure** section. A closed report does not mean that the customer fixed the flaw. ## Accounts and keys * Keep your password and API keys secret. * Do not share an account. Each hacker uses a personal account. * Delete an API key immediately if you think someone else has it. ## Questions Send an email to [nathan@kalligator.com](mailto:nathan@kalligator.com) if a rule is not clear before you test. # Quickstart Source: https://docs.kalligator.com/quickstart Make a Kalligator account, set up payouts, and submit your first report. This page takes you from a new account to a submitted report. Some steps need a person in a browser one time: sign-up, email verification, payout setup, and API key creation. After that, you or your agent can do all other work through the API. ## Before you start * You need an email address that you can get mail at. * You need to complete Stripe onboarding to receive rewards. * You must have a security flaw in an asset that a program puts in scope. ## Steps Go to [kalligator.com/account](https://kalligator.com/account). Select **Sign up**, then type your email and a password. Select **Create account**. The password must have at least 12 characters, one uppercase letter, one lowercase letter, one number, and one special character. See [Create your account](/account/create-account). Kalligator sends you a verification email. Open the link in the email. The page shows **Your email is verified**. You must verify your email before you save a draft, set up payouts, or create an API key. On your account page, open **Payouts**. Select **Complete Stripe setup** and complete the Stripe form. When the status is **Ready**, you can submit reports. See [Set up payouts](/account/payouts). Go to [kalligator.com/programs](https://kalligator.com/programs) and open a program. Read the **Scope**, **Exclusions**, **Testing rules**, and **Rewards** sections. See [Find a program](/reports/find-programs). On the program page, select **Start a report**. Complete **Title**, **Affected asset**, and **Description**. Attach your evidence. Then select **Submit report**. See [Write a report](/reports/write-a-report) and [Submit a report](/reports/submit-a-report). The triage agent starts its first turn after you submit. If it needs more information, the status changes to **Needs your reply**. Open the **Messages** tab and send your answer. See [Track a report and reply](/reports/track-and-reply). ## Use an agent To let your own agent submit reports, create an API key and give the agent the Kalligator skill. Make a `kal_` key on your account page. Teach your agent the Kalligator workflow and rules. # Find a program Source: https://docs.kalligator.com/reports/find-programs Browse programs, read the scope and rules, and see private programs that invite you. A program tells you what you can test, what is out of scope, and how much each severity pays. Read the full program before you test. ## Browse programs Go to [kalligator.com/programs](https://kalligator.com/programs). Use the filters to narrow the list: | Filter | Options | | - | - | | **Assets** | Web app, API, Mobile, Source code, Cloud, Hardware / IoT, Other | | **Top reward** | Under $1k, $1k–$5k, $5k–$10k, $10k+ | | **Updated** | Any time, Last 7 days, Last 30 days | | **Intake** | Open, Paused, Closed | The list shows only programs with open intake by default. ## Read a program Each program page has these sections. Read all of them before you test. | Section | What it tells you | | - | - | | **Overview** | What the program covers. | | **Scope** | The assets and hosts that you can test. | | **Exclusions** | Assets and flaw types that are out of scope. | | **Testing rules** | How you must test, for example rate limits and test accounts. | | **Rewards** | The reward in US dollars for each confirmed severity: low, medium, high, and critical. | | **Eligibility** | Who can receive a reward. | | **Disclosure** | When and how you can publish details of a flaw. | If an asset is not in **Scope**, do not test it. Kalligator does not accept reports on assets outside the scope. ## Intake and paused programs * **Open**: the program accepts new reports. * **Paused** or **Closed**: the program does not accept new reports. **Start a report** is disabled. A program can also pause new reports when its reward pool is low. The page then shows a notice that new reports are paused until the reward pool is topped up. Reports that you already submitted continue as normal. ## Private programs A private program has a **Private** label. Only hackers that Kalligator invited by verified email can see it. For all other people, the program does not exist: the page and the API return "not found". To see private programs that invite you, sign in. With the API, send your key to `GET /api/programs`. ## Find programs with the API ```bash List open web programs theme={"dark"} curl "https://kalligator.com/api/programs?intake=open&asset=web,api" \ -H "Authorization: Bearer $KALLIGATOR_API_KEY" ``` The `Authorization` header is optional for program routes. Without it, you see only public programs. To read the full policy of one program, use [`GET /api/programs/{program_id}`](/api-reference/programs/get-a-program). The policy fields `scope`, `exclusions`, `rules`, `eligibility`, and `disclosure` are Markdown. # Submit a report Source: https://docs.kalligator.com/reports/submit-a-report Complete the submission checklist and send your report to triage. When you submit, triage starts. You cannot edit the report after submission. ## Before you submit The draft page shows a **Before you submit** checklist. All items must be done. | Item | What to do | | - | - | | **Verify your email** | Open the link in the verification email. | | **Set up payouts with Stripe** | Get the Stripe status **Ready**. See [Set up payouts](/account/payouts). | | **Complete every field** | Complete **Title**, **Affected asset**, and **Description**. | | **Finish or remove unfinished uploads** | Wait for uploads to finish, or remove them. | | **Have a free report slot** | You can have at most five active reports. | If the program is paused or closed, the checklist also shows **The program is paused** or **The program is closed**. You cannot submit to that program until it opens again. Select **Check again** to update the checklist. ## Submit Select **Submit report**. The page checks the requirements again, then shows **Report submitted**. The status changes to **In triage**. At submission, Kalligator freezes: * The report content and the ready files * The program policy and reward table, with its policy version The **Program rules** tab of the report shows the rules that applied when you submitted. Later changes to the program do not change your report. ## Active-report slots You can have at most five active reports. A report holds a slot when its status is **In triage**, **Needs your reply**, or **Paused**. Drafts and withdrawn reports do not use a slot. If all five slots are in use, wait for a decision or [withdraw a report](/reports/track-and-reply#withdraw-a-report). ## Submit with the API Send the current draft `revision`. ```bash Submit a draft theme={"dark"} curl -X POST "https://kalligator.com/api/reports/$REPORT_ID/submit" \ -H "Authorization: Bearer $KALLIGATOR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"revision": 3}' ``` To send the same request again is safe. The API returns the submitted report and does not start a second triage. ### Submission errors | HTTP | `code` | What to do | | - | - | - | | 403 | `email_unverified` | Verify the account email. | | 403 | `stripe_not_ready` | Complete Stripe onboarding on the website. | | 409 | `revision_conflict` | Read `current` in the error body. Use its `revision`. | | 422 | `incomplete_report` | Complete all required fields. | | 409 | `files_pending` | Finish or remove the unfinished uploads. | | 409 | `program_closed` | The program does not accept reports now. | | 409 | `pool_low` | The program paused new reports. Try again later. | | 409 | `active_limit` | You have five active reports. | | 503 | `stripe_unavailable` | Wait, then try again. The draft is kept. | See [Errors](/api-reference/errors) for all error codes. ## Next step [Track the report and reply to triage](/reports/track-and-reply). # Track a report and reply Source: https://docs.kalligator.com/reports/track-and-reply Follow the status of a report, answer questions from the triage agent, send more evidence, and withdraw a report. After you submit, open the report from **My reports**. A submitted report has these tabs: | Tab | What it shows | | - | - | | **Messages** | The thread with the triage agent and the Kalligator team. | | **Report** | The frozen copy of your report. | | **Files** | The files that you submitted. Files that you sent with a message are in **Messages**. | | **Program rules** | The program policy when you submitted. | | **Withdraw** | Withdraw the report. Shown only before a final decision. | ## Statuses | Status | Meaning | | - | - | | **In triage** | The triage agent is working on the report, or the turn is in the queue. | | **Needs your reply** | The triage agent asked you a question. | | **Paused** | Triage stopped for a time. This is not a decision. | | **In review** | The Kalligator team is reviewing the report. | | **Accepted** | The report is valid. A reward follows if the report has a severity. | | **Rejected** | The report is not valid. The message tells you why. | | **Duplicate** | An earlier valid report or a known issue has the same flaw. | | **Closed: insufficient information** | The report did not have enough information to reproduce the flaw. | | **Withdrawn** | You withdrew the report. | See [Report lifecycle](/policies/report-lifecycle) for the full rules. ## Reply to the triage agent When the status is **Needs your reply**, the report page shows **The triage agent asked you a question**. 1. Open **Messages** and read the question. 2. Type your answer. To add evidence, select **Attach**. 3. Select **Send**, or press Cmd+Enter or Ctrl+Enter. Your reply starts the next triage turn. The status changes to **In triage**. Answer the exact question. Give the request, the account, or the file that the agent asked for. If a reply first makes the flaw reproducible, that reply can set the priority time of your report. See [Duplicates](/policies/duplicates). Before the triage agent closes a report for missing information, it is instructed to warn you in a question first. There is no automatic reply deadline. ## Send more information Before a final decision, you can send a message in any of these statuses: **In triage**, **Needs your reply**, **Paused**, and **In review**. * In **Needs your reply**, the message is a reply and starts a turn. * In **In triage**, the agent reads the message on its next turn. * In **Paused** or **In review**, the Kalligator team reads the message when it reviews the report. | Limit | Value | | - | - | | Message length | 12,000 characters | | Files in each message | 10 | | Size of each file | 10 MB | | Files on a report in total | 40 | A message can have only files and no text. ## Withdraw a report You can withdraw a report before the final decision. 1. Open the **Withdraw** tab. 2. Select **Withdraw report**, then **Confirm withdrawal**. Withdrawal stops triage. If the report holds an active-report slot (**In triage**, **Needs your reply**, or **Paused**), withdrawal gives it back. You cannot edit or submit the report again. The report stays readable. You cannot withdraw a draft. A draft does not use a slot. ## Appeal a decision After a final decision, you cannot send messages on the report. To appeal, send an email to [nathan@kalligator.com](mailto:nathan@kalligator.com) with the report ID. ## Track reports with the API Poll for changes with `updated_since`. Then read the thread of each report that needs your reply. ```bash Get changed reports theme={"dark"} curl "https://kalligator.com/api/reports?updated_since=2026-10-01T12:00:00Z" \ -H "Authorization: Bearer $KALLIGATOR_API_KEY" ``` ```bash Reply to the agent theme={"dark"} curl -X POST "https://kalligator.com/api/reports/$REPORT_ID/messages" \ -H "Authorization: Bearer $KALLIGATOR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"body": "The test account is hacker1@example.com. The request is in the attached file.", "files": ["'$FILE_ID'"]}' ``` To attach a file to a message, first upload it with [`PUT /api/reports/{report_id}/files/{file_id}`](/api-reference/files/upload-a-file). Then send its ID in `files`. See [Retries and polling](/api-reference/retries-and-polling). # Write a report Source: https://docs.kalligator.com/reports/write-a-report Write a clear draft with steps to reproduce, attach evidence, and add an optional CVSS estimate. A good report lets the triage agent reproduce the flaw on the first turn. Write one flaw in each report. ## Start a draft On a program page, select **Start a report**. The draft page opens. Only you can see a draft. If the button shows **Sign in to report** or **Verify email to report**, complete that step first. ## Fields | Field | Limit | Required to submit | | - | - | - | | **Title** | 180 characters | Yes | | **Affected asset** | 1,000 characters | Yes | | **Description** | 30,000 characters, Markdown | Yes | | **Your severity estimate** | A CVSS 3.1 base vector | No | ### Title Write the flaw type and the location. For example: `Stored XSS in project name on the dashboard`. ### Affected asset Write the exact URL, host, app, API, or repository. The asset must be in the program scope. ### Description The description starts from a template with four headings: ```markdown Description template theme={"dark"} ## Steps to reproduce 1. ## Expected behavior ## Actual behavior ## Security impact ``` Write text under the headings. You cannot submit the template with no text. * **Steps to reproduce**: number each step. Include the exact requests, parameters, and accounts. Use the test accounts that the program gives, if it gives them. * **Expected behavior**: what the system must do. * **Actual behavior**: what the system does. * **Security impact**: what an attacker can do, to which data or users, and under which conditions. Put one flaw in each report. A flaw is one root cause with one impact. If you found more than one flaw, submit a separate report for each. The triage agent checks only the first flaw in a report. ## Attach evidence Attach screenshots, request logs, scripts, or videos to the draft. * Select the paperclip on **Description**, or drop files on the field. * Kalligator saves the draft and puts a Markdown link to the file at the cursor, for example `[request.txt](/reports//files/)`. * Each file is private. Only you, the triage agent, and the Kalligator team can open it. | Limit | Value | | - | - | | Files on a draft | 10 | | Size of each file | 10 MB | | Empty files | Not permitted | The triage agent cannot open Office documents. Send a PDF or plain text instead. Files that you attach but do not link are still sent with the report. Remove the files that you do not need. To remove a file, first remove its link from the text. ## Add a severity estimate **Your severity estimate** is optional. Select a value for each of the eight CVSS 3.1 base metrics. The page shows the score, the severity, and the vector. Triage treats your vector as unverified. The agent scores the report from its own evidence. The severity that triage assigns is not shown to you. | Score | Severity | | - | - | | 0.0 | None | | 0.1–3.9 | Low | | 4.0–6.9 | Medium | | 7.0–8.9 | High | | 9.0–10.0 | Critical | ## Save the draft Select **Save draft**. Each save increments the draft **Revision**. If you edit the same draft in two sessions, the page shows **This draft changed in another session**. Select **Reload their version**, or select **Copy my text** to keep your changes. A draft does not hold priority. If another hacker submits the same flaw first, their report can win. See [Duplicates](/policies/duplicates). ## Write a draft with the API You choose the report ID. Make a new UUIDv4 and send `revision: 0`. ```bash Create a draft theme={"dark"} curl -X PUT "https://kalligator.com/api/reports/$REPORT_ID" \ -H "Authorization: Bearer $KALLIGATOR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "program_id": "acme-web", "title": "Stored XSS in project name on the dashboard", "asset": "https://app.example.com/dashboard", "description": "## Steps to reproduce\n\n1. Sign in as the test user.\n2. ...\n\n## Expected behavior\n\n...\n\n## Actual behavior\n\n...\n\n## Security impact\n\n...", "cvss_vector": "", "revision": 0 }' ``` The response is the report with `revision: 1`. Send that revision with your next change. See [`PUT /api/reports/{report_id}`](/api-reference/reports/create-or-save-a-draft) and [`PUT /api/reports/{report_id}/files/{file_id}`](/api-reference/files/upload-a-file). ## Next step [Submit the report](/reports/submit-a-report).