Authentication & API Keys

Manage API keys and authenticate requests

All Tratto API requests require authentication via bearer tokens (API keys).

API Key Format

Tratto API keys follow this format:

tratto_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
tratto_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • Prefix: tratto_live_ (real delivery) or tratto_test_ (simulated — see Test mode)
  • Followed by 32 random characters
  • Used as a bearer token in the Authorization header

The environment is baked into the key: a test key runs the full pipeline without sending real email, sees only test data, and is rejected by endpoints that reach real recipients.

Authentication Header

Include your API key in every request:

Authorization: Bearer tratto_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Example:

curl https://api.tratto.email/v1/emails \
  -H "Authorization: Bearer tratto_live_..."

Without the header, you'll receive 401 Unauthorized.

Permissions

Every key carries a set of permissions, and they are enforced on each request: a key missing the permission an endpoint requires gets 403 Forbidden, no matter which workspace it belongs to.

PermissionGrants
emails:sendSend email, schedule, cancel, reschedule
emails:readRead the email log and delivery events
contacts:read / contacts:writeRead / create, update, import contacts and audiences
domains:read / domains:writeRead / add, verify, remove sending domains
templates:read / templates:writeRead / create and edit templates
campaigns:read / campaigns:writeRead / create and send campaigns and flows
webhooks:read / webhooks:writeRead / register, edit, rotate webhook endpoints
api-keys:read / api-keys:writeRead / create, update, revoke API keys
billing:read / billing:writeRead plan and usage / start checkout, open the billing portal
workspace:writeChange workspace settings, including the default sender
members:writeInvite, remove, and change the role of workspace members
*Full access — every permission above, including future ones

Grant the narrowest set that does the job. A key that only sends transactional email needs emails:send and nothing else; one that also mirrors signups into your contact list needs contacts:write too.

* is stored on its own: listing it alongside granular permissions is redundant, since it already covers them.

Create an API Key

Generate a new API key via the API, or from Settings → API keys in the dashboard.

name, env and permissions are all required. Omitting permissions (or passing an empty array) returns 422.

cURL

curl -X POST https://api.tratto.email/v1/api-keys \
  -H "Authorization: Bearer tratto_live_EXISTING_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"production","env":"live","permissions":["emails:send"]}'

Node.js

const response = await fetch('https://api.tratto.email/v1/api-keys', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer tratto_live_EXISTING_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ name: 'production', env: 'live', permissions: ['emails:send'] }),
});
const { data } = await response.json();
console.log('New API Key:', data.key);

Python

import requests

response = requests.post(
  'https://api.tratto.email/v1/api-keys',
  headers={
    'Authorization': 'Bearer tratto_live_EXISTING_KEY',
    'Content-Type': 'application/json',
  },
  json={'name': 'production', 'env': 'live', 'permissions': ['emails:send']},
)
api_key = response.json()['data']['key']
print(f"New API Key: {api_key}")

Response:

{
  "data": {
    "id": "key_abc123",
    "name": "production",
    "key": "tratto_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "prefix": "tratto_live_xxxx...xxxx",
    "env": "live",
    "permissions": ["emails:send"],
    "createdAt": "2025-06-30T12:00:00Z",
    "lastUsedAt": null,
    "revokedAt": null
  }
}

⚠️ Important: The key field is shown only once. Save it immediately: it won't be displayed again. If you lose it, revoke and create a new key.

List API Keys

Retrieve all API keys for your tenant.

cURL

curl https://api.tratto.email/v1/api-keys \
  -H "Authorization: Bearer tratto_live_..."

Node.js

const response = await fetch('https://api.tratto.email/v1/api-keys', {
  headers: {
    'Authorization': 'Bearer tratto_live_...',
  },
});
const { data } = await response.json();
console.log(data); // Array of API key objects

Python

import requests

response = requests.get(
  'https://api.tratto.email/v1/api-keys',
  headers={'Authorization': 'Bearer tratto_live_...'},
)
keys = response.json()['data']
for key in keys:
  print(f"Key: {key['prefix']} (Created: {key['createdAt']})")

Response:

{
  "data": [
    {
      "id": "key_abc123",
      "name": "production",
      "prefix": "tratto_live_xxxx...xxxx",
      "createdAt": "2025-06-30T12:00:00Z",
      "lastUsedAt": "2025-06-30T13:45:00Z"
    },
    {
      "id": "key_def456",
      "name": "development",
      "prefix": "tratto_live_yyyy...yyyy",
      "createdAt": "2025-06-29T10:00:00Z",
      "lastUsedAt": "2025-06-29T15:20:00Z"
    }
  ]
}

Note: The full key is never returned after creation, only the prefix (first and last 4 characters).

lastUsedAt is null until the key authenticates a request for the first time. It is refreshed at most once every 15 minutes, so it answers "is this key still in use?" rather than giving an exact timestamp of the last call.

Update Key Permissions

Change the permissions of an existing key, without revoking it or rotating the secret. The key value itself never changes, so nothing that already uses it needs redeploying — permissions are read on every request, and the new set applies to the next one.

cURL

curl -X PATCH https://api.tratto.email/v1/api-keys/key_abc123 \
  -H "Authorization: Bearer tratto_live_..." \
  -H "Content-Type: application/json" \
  -d '{"permissions":["emails:send","contacts:write"]}'

Node.js

const response = await fetch('https://api.tratto.email/v1/api-keys/key_abc123', {
  method: 'PATCH',
  headers: {
    'Authorization': 'Bearer tratto_live_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ permissions: ['emails:send', 'contacts:write'] }),
});
const { data } = await response.json();
console.log('Permissions:', data.permissions);

Python

import requests

response = requests.patch(
  'https://api.tratto.email/v1/api-keys/key_abc123',
  headers={
    'Authorization': 'Bearer tratto_live_...',
    'Content-Type': 'application/json',
  },
  json={'permissions': ['emails:send', 'contacts:write']},
)
print(f"Permissions: {response.json()['data']['permissions']}")

permissions replaces the previous set — it is not merged into it. Send the complete list you want the key to end up with.

Requires api-keys:write. Other responses:

StatusWhen
409The key is revoked — permissions can no longer be changed. Create a new key instead.
422An unknown permission, an empty list, or an attempt to remove api-keys:write from the key making the request.

You cannot strip api-keys:write from the key you are authenticating with: no other endpoint could grant it back, so you would lock yourself out of key management. Removing it from a different key is allowed — that is recoverable.

Revoke an API Key

Immediately revoke an API key to stop it from working.

cURL

curl -X DELETE https://api.tratto.email/v1/api-keys/key_abc123 \
  -H "Authorization: Bearer tratto_live_..."

Node.js

const response = await fetch('https://api.tratto.email/v1/api-keys/key_abc123', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer tratto_live_...',
  },
});
const { data } = await response.json();
console.log('Revoked:', data.id);

Python

import requests

response = requests.delete(
  'https://api.tratto.email/v1/api-keys/key_abc123',
  headers={'Authorization': 'Bearer tratto_live_...'},
)
print(f"Revoked: {response.json()['data']['id']}")

Once revoked, requests with that key will return 401 Unauthorized.

Key Security Best Practices

1. Never Expose Keys Client-Side

API keys grant full workspace access. Never include them in:

  • Frontend JavaScript (React, Vue, Angular)
  • Mobile apps (iOS, Android)
  • Published source code on GitHub

Instead, call your backend API, which holds the key securely.

For framework-specific guidance see React (Server Components and Server Actions) and Angular (SSR and AnalogJS/Nitro).

Wrong:

// ❌ NEVER DO THIS
const trattKey = 'tratto_live_...';
const response = await fetch('https://api.tratto.email/v1/emails', {
  headers: { 'Authorization': `Bearer ${trattoKey}` },
});

Right:

// ✅ Call your backend
const response = await fetch('/api/send-email', {
  method: 'POST',
  body: JSON.stringify({ to, subject, text }),
});

2. Treat Keys Like Passwords

  • Store in environment variables (.env.local, not .env)
  • Use secrets management for production (GitHub Secrets, HashiCorp Vault, etc.)
  • Don't commit them to Git

3. Rotate Keys Regularly

  • Create a new key
  • Update your services to use the new key
  • Wait for all deployments to finish
  • Revoke the old key

4. Use Specific Keys for Environments

Create separate keys for:

  • production: Used only in production
  • staging: Used only in staging
  • development: Used only in local development

This way, a leaked development key doesn't compromise production.

5. Monitor Key Usage

Check lastUsedAt in list responses to identify unused keys. Revoke old, unused keys periodically.

Error Responses

401 Unauthorized

Cause: Missing or invalid API key.

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Missing or invalid Authorization header",
    "docs": "https://docs.tratto.email/en/authentication"
  }
}

Fix:

  • Ensure the Authorization: Bearer tratto_live_... header is present
  • Check for typos in the API key
  • Verify the key hasn't been revoked

403 Forbidden

Cause: API key is valid but doesn't have permission for this operation.

{
  "error": {
    "code": "FORBIDDEN",
    "message": "Your API key does not have permission to perform this action",
    "docs": "https://docs.tratto.email/en/authentication"
  }
}

The response names the permission that was missing, so you can tell which one to add:

{
  "error": {
    "code": "FORBIDDEN",
    "message": "Missing required permission: contacts:write",
    "suggestion": "Create a new API key with the 'contacts:write' permission."
  }
}

Fix it by granting that permission to the key — see Update Key Permissions — or by using a key that already has it. Check the key's current set with GET /v1/api-keys.

Idempotency Header

The Idempotency-Key header prevents duplicate requests on certain endpoints (POST operations that modify state).

Usage:

POST /v1/emails
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

Rules:

  • Value should be a UUID v4 (RFC 4122)
  • Tratto caches the response for 24 hours
  • If you retry with the same key, you get the same response without a duplicate action
  • The cached response is returned for any retry with that key; the request body is not compared, so reuse a key only for the operation that created it

Example:

import { v4 as uuidv4 } from 'uuid';

const idempotencyKey = uuidv4();
const response = await fetch('https://api.tratto.email/v1/emails', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer tratto_live_...',
    'Idempotency-Key': idempotencyKey,
  },
  body: JSON.stringify({
    from: '[email protected]',
    to: '[email protected]',
    subject: 'Hello',
    text: 'World',
  }),
});

See Idempotency for more details.


Next Steps


Edit this page on GitHub

Last updated on