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:

  • uid identifies the authenticated issuer that created the row.
  • tenant is the nil UUID for a personal key or the selected organization UUID.
  • issued_for and recipient_context are immutable attribution and scoping references. They do not change the owner, tenant, or authorization principal.
  • id is the API-key record UUID used by management routes. It is separate from the generated api_key credential.

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:

  1. Create a disabled key with /api_keys/insert.
  2. Store the returned api_key immediately; it cannot be retrieved later.
  3. Configure expiry, a use allowance, IP entries, or metadata with /api_keys/set_knobs when needed.
  4. Activate the key with /api_keys/update_status.
  5. Pause it by setting status to disabled, or permanently end it with /api_keys/revoke.
  6. Permanently remove a revoked record with /api_keys/delete when 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_from on 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.