---
title: "Replace the calling key with a new one"
method: POST
path: "/v1/keys/rotate"
tags: ["Keys"]
---

# Replace the calling key with a new one

`POST /v1/keys/rotate`

## Request

- Base URL: `https://api.attestwire.com`
- URL: `https://api.attestwire.com/v1/keys/rotate`
- Auth: HTTP bearer

Authenticated by the key being replaced — that bearer token IS the proof of ownership, and there is deliberately no email-triggered variant. No request body. Consumes no documents.

Everything follows the new key atomically: tier and limit, the current period's usage (rotation does NOT reset your quota), any past_due flag, the original `created` date, and the subscription link — including billing events that were already in flight when you rotated.

The plaintext key is returned exactly once, and the old key answers 410 `key_rotated` from the moment this call succeeds. Deploy first, then rotate. Limited to 3 rotations per key per UTC day; the limit follows the key through a rotation, so chaining does not reset it. Two simultaneous calls cannot both succeed — the loser gets 409.

If you no longer HAVE the key, this endpoint is closed to you — there is nothing to authenticate the call with. What is available instead depends on the tier, and on the free tier it is still immediate.

If you are on a paid plan and the key is gone, email hello@attestwire.com from the address on your Paddle billing receipt. We check the request against Paddle's record of your subscription rather than against the receipt itself — a receipt can be forwarded, so attaching one proves only that somebody has a copy of it — and the replacement is delivered one way only: as a reply to the billing address on file, whoever did the asking. Whoever handles it retires the old key and issues the new one.

We cannot recover a lost free key — not as a matter of policy, but because we never collected anything that could prove it was yours: a free signup address is never verified, and the key itself only ever existed in one HTTP response. The practical answer is faster than support anyway. The limit is one free key per email address, so a different address gets you a working free key on the spot, and a plus-alias such as you+attestwire@example.com counts. That limit keeps casual signups to one key each; it is housekeeping rather than a security control, and it was never meant to trap you. If you would rather we were able to rescue you, that is part of what a paid plan buys: Paddle holds a billing identity we can check.

## Response `200`

Rotated. Store `key` now; it is never shown again. The other fields are the continuity proof — same tier, same usage.

- object
  - `key` string, required
  - `tier` 'free' | 'solo' | 'starter' | 'scale', required
  - `monthly_limit` integer, required
  - `period` string, required
  - `used` integer, required: Documents already used this period, carried over from the old key.
  - `remaining` integer
  - `resets_at` string, date-time
  - `created` string, date-time, nullable: Original signup date, unchanged by rotation.
  - `rotated_at` string, date-time, required
  - `rotations_remaining_today` integer
  - `warning` string

## Other responses

- `401`: Missing or unknown API key.
- `409`: `rotation_already_done` — this key has already been rotated, or a concurrent rotate call won the race. The replacement was returned to that call.
- `410`: The key presented was retired by an earlier rotation. Its replacement is the live credential; this one is permanently gone, so retrying with it cannot work. Every authenticated endpoint answers this, not just /v1/keys/rotate.
- `429`: `rotation_rate_limited` — more than 3 rotations of this key today.
- `500`: Two different faults share this status and they mean different things. `error: "rotation_failed"` — a step of the rotation failed and the whole thing was unwound: NOTHING changed, your existing key still works, and it is safe to retry. `error: "internal_error"` — the unhandled fault every route can answer, which here leaves the outcome UNKNOWN. Do not rotate again on that one; call GET /v1/usage with the old key first. A `410 key_rotated` proves the rotation completed, and the replacement key was spent on the response you never got — email hello@attestwire.com rather than rotating again. A `200` is evidence it did not, but not proof: that lookup reads an eventually-consistent store, so a rotation that did land can still answer 200 for a short window. Re-check after a minute and only rotate again once 200 is stable.

---

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