# Authentication

One bearer header, checked on every request against what its owner can do right now.

Source: https://docs.leadarc.io/authentication.html

## One header

```http
Authorization: Bearer lak_live_k7m2p9qh_9fK2mQ7pL4vX8nT3bY6cZ1sD5fG0jH2k
```

That is the only scheme. No query parameter, no cookie, no basic auth. No header is [`401 missing_authorization`](errors.html#status-401). A header that is not `Bearer <key>` is [`401 malformed_key`](errors.html#status-401).

## 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:

1. Parse the token. Wrong shape is `401 malformed_key`. A `test` key is [`403 test_keys_unsupported`](errors.html#status-403).
2. Compare the secret hash. No match is [`401 invalid_key`](errors.html#status-401). "No such key" and "wrong secret" give the same error on purpose.
3. Revoked is [`401 key_revoked`](errors.html#status-401). Expired is [`401 key_expired`](errors.html#status-401).
4. Re-read the owner. Deactivated is [`401 owner_deactivated`](errors.html#status-401).
5. Work out which workspaces the owner can reach now, and keep only those the key was given.
6. Keep only the scopes the owner's role still allows.
7. Check the endpoint's scope, then count the request against its rate limit.

> **The grant is a ceiling, never a floor.**
>
> 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:

```http
GET https://api.leadarc.io/v1/workspaces/airpay/leads
```

An 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_mWL78t1o6nBVC6oVbHny
```
Two 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`](errors.html#status-404), 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.

---

Base URL `https://api.leadarc.io/v1`. Generated from the API source.
