Summary
- An API key is a workspace-level credential. It is not a person, and it is not governed by the role scopes that limit people in the app.
- A request is allowed when the key is valid and not revoked or expired, its type allows the request, and it holds the scope the endpoint declares.
- Role scopes (assigned-only jobs, department and region limits) do not apply
to API requests. A key with
candidates:readreads every candidate in the workspace, including candidates the key’s creator could not open in the app.
How a request is authorized
- Key lookup. The key is matched by its hash. Malformed, unknown, revoked,
and expired keys are rejected with
401. - Key type. Secret keys (
harly_sk_…) must be sent in theAuthorizationheader orx-api-key; they are refused in the query string. Publishable keys (harly_pk_…) may also be sent as thepkquery parameter, because the embed widget runs in the browser. A publishable key can hold onlyjobs:readandapplications:write. Requested scopes that a key type may not hold are dropped when the key is created. - Endpoint scope. Each endpoint declares one required scope. A key must
hold it.
webhooks:manageis a legacy umbrella that satisfies bothwebhooks:readandwebhooks:write.GET /merequires no scope; it only reports the key’s own scopes. - Rate limit. Each key has its own budget of 1,000 requests per 10 minutes (120 on the public demo). The limit is checked before any work runs.
- Work. The endpoint’s service runs against the key’s workspace. No role scope, job assignment, or hiring-team membership is checked.
- A key can only be created with a secret key, and only with scopes that the
creating key itself holds (
api_keys:write). In the app, creating a key requiresintegrations:manage. - Rotation is refused when the new key would carry scopes the rotating key does not hold.
Who a write is attributed to
Writes need a workspace user for attribution, such as the creator of a job or a note. Harly resolves that user as follows (server/api/actor.ts):
- The user who created the key, if that user is still a member of the workspace, whatever role they now hold.
- Otherwise, the first workspace owner.
422.
The activity trail records the resolved user as the actor. It does not record
which key made the call, so an API write looks the same as a write by that
person in the app.
Scope inventory
The table lists every v1 endpoint and the scope it requires. No v1 route or contract applies a role scope, and the services those routes call do not either.Known limitations in 0.2.4
- Workspace-wide reach. Key scopes are the only restriction. A key with a read scope sees all records of that kind in the workspace. Role scopes that limit people in the app (assigned jobs, departments, regions) do not limit keys.
- Keys outlive their creator’s access. Removing a member, or changing their role, does not change the keys they created. Removal deletes the membership row only; keys stay valid with their full scopes.
- Attribution falls back to an owner. Once the creator is no longer a member, the key’s writes are attributed to the first owner. That owner is credited with actions they did not take.
- Keys are not identified in the trail. Activity and audit records carry the attributed user, not the key id.
- Publishable keys are public. Their two scopes are meant for browsers.
applications:writelets a browser holding the key create applications, so the key should be treated as public and limited by rate limits only. - Legacy umbrella scope.
webhooks:manageis still accepted and still grants both webhook scopes.
Recommended hardening for a later release
These steps are planned for a later release. They change behavior, so they need a migration path for existing keys.- Give each key a principal. Store the creator’s role and scope on the key when it is created. On every request, apply the same job, candidate, and task scope predicates the app uses, so list endpoints filter results and by-id endpoints refuse out-of-scope records.
- Re-check keys when people change. Suspend or revoke a key when its creator is removed or loses a permission the key relies on. Show the creator and last-used time on each key in Settings.
- Stop attributing to owners. Fail a write when the creator is gone, rather than attributing it to an owner. Attribute writes made with a key to a service actor that records the key id.
- Record the key on every write. Put the key id in activity and audit metadata so the trail can tell a key write from a person’s own action.
- Tighten key management. Limit
api_keys:writeand key creation to owners by default, and retirewebhooks:managefor newly created keys. - Test by scope. Add integration tests that run each endpoint family with a scoped key and assert that out-of-scope records are absent or refused.