---
title: "Update a service credential"
method: PATCH
path: "/credentials/{credentialId}"
tags: ["Credentials"]
---

# Update a service credential

`PATCH /credentials/{credentialId}`

## Request

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

Rename a credential, retarget its agent identity, change its expiry, or change its grant. At least one field must be present.

A new `scopes` array replaces the grant outright; it is not merged. It is bounded by what the caller holds, exactly as it is on creation, so a credential editing itself can only ever narrow. An attempt to widen answers 403 `scope_escalation` and names the refused scopes.

Setting `expiresAt` to `null` removes the expiry; setting it to a timestamp in the past is refused. Omitting it leaves the expiry alone.

Supplying an explicit grant also clears `isLegacy`: once an operator has said what a grandfathered credential should hold, it is no longer grandfathered.

A revoked credential cannot be updated and answers 409 `credential_state`. Accepts an `Idempotency-Key`; a retry with the same key replays the original response instead of re-applying the update.

## Headers

- `Idempotency-Key` string

## Request body

- CredentialUpdateRequest: Fields to change. At least one must be present. Omitted fields are left alone.
  - `name` string
  - `agentIdentity` string
  - `scopes` CredentialScope[]: Replaces the grant outright; not merged. Bounded by what the caller holds, so a credential editing itself can only narrow. Supplying this also clears `isLegacy`.
  - `expiresAt` string, date-time, nullable: A future timestamp to set an expiry, or null to remove one. A past timestamp is refused.
  - `limits` object: Spend ceilings to change. Each field is optional and only the ones present are written; null removes a ceiling. Bounded by the caller's own ceilings, exactly as `scopes` is bounded by the caller's own grant.
    - `maxCreditsPerDay` integer, nullable
    - `maxSendsPerHour` integer, nullable
  - `paused` boolean: True pauses this credential, false lifts the pause. A paused credential authenticates nothing and answers 401 `unauthorized`, like a revoked one, but its keys survive so lifting the pause needs no rotation.

## Response `200`

The credential as it now stands.

- object
  - `data` Credential, 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.

## 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).
- `404`: The resource does not exist, was deleted, or belongs to another workspace. Code: `not_found`. The three cases are answered identically on purpose, so a 404 never confirms that an identifier exists elsewhere.
- `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)
