API keys
An API key is a credential that stands in for one member of one company. Authenticating with it resolves that member, their user account and their company, and every request is scoped to that company's data.
The header
apikey: 8f2c1e... (64 hex characters)
Only this one header is required. Do not send Authorization, and do not send the authmember header that the web-app session path needs — the key already names the member.
:::info Header casing
The server reads req.headers.apiKey || req.headers.apikey. Node lower-cases all incoming header names, so whichever casing you send arrives as apikey. Use lowercase and don't think about it again.
:::
Managing keys
| Method | Endpoint | Notes |
|---|---|---|
POST | /api | Create a key. Returns the full key — the only time you will see it. |
GET | /api | List keys. The key field is projected out; you get maskedKey. |
GET | /api/:id | Fetch one key document. |
DELETE | /api | Delete keys — { "ids": ["..."] } in the body. |
Create
curl -X POST https://api.leadx.in/api/key/protected/api \
-H "apikey: $LEADX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "type": "permanent", "ipRestriction": "open" }'
| Field | Type | Default | Notes |
|---|---|---|---|
type | permanent | temporary | permanent | See the caveat on temporary keys below. |
durationMinutes | number (min 1) | — | Required when type is temporary, else DurationRequired_400. |
ipRestriction | open | restricted | open | Stored, but see the caveat below. |
The response is the created document, including key in full and maskedKey (*********** + last 4).
There is no update endpoint — the route exists in source but is commented out. Keys are immutable: to change one, delete it and create another.
What a key can reach
A key is mounted at /api/key/protected, which wraps the entire protected router — the same 479 endpoints the web app uses. But the two mounts differ in one significant way:
router.use("/protected", authenticate, authorize, require("./protected"));
router.use("/key/protected", authenticateAPIKey, require("./protected"));
The key path does not run authorize. Concretely:
| Check | Session JWT | API key |
|---|---|---|
| Company scoping | ✅ | ✅ |
| Active subscription required | ✅ | ✅ |
Role permission matrix (permission[module][METHOD]) | ✅ enforced | ❌ not enforced |
| Plan feature gating (feature not in plan → 403) | ✅ enforced | ❌ not enforced |
Plan usage limits (enforcePlanLimit on POST) | ✅ enforced | ❌ not enforced |
Member status must be owner/accepted | ✅ | ❌ not checked |
:::warning A key is not scoped by role
Even if the member who created the key has a restricted role, the key reaches every endpoint. Role permissions, plan feature gating and plan usage caps are all applied by authorize, which the key path skips. Treat an API key as full company-level access and issue it accordingly.
:::
Subscription is still required
authenticateAPIKey resolves the company's primary active subscription and reads its plan off it, and most of the router sits behind a further subscriptionCheck. Without an active subscription, authentication itself fails.
:::caution It fails as a 500, not a clean error
The lookup returns null and the plan is read off it without a guard, so a lapsed company gets 500 / Cannot read properties of null (reading 'planDetails') rather than SubscriptionExpired_400. Treat that specific 500 as "subscription problem", not as an outage.
:::
A small set of endpoints is mounted above that subscriptionCheck and stays reachable without one — so a lapsed tenant can still pay:
/plans · /subscription · /razorpay · /stripe · /paypal · /payu · /phonepe · /manual-payment · /coupons · GET /profile · GET /settings · GET /file · POST /upload · GET|PUT /company
Known gaps to design around
These are properties of the current implementation, not of the documented contract. Build as though they will be closed.
ipRestriction is not enforced. The field is stored on the key and returned, but no middleware reads it. Setting restricted does not currently restrict anything. Do not rely on it as a control — put the key behind your own egress rules instead.
type: "temporary" does not expire on this path. durationMinutes is validated on creation and honoured by the MCP endpoint, but authenticateAPIKey never checks it. A temporary key used against /api/key/protected behaves as permanent. Rotate on your own schedule; delete the key when you are done with it.
A key outlives the membership that minted it. The REST key path does not check member.status. Removing someone from the company does not invalidate keys they created — delete those keys explicitly as part of offboarding.
GET /api/:id returns the key in full. Unlike the list endpoint, the single-document read does not project key out. Anyone holding any key of the company can read every other key of that company in plaintext.
Rotation
POST /apito mint the new key.- Deploy it to your integration.
DELETE /apiwith the old key's_idonce traffic has moved.
Keys carry createdAt/updatedAt and a populated createdBy member, so GET /api is a usable audit list of what is outstanding.
Key format
New keys are 32 random bytes hex-encoded — 64 characters, [0-9a-f]. Keys minted before that change were two Math.random().toString(36) slices; they remain valid but carry far less entropy. If you hold a short, mixed-alphanumeric key, rotate it.