Markdown for agents
This page as plain markdown, ready to paste into an LLM.
Authentication
One bearer header, checked on every request against what its owner can do right now.
One header
Authorization: Bearer lak_live_k7m2p9qh_9fK2mQ7pL4vX8nT3bY6cZ1sD5fG0jH2kThat is the only scheme. No query parameter, no cookie, no basic auth. No header is 401 missing_authorization. A header that is not Bearer <key> is 401 malformed_key.
Key format
| Part | Value | Notes |
|---|---|---|
lak | fixed | One secret-scanner rule matches every key |
live or test | environment | test keys are rejected in v1 |
| 8 characters | public handle | Safe to log |
| 32 characters | the secret | Hashed with SHA-256, never stored in the clear |
What happens on every request
Checked every time, with no cache, so revoking a key works on the next call:
- Parse the token. Wrong shape is
401 malformed_key. Atestkey is403 test_keys_unsupported. - Compare the secret hash. No match is
401 invalid_key. "No such key" and "wrong secret" give the same error on purpose. - Revoked is
401 key_revoked. Expired is401 key_expired. - Re-read the owner. Deactivated is
401 owner_deactivated. - Work out which workspaces the owner can reach now, and keep only those the key was given.
- Keep only the scopes the owner's role still allows.
- Check the endpoint's scope, then count the request against its rate limit.
Demote or offboard the owner and their keys lose that reach on the next request. Nothing has to be revoked by hand.
Reaching a workspace
The workspace slug is in the path, and the slug is the workspace id:
GET https://api.leadarc.io/v1/workspaces/airpay/leadsAn unknown slug and a slug you cannot reach give the same error. It never falls back to every workspace you can see.
Object ids
Every id in a response except the workspace slug is an opaque, signed string:
lead_BP2vnPmKyttNP2OlH_mWL78t1o6nBVC6oVbHnyTwo parts. The payload is base32 of (clientId, localId). The tag is the first 6 hex characters of HMAC-SHA256(API_ID_SECRET, prefix + payload). decodeId verifies the tag. A tampered id fails the tag and returns 404 not_found, so ids are not enumerable and cannot be correlated across workspaces.
The rule that makes the signature useful: after decoding, the handler asserts decoded.clientId === resolvedWorkspace.id. A mismatch is 404, not 403, so an id from another tenant is indistinguishable from an id that does not exist. Without that assertion the signature is decoration.
Publishing them exposes the upstream provider's id space and lets two workspaces on the shared EmailBison instance correlate against each other. Separately, isCreatorId(id) => id < 0 is the codebase's "skip auth, use the mock store" signal, and 57 route files short-circuit on it, several before their auth check. Opaque ids mean a caller can never hand a negative integer to a shared internal helper.
| Prefix | Object | Local id source |
|---|---|---|
usr_ | member | users.id |
lead_ | lead | responses_index.eb_lead_id |
rep_ | reply | responses_index.eb_reply_id |
msg_ | message | thread_cache.payload message id |
note_ | note | lead_BP2vnPmKyttNP2OlH_mWL78t1o6nBVC6oVbHny |
tsk_ | task | tasks.id |
mtg_ | meeting | scheduled_meetings.id |
evt_ | event_type | event_types.id |
evh_ | event_type_host | event_type_hosts.id |
fld_ | event_type_form_field | event_type_form_fields.id |
tpl_ | template | reply_templates.id |
tfd_ | template_folder | reply_template_folders.id |
ast_ | asset | client_assets.id |
seq_ | sequence | followup_sequences.id |
stp_ | sequence_step | followup_sequence_steps.id |
enr_ | enrollment | followup_enrollments.id |
cmp_ | campaign | EmailBison campaign id |
ctg_ | campaign_tag | EmailBison tag id |
enrv_ | enrichment_event | enrichment_events.id |
key_ | api_key | api_keys.id |
whk_ | webhook_endpoint | api_webhook_endpoints.id (v2) |
whd_ | webhook_delivery | api_webhook_deliveries.id (v2) |
A caller must treat ids as opaque strings. No parsing, no arithmetic, no sorting.
Handling a key
- Server side only. There are no CORS headers, so a browser cannot call this API.
- One key per integration, so revoking one does not break the others.
- To rotate: make a new key, move the integration, revoke the old one. A secret is never shown twice.
- Log the 8-character handle, never the whole token.