Skip to main content
BunShip provides API key management for programmatic access. Keys are scoped to organizations, support granular permissions, and track usage automatically.

Key Format

API keys follow the format bunship_live_<32 hex characters>:
bunship_live_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0
The prefix bunship_live_ (or bunship_test_ for test mode) makes keys identifiable in logs and configuration files. Only the first 8 characters of the random portion are stored as the display prefix:
bunship_live_a1b2c3d4...

Creating API Keys

Create a key by providing a name, optional scopes, and optional expiration:
const { apiKey, plainKey } = await createApiKey(orgId, userId, {
  name: "Production API Key",
  scopes: ["read:projects", "write:projects", "read:members"],
  rateLimit: 1000, // requests per minute
  expiresAt: new Date("2026-01-01"),
});
API call:
curl -X POST https://api.example.com/api/v1/api-keys \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "CI/CD Pipeline",
    "scopes": ["read:projects", "write:projects"],
    "expiresAt": "2026-01-01T00:00:00Z"
  }'
The full API key is returned only once in the creation response. BunShip stores a SHA-256 hash of the key, not the key itself. There is no way to retrieve the full key after creation.
Response:
{
  "apiKey": {
    "id": "key_abc123",
    "name": "CI/CD Pipeline",
    "keyPrefix": "bunship_live_a1b2c3d4",
    "scopes": ["read:projects", "write:projects"],
    "rateLimit": null,
    "isActive": true,
    "createdAt": "2025-03-15T10:00:00Z",
    "expiresAt": "2026-01-01T00:00:00Z",
    "lastUsedAt": null
  },
  "plainKey": "bunship_live_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0"
}

Scopes and Permissions

API keys use a scope-based permission model. Each scope follows the pattern action:resource:
ScopeDescription
read:projectsList and view projects
write:projectsCreate and update projects
read:membersList organization members
write:membersInvite and manage members
read:webhooksList webhook endpoints and deliveries
write:webhooksCreate and manage webhook endpoints
read:billingView subscription and usage
read:audit-logsQuery audit log entries
Scope checking uses a deny-by-default model. A key with no scopes is denied access to all resources:
export function hasScope(apiKey: ApiKey, requiredScope: string): boolean {
  if (!apiKey.scopes || apiKey.scopes.length === 0) {
    return false;
  }
  return apiKey.scopes.includes(requiredScope);
}
Grant only the scopes each key needs. A deployment pipeline that only reads project data should not have write:members access.

Authentication

Authenticate API requests by passing the key in the X-API-Key header:
curl https://api.example.com/api/v1/projects \
  -H "X-API-Key: bunship_live_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0"
BunShip validates the key by:
1

Hashing the provided key

The raw key is hashed with SHA-256 to produce a digest.
2

Looking up the hash

The hash is compared against stored key hashes in the database.
3

Checking key status

The key must be active and not expired.
if (!apiKey.isActive) {
  throw new AuthenticationError("API key is inactive");
}
if (apiKey.expiresAt && apiKey.expiresAt < new Date()) {
  throw new AuthenticationError("API key has expired");
}
4

Updating last used timestamp

On successful validation, the lastUsedAt field is updated for usage tracking.

Key Rotation

To rotate a key, create a new one, update your application to use it, then revoke the old key:
1

Create a new API key

curl -X POST https://api.example.com/api/v1/api-keys \
  -H "Authorization: Bearer <token>" \
  -d '{ "name": "Production Key (rotated)", "scopes": ["read:projects"] }'
2

Deploy the new key

Update your application or CI/CD pipeline with the new key.
3

Revoke the old key

curl -X DELETE https://api.example.com/api/v1/api-keys/<old_key_id> \
  -H "Authorization: Bearer <token>"
Revocation is immediate. Any in-flight requests using the old key will fail after revocation.

Usage Tracking

Each API key tracks when it was last used. Query usage statistics for a specific key:
curl https://api.example.com/api/v1/api-keys/<key_id>/usage \
  -H "Authorization: Bearer <token>"
Response:
{
  "keyId": "key_abc123",
  "name": "CI/CD Pipeline",
  "prefix": "bunship_live_a1b2c3d4",
  "lastUsedAt": "2025-03-15T14:22:00Z",
  "createdAt": "2025-03-01T10:00:00Z",
  "usage": {
    "requests": 4521,
    "rateLimit": 1000
  }
}
API key actions are also recorded in the audit log. Every request authenticated with an API key creates an audit entry with actorType: "api_key".

Security Details

BunShip applies several measures to protect API keys:
Keys are hashed with SHA-256 before storage. The database never contains the raw key. Even if the database is compromised, the keys cannot be reversed.
export async function hashApiKey(key: string): Promise<string> {
  const encoder = new TextEncoder();
  const data = encoder.encode(key);
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
  return Array.from(new Uint8Array(hashBuffer), (b) =>
    b.toString(16).padStart(2, "0")
  ).join("");
}
After creation, the API only shows the key prefix (e.g., bunship_live_a1b2c3d4). The full key is never returned again.
Keys can be given an expiration date. Expired keys are rejected during validation without any manual intervention.
Each key can have an individual rate limit. The middleware enforces this alongside global rate limits.

Plan Limits

API key counts are enforced per plan:
PlanMax API keys
Free1
Pro10
Enterprise50