# Raw signing

The [sign endpoint](/api-reference#tag/keys) takes a message, evaluates the Signing Policy, and returns a signature. It is synchronous: the vault blocks until the signature is produced or the request is rejected.

```
POST /v1/keys/{key_id}/sign
```

## Request

All four authentication headers are required. See [Machine Users and Agents](/develop/authentication/machine-users) for how to build them.

Request body:

```json
{
  "message": "<hex-encoded bytes>",
  "key_version": "Latest",
  "extra_data": null
}
```

| Field | Required | Description |
|---|---|---|
| `message` | yes | The payload to sign, **hex-encoded** (the raw bytes as a lowercase hex string). A non-hex value returns `400 parse_error: invalid hex`. |
| `key_version` | no | Which key version to sign with. Defaults to `"Latest"`. To target a specific version, use `{"Specific": 3}`. |
| `extra_data` | no | An optional opaque string passed to the Signing Policy for evaluation. Ignored by the vault itself but forwarded to external rule servers if configured. |

## Response

```json
{
  "signature": "<string>"
}
```

The signature encoding (hex, base64url, DER, or other) is determined by the venue's `signing_scheme` and `public_key_encoding` settings. For HMAC keys this is typically lowercase hex. For Ed25519 keys this is typically base64url.

## Example

Sign the UTF-8 bytes of `"hello"` (hex-encoded as `68656c6c6f`) using the key `my-signing-key`:

```bash
curl -s -X POST "$CV_BASE/v1/keys/my-signing-key/sign" \
  -H "Authorization: ApiKey $API_KEY_UUID" \
  -H "X-Request-Timestamp: $TS_HEX" \
  -H "X-Request-Nonce: $NONCE_HEX" \
  -H "X-Request-Assertion: $ASSERTION" \
  -H "Content-Type: application/json" \
  -d '{"message": "68656c6c6f"}'   # hex of "hello"
```

Response:

```json
{ "signature": "3q2-796tvu_erb6t..." }
```

## How policy is applied

Before producing a signature, the vault evaluates the [Signing Policy](/api-reference#tag/signingpolicies) attached to the key's [security group](/api-reference#tag/securitygroups). The policy checks the requesting user's group membership against the configured rules (in order, first match wins).

Possible outcomes:

* **Approved:** the signature is returned in the same response.
* **Denied:** `403 Forbidden` is returned. No signature is produced.

:::note
Unlike many signing systems, `POST /v1/keys/{key_id}/sign` is always synchronous: it returns the signature or an error in the same response and never enters a pending state.
:::

## Selecting a key version

When a key has been rotated, multiple versions exist. The `key_version` field controls which one signs the message.

```json
{ "message": "payload", "key_version": "Latest" }
```

```json
{ "message": "payload", "key_version": { "Specific": 2 } }
```

Use `"Latest"` for new signing requests. Use `"Specific"` only when you need a precise version, for example during a rollover period where some counterparties still verify against the old public key.

## Error responses

See the [Keys API reference](/api-reference#tag/keys) for the full sign endpoint schema. The error shape is shared across all endpoints.

| Status | `err_type` | Meaning |
|---|---|---|
| `400` | `parse_error` | Malformed request body |
| `401` | `unauthorized` | Auth headers missing, invalid, or expired |
| `403` | `forbidden` | Signing Policy denied the request |
| `404` | `resource_not_found` | Key ID not found |
| `422` | `invalid_inputs` | Valid JSON but invalid field values |
| `500` | `internal_server_error` | MPC signing failure or internal error |

All error responses share the same shape:

```json
{ "err_type": "forbidden", "err_msg": "Policy denied: user not in required group" }
```
