# Signing request lifecycle

A call to the [sign endpoint](/api-reference#tag/keys) passes through several stages before a signature is returned or the request is rejected. Understanding these stages helps you write resilient code and handle each outcome correctly.

## State flow

![Signing request state diagram: submitted, policy evaluation, approved, mpc\_signing, completed, and error terminal states](/img/diagrams/signing-request-states.svg)

```mmd
stateDiagram-v2
    [*] --> submitted : POST /v1/keys/{id}/sign

    submitted --> policy_evaluation : Orchestrator fans out to Policy Servers

    policy_evaluation --> blocked_by_freeze : Key / user / venue is frozen
    policy_evaluation --> blocked_by_policy : Caller lacks sign permission\n(Administrative Policy)
    policy_evaluation --> denied : Signing Policy rule matched Deny
    policy_evaluation --> approved : 2-of-3 Policy Servers sign Approve

    approved --> mpc_signing : Key Stores verify 2-of-3 pol sigs\nthen run MPC threshold-sign
    mpc_signing --> completed : Signature produced (200 OK)
    mpc_signing --> failed : Transient MPC error (500, safe to retry)

    blocked_by_freeze --> [*] : 403 returned
    blocked_by_policy --> [*] : 403 returned
    denied --> [*] : 403 returned
    completed --> [*] : Signature returned to caller
    failed --> [*] : 500 returned
```

*All terminal states return immediately. The sign endpoint is synchronous; there is no pending state in the API response.*

The sign endpoint is synchronous. Policy evaluation happens inside the request, and if it passes, MPC signing happens immediately. There is no `pending` state in the sign API response.

## Terminal states

| State | HTTP status | Meaning |
|---|---|---|
| `completed` | `200` | Signature produced and returned. |
| `rejected` (policy denied) | `403` | The Signing Policy denied the request. The `err_msg` field explains which rule matched. |
| `blocked_by_freeze` | `403` | The key, a user in the signing chain, or the venue is frozen. Unfreeze the relevant entity before retrying. |
| `blocked_by_policy` | `403` | The caller is not in a group permitted to call the sign endpoint by the Administrative Policy. |
| `failed` | `500` | MPC signing encountered a technical error. Safe to retry after a short backoff. |

The error response body carries an `err_type` field, not the state name: the three `403` states above (`rejected`, `blocked_by_freeze`, `blocked_by_policy`) all surface as `err_type: "forbidden"`, a `401` as `"unauthorized"`, and a `500` as `"internal_server_error"`. The handling example below switches on `err_type`.

## Async operations that produce a `request_id`

The sign endpoint itself does not return a `request_id`. If you receive a `request_id`, it is from a different operation (for example, [creating a key](/api-reference#tag/keys) or rotating an API key). [Poll the request status](/api-reference#tag/requests) with:

```
GET /v1/requests/{request_id}
```

This endpoint requires only the `Authorization` header. No nonce, timestamp, or assertion needed.

Responses:

* `202 Accepted`: the operation is still processing. Wait and poll again.
* `200 OK`: the operation completed. The body contains either the result or an error.

```json
{
  "status": "processing",
  "request_id": "abc123"
}
```

When completed:

```json
{
  "type": "user",
  "data": { /* CreateUserResponse */ }
}
```

The `type` field indicates which resource type the completed result contains. Check it before reading `data`.

## Handling sign errors in practice

Check the HTTP status code first. For `403`, read `err_type` to distinguish the reason:

```js
const res = await fetch(`${CV_BASE}/v1/keys/${keyId}/sign`, { /* ... */ });

if (res.status === 200) {
  const { signature } = await res.json();
  // use signature
  return signature;
}

const { err_type, err_msg } = await res.json();

if (err_type === 'unauthorized') {
  // Auth headers wrong or expired. Check timestamp clock skew and nonce uniqueness.
  throw new Error(`Auth failed: ${err_msg}`);
}

if (err_type === 'forbidden') {
  // Policy denied or entity frozen. Do not retry automatically.
  throw new Error(`Signing denied: ${err_msg}`);
}

if (err_type === 'internal_server_error') {
  // Transient MPC error. Safe to retry with exponential backoff.
  throw new RetryableError(`MPC error: ${err_msg}`);
}
```

## Frozen entities

A freeze on any of the following blocks signing:

* The specific key being signed with.
* All keys in a venue (if a venue-level freeze was applied).

When frozen, the [sign endpoint](/api-reference#tag/keys) returns `403` with `err_type: "forbidden"`. See [Freeze and unfreeze](/develop/freeze) for how to check and clear freeze state using the [freeze and unfreeze endpoints](/api-reference#tag/keys).

## Retrying

| Condition | Retry? |
|---|---|
| `200 completed` | No |
| `403 forbidden` or `blocked_by_policy` | No: fix the policy or unfreeze first |
| `403 blocked_by_freeze` | After unfreezing |
| `401 unauthorized` | After fixing auth headers |
| `500 internal_server_error` | Yes, with exponential backoff |
| `400` / `422` | No: fix the request |
