API key management
Use this API family to create and manage API keys for a consumer account or an organization. The management API controls ownership, lifecycle, descriptive fields, and optional restrictions. A generated key is returned only once.
API keys do not authenticate this management API. Call these routes with an M7 SSO access token accepted by the API User authorization profile. To validate a complete API key in a receiving service, use the SSO API-key verification endpoint.
Access, ownership, and identifiers
Personal keys belong to the signed-in consumer user. Organization keys belong to an active organization where the signed-in user has current Management-group access. Organization-member tokens do not manage keys through this family.
The service fixes ownership at creation:
uididentifies the authenticated issuer that created the row.tenantis the nil UUID for a personal key or the selected organization UUID.issued_forandrecipient_contextare immutable attribution and scoping references. They do not change the owner, tenant, or authorization principal.idis the API-key record UUID used by management routes. It is separate from the generatedapi_keycredential.
Omit tenant, send null, or send the nil UUID for the personal context. Send
an organization UUID only when the caller currently manages that active,
unarchived organization. Invalid or inaccessible organization contexts fail;
they do not fall back to personal ownership.
Use /api_keys/tenants to list the caller's available personal and active
organization contexts. Use /api_keys/tenant-info with optional tenant to
inspect and validate one context.
Authentication scopes
Every route uses the API User Bearer-token authorization profile. Caller applications subject to route-scope checks need the configured scope below. The service applies its existing dot-delimited prefix comparison; these names do not introduce a wildcard grammar.
| Route | Configured scope |
|---|---|
/api_keys |
api_keys |
/api_keys/insert |
api_keys.insert |
/api_keys/view |
api_keys.view |
/api_keys/search |
api_keys.search |
/api_keys/update |
api_keys.update |
/api_keys/update_status |
api_keys.update_status |
/api_keys/set_knobs |
api_keys.set_knobs |
/api_keys/set_tags |
api_keys.set_tags |
/api_keys/tags/search |
api_keys.tags.search |
/api_keys/tags/insert |
api_keys.tags.insert |
/api_keys/tags/update |
api_keys.tags.update |
/api_keys/tags/delete |
api_keys.tags.delete |
/api_keys/revoke |
api_keys.revoke |
/api_keys/delete |
api_keys.delete |
/api_keys/rotate |
api_keys.rotate |
/api_keys/tenants |
api_keys.tenants |
/api_keys/tenant-info |
api_keys.tenant_info |
All routes in this guide use POST under
https://api.user.m7.org/api/v2.
Lifecycle
A safe setup sequence is:
- Create a disabled key with
/api_keys/insert. - Store the returned
api_keyimmediately; it cannot be retrieved later. - Configure expiry, a use allowance, IP entries, or metadata with
/api_keys/set_knobswhen needed. - Activate the key with
/api_keys/update_status. - Pause it by setting
statustodisabled, or permanently end it with/api_keys/revoke. - Permanently remove a revoked record with
/api_keys/deletewhen its history is no longer needed.
The stored lifecycle states are active, disabled, and revoked. An active
key whose use allowance is exhausted is displayed as exhausted by search,
but its stored status remains active. Revocation is terminal: a revoked key
cannot be reactivated, edited, rotated, or configured. Deletion accepts only an
already-revoked row.
Routes
| Route | Required fields | Purpose |
|---|---|---|
/api_keys/insert |
name |
Creates a disabled key and returns the credential once. |
/api_keys/view |
id |
Returns one accessible, non-secret management record. |
/api_keys/search |
None | Lists keys in the selected tenant context. |
/api_keys/update |
id |
Partially updates main descriptive fields and optional audience/scope lists. |
/api_keys/update_status |
id, status |
Sets active or disabled; cannot revive a revoked key. |
/api_keys/set_knobs |
id plus at least one knob |
Partially updates typed optional controls. |
/api_keys/set_tags |
id, tag_ids |
Replaces the key's tag assignments; [] clears them. |
/api_keys/tags/search |
id |
Lists the API-key tag pool derived from an accessible key. |
/api_keys/tags/insert |
id, name |
Creates a tag in that pool; optional slug and description. |
/api_keys/tags/update |
id, tag_id |
Updates supplied name, slug, or description fields. |
/api_keys/tags/delete |
id, tag_id |
Deletes an unassigned tag from that pool. |
/api_keys/revoke |
id |
Terminally revokes a key; optional reason records why. |
/api_keys/delete |
id |
Permanently deletes an already-revoked row and its child settings. |
/api_keys/rotate |
id |
Atomically revokes the old key and returns one replacement credential. |
/api_keys/tenants |
None | Lists accessible personal and active organization contexts. |
/api_keys/tenant-info |
None | Returns the personal context or the selected active organization context. |
Create a key
insert accepts only creation fields. The service generates the record ID,
credential selector, secret, secret hash, status, counters, and audit fields.
Callers cannot supply them.
| Field | Required | Type | Behavior |
|---|---|---|---|
name |
Yes | String | Human-readable name, up to 191 characters. |
notes |
No | String or null |
Description for people managing the key. |
tenant |
No | UUID | Omit for personal ownership; an organization must be active and manageable by the caller. |
issued_for |
No | String | Opaque attribution reference; defaults to the issuer's uid. |
recipient_context |
No | String | Opaque context reference; defaults to platform. |
allowed_audiences |
No | Array of strings | Optional audience entries. Omission or [] creates no configured audience restriction. |
allowed_scopes |
No | Array of strings | Optional scope entries. Omission or [] creates no configured scope restriction. |
Audience and scope lists are independent and elective. Supplying one never
requires the other. An empty list means that restriction is unconfigured; it
does not grant authority beyond the key's owner and tenant. Receiving services
decide how to apply configured restrictions. They may also ask SSO verification
to require a personal owner or organization tenant; that check
does not change ownership or management access. Each audience entry may contain
up to 255 characters. Each scope entry may contain up to 191 characters and is
normalized to lowercase. Duplicate entries are removed case-insensitively.
curl --request POST \
'https://api.user.m7.org/api/v2/api_keys/insert' \
--header 'Authorization: Bearer ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"name": "Example integration",
"notes": "Used by the reporting worker",
"allowed_audiences": ["https://api.example.m7.org"],
"allowed_scopes": []
}'
The response uses Cache-Control: no-store. Its data.record is the disabled
management row. data.api_key is the only usable credential copy:
{
"status": 1,
"comment": "INSERT",
"data": {
"record": {
"id": "API_KEY_RECORD_UUID",
"name": "Example integration",
"status": "disabled",
"tenant": "00000000-0000-0000-0000-000000000000",
"allowed_audiences": ["https://api.example.m7.org"],
"allowed_scopes": [],
"uses_used": 0
},
"api_key": "m7ak_1_<selector>_<secret>"
}
}
Store the credential in a secret manager before leaving the response. Never put it in a URL, source file, log, analytics event, or support message.
View and search
view takes the management row id and returns an accessible record without
the generated credential or its stored hash. Management records include owner
and tenant context, attribution, status, expiry, use counters, revocation and
rotation references, independent audience/scope/IP lists, typed knobs, assigned
tags, and timestamps.
search defaults to the personal tenant. Send tenant for an accessible
organization. Ownership and tenant constraints are applied before filtering,
counting, and pagination.
Useful search fields include:
| Field | Behavior |
|---|---|
q |
Case-insensitive substring search over visible identity, description, attribution, audience, scope, and IP fields. Metadata is excluded. |
display_status |
all, active, exhausted, disabled, or revoked. |
status |
Stored status filter: active, disabled, or revoked. |
id, name, issued_for, recipient_context, expires, rotated_from |
Exact stored-field filters. |
tag_id |
Includes only keys assigned to this tag in the selected context. |
include_tag_facets |
With pagination and true, adds tag_facets containing in-use tags and usage_count. |
limit, page_number, offset, cursor |
Shared pagination controls. |
When any pagination field is present, API-key search uses a default and maximum
limit of 50 and returns:
{
"status": 1,
"comment": "OK",
"data": {
"items": [],
"limit": 50,
"total_count": 0,
"next_offset": 50,
"page_number": 1,
"page_total": 0,
"next_cursor": null
}
}
Tag counts are calculated across accessible keys matching the other supplied filters, including text and status, before applying the selected tag and page limit. The selected tag is then applied before pagination totals. A missing or out-of-context tag matches no keys. Counts never include another owner or organization.
Without pagination fields, data is the legacy raw array. Treat
next_cursor as opaque and send it unchanged on the next request.
Organize keys with tags
API-key tags use the same name, slug, and optional description model as application-registration tags, with a separate tag pool. A personal API-key pool belongs to the user; an organization API-key pool belongs to that organization and is shared by its authorized managers. OAuth application tags and API-key tags are separate.
Every pool route takes an accessible API-key record id. The server derives
the pool from that record; callers cannot override its owner or organization.
Pool mutations and assignment require a nonrevoked key. Revoked keys keep their
assigned tags for viewing and filtering, but cannot be used to modify them.
Create a tag with /api_keys/tags/insert:
{
"id": "API_KEY_RECORD_UUID",
"name": "Reporting",
"slug": "reporting",
"description": "Keys used by reporting jobs"
}
Omitting slug derives it from the name. Slugs must be unique within the pool.
Insert and update return the tag record as data. Updating a shared tag changes
its label wherever it is assigned in that pool; it does not move the tag into
a different pool.
Assign tags with /api_keys/set_tags:
{
"id": "API_KEY_RECORD_UUID",
"tag_ids": ["TAG_RECORD_UUID"]
}
This replaces the complete assignment list. Send [] to remove all assignments;
omitting tag_ids is an error. Every supplied ID must belong to the same pool,
and duplicate IDs are rejected. The response data is the updated management
record, including its tags array.
Pool search accepts optional exact tag_id, slug, and name filters and the
shared offset/cursor pagination inputs. Its default limit is 50 and maximum is
100. Its data contains scope, tags, limit, offset, has_more,
next_offset, and next_cursor; continue until has_more is false.
Pool deletion fails while any key still uses the tag. Remove assignments before deleting it. Rotation copies assignments to the replacement in the same transaction and preserves the predecessor's tags; permanent key deletion clears that key's assignments. A failed tag copy rolls back the rotation.
Tags are management metadata. They grant no permissions, are not verifier constraints, and are not included in the key-only verification profile. Use optional audience/scope expectations when the receiving service needs those checks.
Update main fields
Use /api_keys/update for name, notes, allowed_audiences, and
allowed_scopes. This is a partial update: omitted fields stay unchanged.
Sending [] explicitly clears the corresponding audience or scope list.
The route cannot change id, uid, tenant, issued_for,
recipient_context, credential material, lifecycle audit fields, use counters,
or rotation lineage. Use the typed routes for lifecycle and knobs; arbitrary
configuration names are not accepted through the public API.
curl --request POST \
'https://api.user.m7.org/api/v2/api_keys/update' \
--header 'Authorization: Bearer ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"id": "API_KEY_RECORD_UUID",
"name": "Reporting integration",
"allowed_scopes": ["reports.read"]
}'
Configure typed knobs
/api_keys/set_knobs accepts only these fields. Omitted knobs remain
unchanged, so send only the settings being changed.
| Field | Type | Behavior |
|---|---|---|
expires |
Absolute date-time or null |
null means Never. ISO 8601 with an offset is accepted; YYYY-MM-DD HH:MM:SS is interpreted as UTC. |
use_limit |
Positive integer or null |
null means Unlimited. Zero and negative values are rejected. |
ip_whitelist |
Array of IP/CIDR strings | [] clears the list. Bare IPv4/IPv6 addresses are stored as /32 or /128. |
metadata |
JSON object or null |
Customer metadata; null clears it. Metadata is descriptive and grants no authority. |
expires, use_limit, audience, scope, and IP entries are optional configured
restrictions. The key-only verifier always enforces a configured IP whitelist
against its direct connection address. A receiving service can also supply one
expected audience and a whitespace-delimited set of required scopes. Audience
matching is exact and case-sensitive; requested scope tokens are normalized to
lowercase and must all exactly match configured scopes. There is no wildcard or
prefix grant. Omitting audience or scope skips that check, while supplying a
check against an empty stored list fails.
Verification accepts separate optional ownership expectations. owner accepts
a consumer UUID or canonical M7 username, or an any-match list of those
values, and matches personal keys only. Every organization key fails a supplied
owner check, even when it contains the organization UUID or issuing user's
identity. tenant accepts a non-nil organization UUID or an any-match list of
such UUIDs and matches organization keys only. Organization uid remains
issuer attribution. Neither check changes ownership or management access.
Both expectations accept JSON scalar/list input and form scalar or repeated
unindexed list input (owner[] or tenant[]). Each supplied list is limited to
32 entries before deduplication. Owner entries are limited to 255 trimmed
bytes; tenant entries must be dashed non-nil UUIDs. If both expectations are
supplied, both must pass, so the result is invalid for either key kind. Omitting
both preserves key-only verification. A mismatch consumes no use and discloses
no profile. See the
SSO verification contract
for the complete input and error rules.
Each successful verification increments uses_used, including for an
unlimited key, and sets last_used_at in UTC. Failed credential, lifecycle,
IP, audience, or scope checks do not update either field.
curl --request POST \
'https://api.user.m7.org/api/v2/api_keys/set_knobs' \
--header 'Authorization: Bearer ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"id": "API_KEY_RECORD_UUID",
"expires": "2030-01-15T12:00:00Z",
"use_limit": 5000,
"ip_whitelist": ["192.0.2.40", "2001:db8::/48"],
"metadata": {"environment": "example"}
}'
Activate, pause, revoke, and delete
Activate or pause a nonrevoked key with /api_keys/update_status:
{
"id": "API_KEY_RECORD_UUID",
"status": "active"
}
Only active and disabled are accepted by this route. Activation of an
organization key revalidates that the organization is active and manageable.
Revoke permanently with an optional reason:
{
"id": "API_KEY_RECORD_UUID",
"reason": "Credential was replaced"
}
After revocation, /api_keys/delete with the same id permanently removes the
row and its audience, scope, IP, metadata, and tag assignments. Shared tag
definitions remain in their pool. Active and disabled
keys must be revoked before deletion.
Rotate a key
/api_keys/rotate takes the current row id. Rotation is an atomic
replacement rather than an overlap period:
- it creates a new row with a new ID and new one-time credential;
- it preserves owner, tenant, attribution, active/disabled status, expiry, remaining use state, IP entries, audience/scope lists, metadata, and tag assignments;
- it records
rotated_fromon the replacement; and - it terminally revokes the predecessor in the same operation.
The response uses Cache-Control: no-store and has the same
data.record/data.api_key shape as insertion. Store the replacement before
discarding the response. Rotation cannot revive or rotate an already-revoked
row.
Errors
Failures use the standard API User response envelope.
Handle its application status and the HTTP status separately; treat
comment as diagnostic prose rather than a machine protocol.
Requests fail when required fields are missing or malformed, list or typed-knob values fail validation, or the selected organization is invalid, inactive, or outside the caller's current management access. A record ID owned by another personal user or inaccessible organization is not exposed. Revoked rows reject editing, status changes, knob changes, and rotation, while active or disabled rows reject permanent deletion. Re-read the accessible record before deciding whether to retry a lifecycle action.
Verification boundary
The receiving service can submit the complete API key to
POST https://sso.user.m7.org/api-key/verify. The complete key is its only
credential; no API.User management access token or second credential is
required. Optional owner and tenant expectations pin personal and organization
keys respectively.
Unknown, wrong-secret, disabled, revoked, expired, or exhausted keys return
one uniform invalid result; a valid key returns a safe allowlisted dataset.
Malformed requests return HTTP 400.
Successful verification atomically consumes one use, even when use_limit is
Unlimited, records last_used_at in UTC, and returns the updated count and
time. The final available use succeeds; the next request returns
valid: false. A configured IP whitelist is enforced against the verifier's
direct connection address. Optional supplied audience and scope expectations
are enforced as described above. owner applies only to personal keys; any
organization key fails it. Use tenant to require a particular organization
UUID. Each supports an any-match list. These checks do not reinterpret
issued_for or the issuer. Failed checks do not consume a use.
The verification transaction commits before the caller receives the HTTP response. A lost response or later resource-processing failure does not refund the use, so do not call verification repeatedly for one resource request. Verification still does not assert that the owner currently has permission to perform the receiving service's operation. Use the returned owner context and policy as inputs to that service's own documented authorization decision. See the SSO verification contract for request transports and response details.