Permissions and scope
API v3 authorization answers two questions, in this order: does the credential have the permission the operation requires, and is the requested resource within the scope of that permission? Without the permission, the API returns 403 permission_denied. With the permission but outside its scope, the API returns 404 not_found, the same response as for a resource that does not exist.
Permissions
Permission codes follow the pattern <plural resource>.<verb>: products.read, products.update, products.publish, users.create, webhooks.replay. Retrieve the full catalog for your version over HTTP:
GET /api/v3/credentials/permission-catalogEach entry includes the code, the resource type, a description, whether the permission can be delegated, and the release that introduced it. The catalog only grows: an existing code never changes meaning.
Three kinds of action require a dedicated permission rather than the generic write permission:
- State transitions: publishing a product requires
products.publishin addition toproducts.update. - Sensitive fields: personal user data in listings and reports requires
users.read_personal. Without it, the key is omitted from the response. - High-impact operations: revoking a certificate (
certificates.revoke), replaying a webhook (webhooks.replay), and adjusting progress (progress.adjust).
Each group page in the API reference lists the exact permissions required by each operation.
Policies, policy revisions, and selectors
A credential's access is defined by its policies. Together, they form a policy revision, and the one in effect is the active policy revision. You manage policies one at a time: add a policy (POST /credentials/{credential_id}/policies), update one (PATCH /credentials/{credential_id}/policies/{policy_id}), or remove one (DELETE /credentials/{credential_id}/policies/{policy_id}). Each change publishes a new policy revision that contains the full set of policies, and a policy keeps its policy_id across revisions.
When you update a policy, send the ETag from the retrieve operation in the If-Match header. If the credential changed in the meantime, the API returns 412 revision_mismatch and publishes nothing.
Each policy combines permissions with resource selectors:
{
"capabilities": ["products.read", "modules.read", "lessons.read"],
"resources": [
{ "type": "product", "selector": "ids", "ids": ["prd_12", "prd_34"] }
]
}| Selector | What it covers |
|---|---|
instance | The entire account, for permissions that have no resource of their own (settings, events, audit). |
all | Every resource of that type in the account. With include_future: false, it is frozen to the IDs that exist at publication time. |
descendants | Everything under a parent, set in parent (module, lesson, exam, certificate, and comment belong to product; enrollment belongs to user). With include_future: false, it is frozen the same way. |
ids | An explicit list, with at most 1,000 IDs. |
The selector type must match the permission's resource type. A module permission accepts a module selector by ids, descendants of a product, all, or instance.
How scope appears in responses
- Listings: the operation requires the permission with any selector, and the page contains only what is in scope. There is no error; the collection is smaller.
- Single resources: a resource outside the scope returns
404. - Human mode: the policy is always the credential's own. The administrator behind the access token determines authorship and eligibility, never additional access. See Human mode (OAuth).
Delegation
A credential with credentials.policies.manage can manage the policies of another credential, as long as what it grants is contained in what it has itself, and only with permissions marked as delegable in the catalog. Going beyond that returns 403 delegation_limit_exceeded, and no new revision is published. Credential management permissions (credentials.*) and administrator permissions (administrators.*) are not delegable, and a credential cannot modify its own policies.
Large policies
- Prefer
descendantsandallover ID lists, withinclude_future: true: the policy stays smaller, covers resources created later, and does not need to be republished. - An
idsselector with more than 1,000 IDs is rejected. If you want "everything that exists today", useallwithinclude_future: falseand let the API resolve the IDs. The same 1,000 limit applies to the resolved set. - A revision with many policies is normal; what matters is the set, not the order.
GET /credentials/current/policiesreturns the active policy revision of the calling credential, which is useful for debugging a403before you change anything.
Rejections
| Code | When |
|---|---|
permission_denied | The credential lacks the permission for the operation, field, or transition. |
not_found | The credential has the permission, but the resource is outside its scope (or does not exist). |
delegation_limit_exceeded | The requested policy goes beyond what the issuing credential has. |
validation_failed | A permission is not in the catalog, a selector is incompatible, an ID does not exist, or a selector exceeds 1,000 IDs. |
revision_mismatch | The If-Match value does not match the credential's current revision. |