Skip to content

Every request to the Tenor platform API must be authenticated. Authentication identifies the calling system, resolves it to a contract persona, and checks permissions. There are no anonymous requests and no session cookies --- every call carries a Bearer token.

API Key Format

Tenor API keys follow a consistent format that encodes the environment:

tk_live_<32 hex chars>    — production environment
tk_test_<32 hex chars>    — test environment
tk_admin_<32 hex chars>   — management API (admin scope)

Examples:

tk_live_9f4a2b3c4d5e6f7a8b9c0d1e2f3a4b5c
tk_test_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d
tk_admin_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6

The prefix determines which environment the key can access. A tk_live_ key cannot access test state, and a tk_test_ key cannot access production state. This isolation is enforced at the platform level, not by convention.

Sending Authentication

Include the key as a Bearer token in the Authorization header:

bash
curl -X POST https://api.tenor.run/acme/escrow/evaluate \
  -H "Authorization: Bearer tk_live_9f4a2b3c4d5e6f7a8b9c0d1e2f3a4b5c" \
  -H "Content-Type: application/json" \
  -d '{"facts": {"payment_received": true}}'

Missing or malformed tokens receive 401 Unauthorized:

json
{
  "error": "unauthorized",
  "code": 401,
  "message": "Missing or invalid Authorization header. Expected: Bearer tk_..."
}

Permissions Model

Every API key carries a set of permissions that control what operations it can perform. Permissions are granted at key creation and cannot be modified after the fact --- to change permissions, revoke the key and create a new one.

Permission Levels

PermissionWhat It Grants
evaluateCall POST /{org}/{contract}/evaluate and GET /{org}/{contract}/actions
executeCall POST /{org}/{contract}/flows/{flow_id}/execute
simulateCall POST /{org}/{contract}/simulate
manageManage deployments and persona mappings via management API
adminFull access: everything above plus key management, org settings, usage data

Permissions are additive and independent. Granting execute does not implicitly grant evaluate. If your application needs to evaluate before executing, grant both:

bash
curl -X POST https://api.tenor.run/manage/orgs/org_001/api-keys \
  -H "Authorization: Bearer tk_admin_abc123" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Backend Service Key",
    "environment": "production",
    "permissions": ["evaluate", "execute", "simulate"]
  }'

Permission Denied

If a key lacks the required permission, the request fails with 403 Forbidden:

json
{
  "error": "forbidden",
  "code": 403,
  "message": "API key lacks 'execute' permission. Granted permissions: [evaluate, simulate]",
  "required_permission": "execute",
  "granted_permissions": ["evaluate", "simulate"]
}

Admin vs Scoped Keys

In practice, you should use two kinds of keys:

Admin keys (permissions: ["admin"]) are for your infrastructure: CI/CD pipelines deploying contracts, scripts managing persona mappings, dashboards reading usage data. Keep these in your secrets manager, never in application code.

Scoped keys (permissions: ["evaluate", "execute"] with persona_bindings) are for your application services. Each key represents a specific actor in the system. A microservice that processes buyer actions gets a key bound to the buyer persona with evaluate and execute permissions. It cannot manage deployments, create other keys, or act as a different persona.

bash
# Admin key — for deployment pipelines
curl -X POST https://api.tenor.run/manage/orgs/org_001/api-keys \
  -H "Authorization: Bearer tk_admin_abc123" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "CI/CD Deploy Key",
    "environment": "production",
    "permissions": ["admin"]
  }'

# Scoped key — for a buyer-facing service
curl -X POST https://api.tenor.run/manage/orgs/org_001/api-keys \
  -H "Authorization: Bearer tk_admin_abc123" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Buyer Service Key",
    "environment": "production",
    "permissions": ["evaluate", "execute"],
    "persona_bindings": ["buyer"]
  }'

Persona-to-API-Key Mapping

Tenor contracts define personas (escrow_agent, buyer, seller) as abstract roles. The platform must resolve an incoming API key to one or more personas before evaluation or execution.

There are two mapping mechanisms, and they are checked in order.

1. Key-Level Persona Bindings

When creating an API key, you can bind it directly to one or more personas:

json
{
  "name": "Agent Service Key",
  "permissions": ["evaluate", "execute"],
  "persona_bindings": ["escrow_agent"]
}

A key with persona bindings can only act as those personas. If the key is bound to escrow_agent and an execution request specifies "persona": "buyer", the request fails with 403.

This is the simplest and most secure approach: one key per persona per service.

2. Deployment-Level Persona Map

For more complex identity resolution, you can configure a persona map on the deployment itself. This supports multiple identity types:

json
{
  "persona_map": {
    "escrow_agent": [
      "tk_live_agent_key",
      "role:escrow-admin",
      "email:agent@acme.com"
    ],
    "buyer": [
      "tk_live_buyer_key",
      "role:customer",
      "sub:user_123"
    ],
    "seller": [
      "tk_live_seller_key",
      "role:merchant",
      "group:sellers"
    ]
  }
}

The executor resolves persona in order:

  1. Key bindings first. If the API key has persona_bindings, those are the only personas available.
  2. Deployment persona map. If the key has no bindings, check the deployment's persona_map for a matching identity.
  3. Reject. If no mapping is found, return 403 Forbidden.

Resolving Ambiguity

When an identity maps to multiple personas (e.g., a user who is both buyer and seller), the execution request must include a persona field to disambiguate:

json
{
  "persona": "buyer",
  "flow": "dispute_flow",
  "facts": {"dispute_reason": "Item not as described"}
}

If the persona field is omitted and the identity maps to more than one persona, the platform returns 400 Bad Request:

json
{
  "error": "ambiguous_persona",
  "code": 400,
  "message": "Identity maps to multiple personas: [buyer, seller]. Specify 'persona' in the request.",
  "available_personas": ["buyer", "seller"]
}

Organization Scoping

API keys are scoped to a single organization. A key created for org acme cannot access contracts deployed to org initech. This is enforced unconditionally --- there is no cross-org access, even for admin keys.

The URL path encodes the organization:

https://api.tenor.run/acme/escrow/evaluate    — org "acme"
https://api.tenor.run/initech/billing/evaluate — org "initech"

If a key for acme attempts to call an initech endpoint, the response is 403 Forbidden (not 404) to prevent org enumeration:

json
{
  "error": "forbidden",
  "code": 403,
  "message": "API key is not authorized for this organization"
}

Key Expiration and Rotation

API keys can be created with an optional expires_at timestamp:

json
{
  "name": "Temporary Integration Key",
  "permissions": ["evaluate"],
  "expires_at": "2026-03-15T00:00:00Z"
}

After expiration, the key is automatically invalid. No cleanup is needed.

For key rotation without downtime:

  1. Create a new key with the same permissions and persona bindings
  2. Update your application to use the new key
  3. Verify the new key works
  4. Revoke the old key

Both keys are valid simultaneously during the transition period. There is no limit on the number of active keys per organization.

Key Lifecycle Events

All key events are recorded in the organization's audit log:

EventLogged Data
Key createdKey ID, name, permissions, persona bindings, creator
Key usedKey ID, endpoint called, persona resolved, timestamp
Key revokedKey ID, revoker, timestamp
Key expiredKey ID, expiration time
Key rejectedKey ID (if valid but wrong permissions/org), reason

Audit logs are available via the management API and can be exported to external SIEM systems on the Enterprise plan.

Security Recommendations

One key per service per persona. If your buyer service and seller service are separate microservices, give each its own key with the appropriate persona binding. Never share keys across services.

Use test keys for development. Test keys access a completely isolated state store. You can create, destroy, and reset test state without affecting production.

Set expiration on temporary keys. Integration tests, contractor access, and demo keys should always have an expires_at. Permanent keys should be reserved for long-lived production services.

Rotate keys periodically. Even without a security incident, rotating production keys quarterly limits the blast radius of a hypothetical compromise.

Never embed keys in client-side code. Tenor API keys are server-side credentials. If you need client-side contract evaluation, use the local evaluator (WASM or native) with no API key.