---
title: "Create delegation"
method: POST
path: "/api/v1/delegations"
tags: ["Delegations"]
---

# Create delegation

`POST /api/v1/delegations`

## Request

- Base URL: `https://api.proof.holdings`
- URL: `https://api.proof.holdings/api/v1/delegations`
- Auth: one of:
  - HTTP bearer
  - API key in cookie `auth_token`

Issue a delegation: your proven domain (the `control_proof`) authorizes an artifact — a package (`purl`) or an https endpoint (`url`) — for a set of capability scopes. Mints a signed delegation token (ES256 — same key and JWKS as proof tokens) carrying `principal`, `delegate`, `scope`, `control_proof` and its own revocation handle. The token is what you publish; the record is what you revoke. A requested lifetime longer than the control proof's remaining one is rejected, never truncated. Each mint consumes 1 proof from your plan quota (re-issuance included).

## Request body

- CreateDelegationRequest: Create a delegation from a proven domain to an artifact
  - `control_proof` string, required: Public handle (`ph_ctl_<32 hex>`) of YOUR active domain proof — the authority the delegation chains down to
  - `delegate` DelegateIdentifier, required: Typed delegate identifier
    - `type` 'url' | 'purl', required: Delegate identifier type: `purl` for a package (`pkg:npm/postmark-mcp`), `url` for an https endpoint
    - `value` string, required: The identifier. Canonicalized per type at creation: purl version/qualifiers/subpath are rejected (delegate the artifact, not a version); urls must be https with no credentials, query or fragment
  - `scope` string[], required: Capability tokens: lowercase kebab-case, 1-32 entries, at most 64 characters each (e.g. `send-email`). These are NOT API scopes — an API-style `resource:action` such as `payments:read` is rejected with `400 invalid_scope`; an over-long or over-count list is rejected by the request schema as `validation_error`. Deduplicated and sorted at creation; verifiers compare by exact string match
  - `expires_in` integer: Lifetime in seconds. Defaults to the shorter of one year and the control proof remainder. A request LONGER than the control proof remainder is rejected (400), never silently truncated

## Response `201`

Delegation created — the response carries the publishable signed token AND the same derived `effective_status`/`is_valid` the reads carry, so the shape of a delegation does not depend on which verb produced it (m-fix-proof-api-consistency)

- DelegationDetail: A delegation with its derived effective status
  - `id` string, required: MongoDB ObjectId
  - `delegation_id` string, nullable, required: Stable public handle (`ph_dlg_<32 hex>`) — also the token's `sub`, and accepted by the proof status endpoint
  - `principal` string, required: The proven domain that authorized the delegate — derived from the control proof, never client input
  - `delegate` DelegateIdentifier, required: Typed delegate identifier
    - `type` 'url' | 'purl', required: Delegate identifier type: `purl` for a package (`pkg:npm/postmark-mcp`), `url` for an https endpoint
    - `value` string, required: The identifier. Canonicalized per type at creation: purl version/qualifiers/subpath are rejected (delegate the artifact, not a version); urls must be https with no credentials, query or fragment
  - `scope` string[], required
  - `control_proof` string, required: The control proof handle this delegation points down to
  - `status` 'active' | 'revoked', required: The owner's OWN record — whether they revoked this delegation themselves. Nothing else writes it: neither the control proof dying nor this delegation's own lifetime running out. Liveness is DERIVED and reported separately as `effective_status`. This stored field is also what `?status=` filters on
  - `revoked_at` string, date-time, nullable, required
  - `revoked_reason` string, nullable, required
  - `environment` 'production' | 'test', required
  - `issued_at` string, date-time, required
  - `expires_at` string, date-time, required: Never later than the control proof's own expiry at issuance (effective expiry is min of the two)
  - `created_at` string, date-time, required
  - `token` string, nullable, required: The signed delegation JWT — the PUBLISHABLE artifact (publish it in your MCP server card under `_meta` → `holdings.proof/delegation`). Same key, algorithm and JWKS as proof tokens
  - `effective_status` 'active' | 'revoked' | 'expired' | 'suspended', required: DERIVED liveness, computed at read time from BOTH clocks that govern a delegation: the control proof it stands on (cascade rule a — revoked, suspended or expired flows through) and its own `expires_at` (rule b — effective expiry is the earlier of the two). Either one running out reports `expired` here while the delegation's own `status` stays untouched. This is the field to render — `status` alone would show a live badge on a delegation every verifier already rejects
  - `is_valid` boolean, required: True only when `effective_status` is `active`. It is a restatement of that field, not a second opinion: expiry is decided once, inside the derivation above

## Other responses

- `400`: `invalid_delegate` (the artifact could not be canonicalized), `invalid_scope` (a scope entry is not a lowercase kebab-case token — an API-style `payments:read` is the common mistake), `control_proof_not_active`, `control_proof_not_domain`, or `delegation_lifetime_exceeds_control_proof`. The code names WHICH input to change. A scope entry longer than 64 characters, more than 32 entries, or an empty array is caught by the request schema BEFORE the controller and answers `validation_error` instead — `invalid_scope` covers the token GRAMMAR
- `402`: card_required. Only once billing is live: a free account with no card on file that has used its included proofs and the 50-proof grace this month is refused new proof requests until a card is added (add_card_url). Proofs already issued keep working. The error carries add_card_url, used, included and hard_limit.
- `403`: `account_key_required` — this API key is bound to one sub-account; account-level changes need an account-wide key or the dashboard.
- `404`: Control proof not found (uniform — a foreign proof is indistinguishable from a missing one)
- `409`: `domain_revoking` — the domain's revocation cascade is running (h-implement-proper-deletion, DOM-1); nothing is minted for it — retry in a moment
- `503`: Delegation issuance temporarily unavailable (registry unreachable — issuance fails closed)

---

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