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_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6The 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:
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:
{
"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
| Permission | What It Grants |
|---|---|
evaluate | Call POST /{org}/{contract}/evaluate and GET /{org}/{contract}/actions |
execute | Call POST /{org}/{contract}/flows/{flow_id}/execute |
simulate | Call POST /{org}/{contract}/simulate |
manage | Manage deployments and persona mappings via management API |
admin | Full 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:
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:
{
"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.
# 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:
{
"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:
{
"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:
- Key bindings first. If the API key has
persona_bindings, those are the only personas available. - Deployment persona map. If the key has no bindings, check the deployment's
persona_mapfor a matching identity. - 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:
{
"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:
{
"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:
{
"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:
{
"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:
- Create a new key with the same permissions and persona bindings
- Update your application to use the new key
- Verify the new key works
- 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:
| Event | Logged Data |
|---|---|
| Key created | Key ID, name, permissions, persona bindings, creator |
| Key used | Key ID, endpoint called, persona resolved, timestamp |
| Key revoked | Key ID, revoker, timestamp |
| Key expired | Key ID, expiration time |
| Key rejected | Key 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.