---
title: "Rotate the calling app's secret key"
method: POST
path: "/api/v1/keys/rotate"
tags: ["Credentials"]
---

# Rotate the calling app's secret key

`POST /api/v1/keys/rotate`

## Request

- Base URL: `https://app.dodomain.io`
- URL: `https://app.dodomain.io/api/v1/keys/rotate`
- Auth: HTTP bearer

The calling app's secret key rotates ITSELF, and the new `dd_sk_...` is returned **once** — this is what makes scheduled credential rotation automatable instead of a dashboard click.

**SELF-rotation only, deliberately.** There is no create, list, or revoke-another-key on this API: key inventory stays behind a human dashboard session. A stolen `dd_sk_` can already do everything the app can do until it is rotated, but it must not be able to mint a second, hidden credential that survives the owner rotating the one they know about. Rotating is, in effect, the revoke.

**DEFAULT: no grace window.** With no body (or `overlapHours: 0`) the previous key stops authenticating the instant this returns, so **this response is the only copy of the new key** — a caller that drops it has locked itself out of the API and must rotate again from the dashboard. A zero-overlap rotate also **terminates any overlap window still live** from an earlier rotation; rotating with the default is how you revoke a previous key early.

**OPT-IN OVERLAP.** `{"overlapHours": 1}` or `{"overlapHours": 24}` keeps the old key authenticating alongside the new one until `previousKeyExpiresAt`, so a rotator can deploy the new key with zero downtime. **Exactly one previous key is ever kept:** rotating again overwrites the slot and key n-1 dies instantly, regardless of remaining window. `publicKey` is echoed unchanged (it identifies the app in the widget and is never rotated here) so a CI job can assert it rewrote the right app.

## Headers

- `DoDomain-Team` string

## Request body

- RotateAppSecretKeyInput
  - `overlapHours` union
    - 0
    - 1
    - 24

## Response `200`

Success.

- RotateAppSecretKeyResponse
  - `appId` string, required
  - `previousKeyExpiresAt` string, date-time, nullable, required
  - `publicKey` string, required
  - `rotatedAt` string, date-time, required
  - `secretKey` string, required

## Other responses

- `400`: `400 invalid_request` — The request body or query failed schema validation, or a documented capability rule rejected it. `details` carries the field-level report when the failure came from schema validation.
- `401`: `401 unauthorized` — No `Authorization: Bearer` header, or the credential is malformed, unknown, or (for OAuth tokens) fails verification.
- `403`: `403 forbidden` — The OAuth access token was not granted the scope this operation requires. `details.code` is `SCOPE_MISSING` and `details.requiredScope` names the scope. Secret keys (`dd_sk_`) carry no scopes and never see this for a missing scope. The one-click start routes also answer it for a session created without one-click (a Free-plan team's session): no `details`, and the end user falls back to the manual records.
- `404`: `404 not_found` — The addressed resource does not exist, or the caller does not own it — the two collapse to one status on purpose, so an id cannot be enumerated by a caller who does not own it.
- `429`: `429 rate_limited` — Rate limited. `Retry-After` (seconds) is set and `details.retryAfterSeconds` repeats it. Branch on `details.reason`: `request_rate` means the caller is calling too fast (`details.limit` carries the cap), `recently_checked` means this one connection was DNS-checked inside its cooldown.
- `500`: `500 internal` — An unhandled server-side failure. Reported to our error tracker; safe to retry.

---

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