---
title: "Mint a service credential"
method: POST
path: "/credentials"
tags: ["Credentials"]
---

# Mint a service credential

`POST /credentials`

## Request

- Base URL: `https://inkseal.net/api/v1`
- URL: `https://inkseal.net/api/v1/credentials`
- Auth: HTTP bearer

Mint a credential and its first secret, bound to the caller's workspace.

**The raw key is returned exactly once, in this response, and can never be read again.** Only its SHA-256 hash is stored, alongside the prefix and last four characters used to recognise it in a list. If the response is lost, the only remedy is to rotate or to mint a new credential. Write the `key` field to your secret store before doing anything else with it.

The key format is `isk_` followed by 40 base62 characters, 44 characters in total. The `isk_` prefix is there so a leaked key is greppable. Present it as `Authorization: Bearer isk_...`.

`scopes` is the grant and is required: at least one, from the ten-value vocabulary, deduplicated and returned in vocabulary order. **A caller can never grant a scope it does not itself hold.** This is enforced, not advisory: a request for a wider grant is refused with 403 `scope_escalation`, and the error names the offending scopes. A workspace admin or owner holds all ten, so the rule is invisible to them; a service credential minting another credential is bounded by its own grant, which means an agent trusted to manage credentials still cannot mint one broader than itself.

`expiresAt`, when given, must be in the future. A workspace holds at most 10 credentials; exceeding that answers 409 `limit_exceeded`.

Accepts an `Idempotency-Key`. A retry with the same key replays the original response verbatim, including the one-time key, instead of minting a second credential.

## Headers

- `Idempotency-Key` string

## Request body

- CredentialCreateRequest: A new credential. The workspace comes from the caller's own binding and cannot be set here.
  - `name` string, required: A human label. Trimmed; must not be blank after trimming.
  - `agentIdentity` string, required: Who or what will act under this credential. Recorded on the audit rows it writes.
  - `scopes` CredentialScope[], required: The grant. At least one scope is required: a credential that can do nothing still authenticates, which makes it a confusing dead end. Every scope requested must be one the caller already holds, or the request is refused with 403 `scope_escalation`.
  - `expiresAt` string, date-time: Optional expiry. Must be in the future. Omit for a credential that does not expire on its own.
  - `limits` CredentialLimits: The spend ceilings on a credential. Null on either means no ceiling. They are enforced in the transaction that claims an envelope for send, so concurrent sends cannot cross one, and a refusal answers 429 `credential_limit_exceeded` having sent nothing.
    - `maxCreditsPerDay` integer, nullable, required: Credits this credential may spend in one UTC day. Counted in credits rather than sends, so a bigger envelope draws more of it down, and counted only on a metered workspace.
    - `maxSendsPerHour` integer, nullable, required: Envelopes this credential may send in one UTC hour, whether or not the workspace is metered.

## Response `201`

The credential, plus the only copy of its raw key that will ever exist. Store `data.key` now; it is not recoverable.

- object
  - `data` CredentialIssued, required: A service credential: an identity, a grant, and the keys issued for it. Raw key material never appears here.
    - `id` string, required: The credential's public identifier, stable across rotations.
    - `name` string, required: A human label, 1 to 255 characters.
    - `agentIdentity` string, nullable, required: Who or what acts under this credential. Recorded on every audit row the credential writes, so an action can be traced back to the agent rather than to the human who issued the key.
    - `scopes` CredentialScope[], required: The grant, deduplicated and ordered by the scope vocabulary. Never empty.
    - `isLegacy` boolean, required: True for a credential grandfathered by the scoping migration, which holds the full scope set. Writing an explicit grant clears it.
    - `isActive` boolean, required: False once revoked. A revoked credential still appears in listings so the audit trail stays legible.
    - `createdAt` string, date-time, required
    - `expiresAt` string, date-time, nullable, required: When the credential itself stops authenticating, if an expiry was set.
    - `revokedAt` string, date-time, nullable, required: When the credential was revoked.
    - `lastUsedAt` string, date-time, nullable, required: When any of this credential's keys last authenticated a request.
    - `secrets` CredentialSecretSummary[], required: The keys issued for this credential, newest first. Ordinarily one; two while a rotation grace window is open.
      - `prefix` string, required: The first 8 characters of the key: `isk_` plus the first four characters of its body.
      - `lastFour` string, required: The last four characters of the key.
      - `createdAt` string, date-time, required
      - `expiresAt` string, date-time, nullable, required: Set only while a rotated-out secret serves its 24-hour grace window. Null for the current secret.
      - `revokedAt` string, date-time, nullable, required: When this key stopped working, either because the credential was revoked or because a second rotation cut its grace window short.
      - `lastUsedAt` string, date-time, nullable, required: When this key last authenticated a request. Null if it never has.
    - `limits` CredentialLimits, required: The spend ceilings on a credential. Null on either means no ceiling. They are enforced in the transaction that claims an envelope for send, so concurrent sends cannot cross one, and a refusal answers 429 `credential_limit_exceeded` having sent nothing.
      - `maxCreditsPerDay` integer, nullable, required: Credits this credential may spend in one UTC day. Counted in credits rather than sends, so a bigger envelope draws more of it down, and counted only on a metered workspace.
      - `maxSendsPerHour` integer, nullable, required: Envelopes this credential may send in one UTC hour, whether or not the workspace is metered.
    - `pausedAt` string, date-time, nullable, required: Set while the credential is paused. A pause is reversible and leaves the issued keys intact, unlike a revocation: a paused key answers 401 `unauthorized` and starts working again the moment the pause is lifted.
    - `key` string, required: **The raw key, returned once and never again.** Only its SHA-256 hash is stored, so nothing can read it back: if this response is lost the credential must be rotated or replaced. Format: `isk_` followed by 40 base62 characters, 44 in total. Send it as `Authorization: Bearer <key>`. The example below is redacted and will not authenticate.

## Other responses

- `400`: The request is malformed or the resource is in the wrong state for a synchronous check. Codes: `validation_error`, `invalid_body`, `invalid_parameter`, `invalid_idempotency_key`, `invalid_event_id`, `invalid_file`, `invalid_url`, `invalid_status`, `key_mismatch`, `no_signers`, `not_completed`, `envelope_exists`, `document_not_ready`, `upload_failed`.
- `401`: The credential is missing, malformed, unknown, expired, revoked, or past its rotation grace window. Code: `unauthorized`. Every authentication failure is deliberately indistinguishable.
- `403`: The credential is valid but may not do this. Codes: `insufficient_scope` (the route's scopes are not all held; `details` names `requiredScopes` and `missingScopes`), `forbidden` (the capability check on the resolved resource failed), `scope_escalation` (a grant wider than the caller's own was requested).
- `409`: The resource's state forbids this transition, or another request owns the work. Codes: `invalid_state`, `invalid_status`, `credential_state`, `limit_exceeded`, `envelope_missing`, `not_retryable`, `owner_unavailable`, `workspace_unavailable`, `detection_not_completed`, `document_in_use`, `version_conflict`, `document_processing`, `document_processing_failed`, `normalized_pdf_unavailable`, `pages_not_rendered`, `document_not_ready`, `idempotency_in_progress` (which also carries `Retry-After`), `payment_replayed` (a USDC payment signature presented twice), `payment_pending` (a USDC settlement whose outcome is not yet known).
- `422`: The request parsed but its content is not acceptable. Codes: `validation_error` (envelope configuration, with an `issues` array in `details`), `invalid_envelope`, `envelope_not_ready`, `document_page_limit`, `idempotency_key_reuse`.
- `429`: A limit was reached. Codes: `rate_limited` (the credential's per-bucket HTTP limit, or the per-workspace detection throttle), `workspace_limit_exceeded` (a workspace content or send limit; `details.limit` names it), `daily_budget_exceeded` (the workspace's daily detection budget). The four rate-limit headers are sent on this response and on no successful one, so a client cannot read its remaining budget without being refused. Retrying after `Retry-After` is always safe: a rate-limit refusal happens before the handler runs.
- `503`: A dependency the request needs is unreachable and nothing was changed. Codes: `queue_unavailable` (a background job could not be enqueued; retry the call), `billing_unavailable` (Stripe would not open a checkout session; nothing was charged, retry the call), `service_unavailable` (the rate-limit store is down, so the request was refused before running).

---

[API](https://skmtc.dev/inkseal/apis/inkseal-api.md) · [All operations](https://skmtc.dev/inkseal/apis/inkseal-api/llms.txt) · [OpenAPI document](https://skmtc.dev/inkseal/apis/inkseal-api/revisions/c440c981d578?raw)
