Docs / Getting started
Authentication and API keys
How Tend authenticates requests, what the tnd_live_ and tnd_dev_ prefixes mean, and how to store, scope and rotate keys safely.
Last updated February 18, 2026
#Bearer authentication
Every request to the Tend API must carry an API key in the Authorization header using the Bearer scheme. There are no query-string keys, no cookies, and no basic auth. This is intentional: URLs end up in logs, proxies and browser history, and headers mostly do not.
Authorization: Bearer tnd_live_4Kp9vXc2RtQe7WnJRequests are made over HTTPS to https://api.tendcomputer.com/v2. Plain HTTP connections are refused rather than redirected, so a misconfigured client fails loudly instead of sending a key in the clear.
curl https://api.tendcomputer.com/v2/schedules?limit=25 \
-H "Authorization: Bearer $TEND_API_KEY"import os
from tend import Tend
# The SDK reads TEND_API_KEY from the environment if api_key is omitted
client = Tend(api_key=os.environ["TEND_API_KEY"])
print([s.name for s in client.schedules.list(limit=25)])import { Tend } from "@tend/sdk";
const client = new Tend({ apiKey: process.env.TEND_API_KEY });
const page = await client.schedules.list({ limit: 25 });
console.log(page.data.map((s) => s.name));client := tend.NewClient(os.Getenv("TEND_API_KEY"))
page, err := client.Schedules.List(ctx, &tend.ListParams{Limit: 25})
if err != nil {
log.Fatal(err)
}
for _, s := range page.Data {
fmt.Println(s.Name)
}#Key prefixes and environments
A key's prefix tells you which environment it belongs to, and the API enforces that boundary. Development keys begin with tnd_dev_. Production keys begin with tnd_live_. The two environments live inside the same project but are otherwise separate worlds: a tnd_dev_ key cannot see, modify or trigger anything created with a tnd_live_ key, and the reverse is also true.
| Prefix | Environment | Typical use | Can see the other environment? |
|---|---|---|---|
tnd_dev_ | Development | Local work, staging, CI | No |
tnd_live_ | Production | Deployed services | No |
Because of this isolation, a job ID from one environment returns 404 resource_not_found in the other. That error is correct behavior and not a bug: the resource genuinely does not exist from that key's point of view. Schedule names are unique per environment, so you can have a nightly-usage-report schedule in both development and production without a schedule_name_taken conflict.
#Scopes and least privilege
A key is created with a set of scopes, and it can only perform operations those scopes allow. Prefer the narrowest scope that works. A service that only enqueues jobs does not need permission to delete schedules, and a dashboard that only displays run history does not need to write anything.
| Scope | Allows |
|---|---|
jobs:read | List and retrieve jobs, runs and attempts |
jobs:write | Create, update and cancel jobs |
schedules:read | List and retrieve schedules |
schedules:write | Create, update, pause and delete schedules |
keys:manage | Create and revoke other keys (dashboard and Scale plan role-based access) |
Attempting an operation without the required scope returns 403 insufficient_scope. This is distinct from 401 invalid_api_key, which means the key itself is unknown, revoked, or does not begin with tnd_live_ or tnd_dev_. A missing or malformed Authorization header returns 401 missing_api_key.
{
"error": {
"code": "insufficient_scope",
"message": "This key does not have the schedules:write scope.",
"request_id": "req_01J8QK3T9DXV5M2WBA7E4NHCYR"
}
}#Storing keys safely
Treat a tnd_live_ key like a database password, because that is effectively what it is: anyone holding it can schedule work that calls your endpoints and consumes your run quota. The practices below are not exotic, but they are the ones that get skipped.
- Load keys from environment variables or a secrets manager. Never commit them to source control, including private repositories.
- Use separate keys per service and per environment, so revoking one does not take down everything.
- Never embed a key in a browser bundle or mobile app. Anything shipped to a client is public. Route through your own backend instead.
- Keep
tnd_live_keys out of CI logs. Mask them in your CI provider's secret settings. - Give CI and staging a
tnd_dev_key. There is rarely a good reason for a staging pipeline to hold production credentials.
Tend participates in secret scanning with major code hosting providers. If a tnd_live_ key appears in a public repository, we are notified, revoke the key automatically, and email the project owner. This is a safety net, not a plan.
#Rotating and revoking keys
Rotate keys on a schedule you can defend, and immediately after any suspected exposure or when someone with access leaves the team. Rotation without downtime is straightforward because a project can hold multiple active keys at once.
- Create a new key with the same scopes in the dashboard, or with the API using a key that holds
keys:manage. - Deploy the new key to your services and confirm requests succeed with it.
- Revoke the old key. Revocation propagates globally within a few seconds.
- Watch for
401 invalid_api_keyresponses in your logs, which indicate a service still using the old key.
# Create a replacement key
curl -X POST https://api.tendcomputer.com/v2/keys \
-H "Authorization: Bearer $TEND_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "billing-service-2026-03", "environment": "live", "scopes": ["jobs:write", "jobs:read"]}'
# Revoke the old one
curl -X DELETE https://api.tendcomputer.com/v2/keys/key_01J7ZM8C4RQ2XH6VNT0BWAE5DS \
-H "Authorization: Bearer $TEND_ADMIN_KEY"new_key = client.keys.create(
name="billing-service-2026-03",
environment="live",
scopes=["jobs:write", "jobs:read"],
)
# Store new_key.secret now; it is shown exactly once.
client.keys.revoke("key_01J7ZM8C4RQ2XH6VNT0BWAE5DS")const newKey = await client.keys.create({
name: "billing-service-2026-03",
environment: "live",
scopes: ["jobs:write", "jobs:read"],
});
// Store newKey.secret now; it is shown exactly once.
await client.keys.revoke("key_01J7ZM8C4RQ2XH6VNT0BWAE5DS");Revoking a key does not cancel jobs or schedules that key created. Those belong to the project and continue to run. To stop work, cancel the jobs or pause the schedules explicitly.
#Rate limits and authentication errors
Rate limits apply per project, not per key, and are measured in requests per minute. Exceeding the limit returns 429 rate_limit_exceeded with a Retry-After header giving the number of seconds to wait. Honor it. The SDKs do so automatically.
| Plan | Requests per minute | Concurrency limit |
|---|---|---|
| Hobby | 60 | 5 |
| Pro | 600 | 50 |
| Scale | 3,000 | 500 |
| Enterprise | 10,000 (negotiable) | 2,000 (dedicated pools available) |
Authentication failures are cheap to diagnose if you know what each code means.
| HTTP | Code | What to check |
|---|---|---|
| 401 | missing_api_key | The Authorization header is absent or is not in the form Bearer <key>. |
| 401 | invalid_api_key | The key is mistyped, revoked, or does not start with tnd_live_ or tnd_dev_. |
| 403 | insufficient_scope | The key is valid but lacks the scope this operation requires. |
| 404 | resource_not_found | Often a tnd_dev_ key looking for a tnd_live_ resource, or the reverse. |
| 429 | rate_limit_exceeded | Back off for Retry-After seconds; consider a higher plan for sustained load. |
Every error response includes a request_id. Include it when you contact support and we can find your request in seconds, rather than asking you to describe it in prose.