Postbox docs

API keys

Create, list, scope, and rotate project API keys over the API, not just in the console.

A project API key (pb_live_..., or pb_test_... in a test project) is the credential your backend uses to call Postbox. You can create keys in the console, but you can also manage them over the API: mint them, list them, and revoke them, which is what makes automated key rotation possible.

Keys are scoped to one project, so key calls take an X-Project-Id header.

Create

The project needs a verified domain first (a key with no sending identity can't do anything). The full secret is returned once, in the apiKey field. Store it now; it is never shown again.

import { Postbox } from "@postbox/sdk";

const pb = new Postbox({
  apiKey: process.env.PB_KEY!,
  projectId: process.env.PROJECT_ID!,
});

const created = await pb.apiKeys.create({
  label: "backend-prod",
  permissions: ["send", "receive"],
  expiresIn: 7776000, // seconds (90 days)
});

console.log(created.apiKey); // pb_live_… shown once
import os
from postbox import Postbox

pb = Postbox(api_key=os.environ["PB_KEY"], project_id=os.environ["PROJECT_ID"])

created = pb.api_keys.create(
    {
        "label": "backend-prod",
        "permissions": ["send", "receive"],
        "expiresIn": 7776000,  # seconds (90 days)
    }
)

print(created["apiKey"])  # pb_live_… shown once
using Postbox;

var pb = new PostboxClient(
    new PostboxOptions
    {
        ApiKey = Environment.GetEnvironmentVariable("PB_KEY")!,
        ProjectId = Environment.GetEnvironmentVariable("PROJECT_ID")!,
    }
);

var created = await pb.ApiKeys.CreateAsync(
    new ApiKeysCreateBody
    {
        Label = "backend-prod",
        Permissions = new List<string> { "send", "receive" },
        ExpiresIn = 7776000, // seconds (90 days)
    }
);

Console.WriteLine(created!.ApiKey); // pb_live_… shown once
import cloud.postboxapp.*;
import cloud.postboxapp.gen.*;
import java.util.*;

Postbox pb = new Postbox(PostboxOptions.builder().apiKey(System.getenv("PB_KEY")).projectId(System.getenv("PROJECT_ID")).build());

CreatedApiKey created = pb.apiKeys.create(ApiKeysCreateBody.builder().label("backend-prod").permissions(List.of("send", "receive")).expiresIn(7776000).build());

System.out.println(created.apiKey()); // pb_live_… shown once
import postbox "github.com/postboxapp/postbox-go"

pb, err := postbox.New(postbox.Options{
	APIKey:    os.Getenv("PB_KEY"),
	ProjectID: os.Getenv("PROJECT_ID"),
})
ctx := context.Background()

created, err := pb.ApiKeys.Create(ctx, &postbox.ApiKeysCreateBody{
	Label:       "backend-prod",
	Permissions: []string{"send", "receive"},
	ExpiresIn:   7776000,
})

fmt.Println(created.ApiKey) // pb_live_… shown once
curl -X POST https://api.postboxapp.cloud/v1/api-keys \
  -H "Authorization: Bearer $PB_KEY" \
  -H "X-Project-Id: $PROJECT_ID" \
  -H "Content-Type: application/json" \
  -d '{
        "label": "backend-prod",
        "permissions": ["send", "receive"],
        "expiresIn": 7776000
      }'
FieldRequiredNotes
labelyesA human name, so you can tell keys apart when rotating.
permissionsnoScopes: read, send, receive, manage. See Permissions.
expiresInnoLifetime in seconds. Omit for a non-expiring key.

An empty scope list means full access

Omitting permissions (or passing []) does not mean "no access", it means unscoped, full access. Always name the scopes a key actually needs. A leaked send-only key cannot read mail or manage the account.

List

Listing never returns the secret, only its prefix and metadata, so it's safe to call from anywhere:

const keys = await pb.apiKeys.list();
keys = pb.api_keys.list()
var keys = await pb.ApiKeys.ListAsync();
var keys = pb.apiKeys.list();
keys, err := pb.ApiKeys.List(ctx)
curl https://api.postboxapp.cloud/v1/api-keys \
  -H "Authorization: Bearer $PB_KEY" -H "X-Project-Id: $PROJECT_ID"
{
  "success": true,
  "data": [
    {
      "id": "key_...",
      "keyPrefix": "pb_live_a1b2",
      "label": "backend-prod",
      "permissions": ["send", "receive"],
      "lastUsedAt": "2026-08-02T14:03:00.000Z",
      "expiresAt": "2026-10-31T00:00:00.000Z",
      "active": true
    }
  ]
}

lastUsedAt is how you spot a key that nothing is using anymore, or one that is still live and can't be retired yet.

Revoke

await pb.apiKeys.delete(keyId);
pb.api_keys.delete(key_id)
await pb.ApiKeys.DeleteAsync(keyId);
pb.apiKeys.delete(keyId);
pb.ApiKeys.Delete(ctx, keyID)
curl -X DELETE https://api.postboxapp.cloud/v1/api-keys/$KEY_ID \
  -H "Authorization: Bearer $PB_KEY" -H "X-Project-Id: $PROJECT_ID"

Revocation is immediate. In-flight requests using that key start failing with 401.

Rotating a key

Rotation is create-then-revoke, never revoke-then-create, so there is no window where your service has no working key:

  1. create a new key with the same scopes and a fresh label.
  2. Deploy it to your service and confirm traffic is flowing on it (watch lastUsedAt on the new key).
  3. delete the old key.

Give each service its own key. Then a rotation, or a leak, is contained to one service instead of taking down everything at once.