> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tesslate.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API Keys

> Two kinds of keys in OpenSail: external keys for invoking agents from outside, and BYOK provider keys for model routing. Both encrypted at rest.

<img src="https://mintcdn.com/tesslate/VT6tbZolrCfpx26M/images/opensail-banner.png?fit=max&auto=format&n=VT6tbZolrCfpx26M&q=85&s=66579c47537b3464fb65b229cc9ab0fd" alt="Tesslate OpenSail" width="4712" height="1612" data-path="images/opensail-banner.png" />

## Two kinds of keys

OpenSail uses two distinct kinds of API keys, and it's important to know which one you need.

<CardGroup cols={2}>
  <Card title="External API keys" icon="key">
    For calling OpenSail agents from outside the platform: Slack bots, CI pipelines, cron jobs, external integrations. `tsk_` prefixed. Created in Settings.
  </Card>

  <Card title="BYOK provider keys" icon="microchip">
    For using your own OpenAI, Anthropic, OpenRouter, or other provider keys instead of platform credits. Encrypted at rest. Pro and Ultra only.
  </Card>
</CardGroup>

This page covers both. If you're wiring up a Slack bot, you want external API keys. If you're trying to bypass platform credit costs by using your own OpenAI key, you want BYOK. For BYOK specifics, see `/guides/model-management`.

## External API keys

External keys let outside systems invoke OpenSail agents through a REST API. The canonical flow: a Slack bot mentions your agent, a CI job kicks off a code review, a cron schedule fires a weekly report, an external coding agent (Cursor, Claude Code) reaches into your OpenSail instance for sandboxed compute.

### Key format

* Prefix: `tsk_`
* Body: 32 random hex characters
* Example: `tsk_8f3a1b2c4d5e6f7a8b9c0d1e2f3a4b5c`

### How storage works

Raw keys are never stored. When you create a key:

1. The server generates 32 random hex characters
2. Prepends `tsk_` to form the full token
3. Computes SHA-256 of the full token
4. Stores only the hash plus a 4-character prefix for identification
5. Returns the raw key to you **once**

You cannot retrieve the raw key later. If you lose it, rotate it.

### Creating a key

<Steps>
  <Step title="Open Settings">
    Settings, then the API Keys section. You'll see tabs for external keys and BYOK keys.
  </Step>

  <Step title="Create External Key">
    Click Create Key. Give it a human-readable name ("Slack Bot", "CI Deploy Hook").
  </Step>

  <Step title="Set scope (optional)">
    Optionally restrict the key to a single project by setting a `project_id`. Scoped keys can only invoke agents on that project. Unscoped keys can access any project you own.
  </Step>

  <Step title="Set expiration (optional)">
    Optionally set `expires_at`. After that timestamp, the key stops authenticating.
  </Step>

  <Step title="Copy the key">
    The raw key is shown one time. Copy it into your secret manager or the integration you're wiring up. You cannot view it again.
  </Step>
</Steps>

<Warning>
  Treat `tsk_` keys like passwords. Never commit them to git. Use your platform's secret store (env vars, Vault, AWS Secrets Manager, etc.).
</Warning>

### Using a key

Pass the raw key as a Bearer token:

```
Authorization: Bearer tsk_8f3a1b2c4d5e6f7a8b9c0d1e2f3a4b5c
```

The most common endpoint is the agent invoke endpoint:

```bash theme={null}
curl -X POST https://your-opensail.example.com/api/external/agent/invoke \
  -H "Authorization: Bearer tsk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "project_id": "550e8400-e29b-41d4-a716-446655440000",
    "message": "Review the open PR and post a summary in Slack",
    "webhook_url": "https://my-service.example.com/hooks/opensail"
  }'
```

The response comes back immediately:

```json theme={null}
{
  "task_id": "arq:task:abc123",
  "chat_id": "660e8400-e29b-41d4-a716-446655440000",
  "events_url": "/api/external/agent/events/arq:task:abc123",
  "status": "queued"
}
```

### Reading results

Three options, pick whichever fits your integration.

<Tabs>
  <Tab title="SSE stream (recommended)">
    Open a Server-Sent Events connection to `events_url`. You get live tool calls, text, and the final response as the agent works.

    ```bash theme={null}
    curl -N -H "Authorization: Bearer tsk_..." \
      https://your-opensail.example.com/api/external/agent/events/arq:task:abc123
    ```

    If the connection drops, reconnect with `?last_event_id=evt_002` to replay missed events before resuming live stream.
  </Tab>

  <Tab title="Polling">
    For simple webhooks and CI pipelines where SSE is impractical:

    ```bash theme={null}
    curl -H "Authorization: Bearer tsk_..." \
      https://your-opensail.example.com/api/external/agent/status/arq:task:abc123
    ```

    Response:

    ```json theme={null}
    {
      "status": "completed",
      "response": "I reviewed the PR and posted a summary to #engineering.",
      "iterations": 3,
      "tool_calls": 5,
      "started_at": "2026-04-23T10:00:01Z",
      "completed_at": "2026-04-23T10:00:15Z"
    }
    ```

    Statuses: `queued`, `running`, `completed`, `failed`, `cancelled`.
  </Tab>

  <Tab title="Webhook callback">
    Pass `webhook_url` in the invoke payload. When the task completes, OpenSail POSTs the final status to your URL. Your endpoint receives the same payload shape as the polling response.

    Good for: CI pipelines, serverless workflows, systems that can't hold an open connection.
  </Tab>
</Tabs>

### Scopes and rate limits

| Setting              | Behavior                                        |
| -------------------- | ----------------------------------------------- |
| `project_id` scope   | Key can only invoke agents on the named project |
| `expires_at`         | Key stops authenticating after this timestamp   |
| Active keys per user | Capped at 10                                    |
| `is_active`          | Soft-delete; revoked keys cannot be reactivated |

Rate limits apply per key to prevent runaway loops. Defaults are generous but enforced.

### Rotation and revocation

<Steps>
  <Step title="Create a replacement">
    Generate a new key in Settings. Copy the raw value.
  </Step>

  <Step title="Deploy the new key">
    Update your integration to use the new key. Verify calls succeed.
  </Step>

  <Step title="Revoke the old key">
    Back in Settings, click Revoke on the old key. `is_active` flips to false; the key stops authenticating immediately.
  </Step>
</Steps>

Keys can also be revoked via `DELETE /api/external/keys/{key_id}`.

## BYOK provider keys

BYOK (Bring Your Own Key) lets you route AI calls through your own provider key instead of paying platform credits. Available on Pro and Ultra subscription tiers.

<Tabs>
  <Tab title="How it works">
    When the agent picks a model whose provider matches a key you have stored, LiteLLM routes the call through your key. Cost is zero in platform credits; you pay the upstream provider directly. The `UsageLog` row is still created for your analytics with `is_byok=True` and `billed_status="exempt"`.
  </Tab>

  <Tab title="Providers">
    Supported: OpenAI, Anthropic, OpenRouter (covers 100+ models), Google, DeepSeek, Groq, Together, Fireworks, or any OpenAI-compatible endpoint. See `/guides/model-management` for the full list and self-hosted options.
  </Tab>
</Tabs>

### Adding a provider key

<Steps>
  <Step title="Get the key from the provider">
    Sign in at the provider console (platform.openai.com, console.anthropic.com, openrouter.ai, etc.) and generate an API key.
  </Step>

  <Step title="Open Settings, API Keys, BYOK tab">
    The BYOK tab lists every provider key you have stored (values masked).
  </Step>

  <Step title="Add the key">
    Pick the provider, paste the key, optionally name it.
  </Step>

  <Step title="Save">
    The key is Fernet-encrypted at rest. Raw keys are never logged or cached.
  </Step>

  <Step title="Verify">
    Start an agent session with a matching model. The model selector shows a BYOK badge and the session does not deduct platform credits.
  </Step>
</Steps>

### Where keys are stored

Each BYOK key is a `UserAPIKey` row:

| Field             | Purpose                                                              |
| ----------------- | -------------------------------------------------------------------- |
| `provider`        | Provider identifier (`openai`, `anthropic`, `openrouter`, etc.)      |
| `auth_type`       | `api_key`, `oauth_token`, `bearer_token`, or `personal_access_token` |
| `key_name`        | Your label                                                           |
| `encrypted_value` | Fernet-encrypted raw key                                             |
| `is_active`       | Soft-deleted keys remain in the table for audit                      |
| `last_used_at`    | Timestamp of most recent use                                         |

### BYOK detection

When you invoke a model, `is_byok_model(model_name)` checks the provider prefix against `BUILTIN_PROVIDERS`. If the prefix matches one of your stored keys, the call runs BYOK. Adding a new provider to the registry automatically enables BYOK routing for its models.

<Info>
  BYOK only applies when a matching key exists for your user. If you have an Anthropic key but pick a DeepSeek model, the DeepSeek call uses platform credits. Keys don't cross providers.
</Info>

## Security

<AccordionGroup>
  <Accordion icon="lock" title="At-rest encryption">
    External key hashes are SHA-256. BYOK keys are encrypted with Fernet using a server-held key (`ENCRYPTION_KEY`). Raw values are never persisted in plaintext.
  </Accordion>

  <Accordion icon="eye-slash" title="No plaintext in logs">
    Tool outputs are scrubbed for high-entropy strings before being returned to the model. Your keys never leak into the transcript or an exported trajectory.
  </Accordion>

  <Accordion icon="clock-rotate-left" title="Rotation policy">
    Rotate external keys whenever a team member changes roles. Rotate BYOK keys on any suspicion of compromise and whenever the provider recommends it.
  </Accordion>

  <Accordion icon="user-gear" title="Separation of duties">
    External keys bind to a user, not to a service account. If a user leaves, revoke their external keys. Plan for handover: critical integrations should use service-owned user accounts.
  </Accordion>
</AccordionGroup>

## Common patterns

<Tabs>
  <Tab title="Slack bot">
    Generate an external key scoped to one project. Configure your Slack app to POST mentions to a worker. The worker calls `/api/external/agent/invoke` with the mention text. Use the webhook callback to post the agent's response back into Slack.
  </Tab>

  <Tab title="CI review bot">
    Generate a short-lived external key (with `expires_at`). Use it in a GitHub Actions workflow to call `/api/external/agent/invoke` on PR events. Poll the status endpoint until complete, then post the agent's response as a PR comment.
  </Tab>

  <Tab title="Scheduled report">
    Use OpenSail's built-in scheduler (see `/guides/using-agents`) instead of an external cron. The scheduler invokes agents on a cron expression with full context. External keys are for when you must trigger from outside.
  </Tab>

  <Tab title="External coding agent">
    Cursor, Claude Code, or any MCP-capable agent can talk to OpenSail's MCP server using an external key. The agent gets sandboxed compute, access to your project tools, and can publish apps back to OpenSail.
  </Tab>
</Tabs>

## Troubleshooting

<AccordionGroup>
  <Accordion icon="ban" title="401 Unauthorized on invoke">
    Key expired, revoked, or mis-pasted. Check the raw value has no leading whitespace or trailing newline. Verify the key is still active in Settings.
  </Accordion>

  <Accordion icon="shield-xmark" title="403 Forbidden on project access">
    Key is scoped to a different project. Either create a new key for the target project or use an unscoped key.
  </Accordion>

  <Accordion icon="hourglass-half" title="Task stuck in queued">
    ARQ worker pods may be saturated. Check worker health. For long queues, tune `worker_max_jobs` in the platform config.
  </Accordion>

  <Accordion icon="key-skeleton" title="BYOK call falls through to credits">
    Verify the model's provider prefix matches your stored key's provider. A Claude model call only uses an Anthropic key, not an OpenAI key. Check BYOK is enabled for your tier (Pro or Ultra).
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Model Management" icon="microchip" href="/guides/model-management">
    BYOK routing, passthrough mode, self-hosted models
  </Card>

  <Card title="Billing" icon="credit-card" href="/guides/billing">
    How BYOK zeros your credit cost while keeping analytics
  </Card>

  <Card title="Using Agents" icon="comments" href="/guides/using-agents">
    Invoke an agent from the chat UI instead of the API
  </Card>

  <Card title="Connectors (MCP)" icon="plug" href="/guides/connectors-mcp">
    Wire Slack, Gmail, and more into the agent itself instead of bolting external callers on top
  </Card>
</CardGroup>
