# Unpublish a form

Source: https://kirora.io/docs/api/reference/unpublishForm

`POST /v1/forms/{id}/unpublish`

Requires an API key with the `forms:write` scope. [About keys and scopes](https://kirora.io/docs/api/reference/authentication)

Works with integration and partner keys, not agent keys.

Safe to retry: a repeat returns the current state, or 409 once it has moved on from it. [About idempotency](https://kirora.io/docs/api/reference/idempotency)

Sets the form back to `draft`: its link and embeds stop taking submissions until it is published again. Its submissions and contacts stay. Unpublishing a draft returns it. Agent keys are refused with 403 `AGENT_KEY_REFUSED`.

## Path parameters

- `id` (string, required, uuid) — The form's id.

## Header parameters

- `Kirora-Workspace` (string, optional, pattern `^[A-Za-z0-9._:-]{1,128}$`) — Partner keys only: the child workspace to act in, by the external id the partner gave it. Without it, a partner key acts in its own workspace. Any other key that sends it is refused with 403, and an unknown workspace is 404.
- `Idempotency-Key` (string, optional, 8–255 characters) — Makes a retry safe: a repeat with the same key and body within 24 hours gets the first response again, with `Idempotency-Replay: true`. The same key with another request is 409 `IDEMPOTENCY_KEY_REUSED`; while the first is still running, 409 `IDEMPOTENCY_KEY_IN_USE`; after an `unsafe` first request failed or never returned, 409 `IDEMPOTENCY_KEY_OUTCOME_UNKNOWN`. Use a new key, such as a UUID, for each new request.

## Returns

Returns `200 OK` with a `Form` in `data`. It can also return `409 Conflict`; see the statuses below. Otherwise returns an error; [Errors](https://kirora.io/docs/api/reference/errors) lists every code and which to retry.

- `id` (string, required, uuid)
- `publicId` (string, required, pattern `^[a-z0-9]{24}$`) — The id the public embed and hosted page use.
- `slug` (string | null, required) — The readable part of the form's link; `null` uses `publicId` alone.
- `name` (string, required)
- `heading` (string | null, required) — What visitors see above the fields; `null` shows none.
- `intro` (string | null, required) — A line or two under the heading; `null` shows none.
- `location` (string, required) — Where the form sits on your site, a label you choose (`website-footer`, `checkout-page`). Recorded on each submission, as `properties.location` on its `contact.form_submitted` event and on the `form.submitted` webhook, which is how a segment or journey uses it.
- `url` (string, required, uri) — The hosted form's link, by its `slug` when it has one, else its `publicId`. It serves once the form is published.
- `status` (string, required, one of `draft`, `published`) — Whether the form accepts submissions: `published` once `POST /v1/forms/{id}/publish` puts it live, `draft` until then or after unpublishing.
- `listId` (string | null, required, uuid) — The list every submitter is added to.
- `fields` (object[], required) — The fields, top to bottom.
  - `key` (string, required) — Where the response goes, such as `email` or `attributes.plan`.
  - `type` (string, required, one of `email`, `text`, `textarea`, `select`, `checkbox`, `date`, `phone`) — The input the visitor sees.
  - `label` (string, required) — The text shown beside the field.
  - `placeholder` (string, optional) — Hint text shown inside the empty field.
  - `required` (boolean, required) — Whether the visitor must fill it in.
  - `options` (object[], optional) — The choices of a `select` or `checkbox` field.
    - `value` (string, required)
    - `label` (string, required)
- `hiddenFields` (map<string, object>, required, at most 50 keys, keys: pattern `^[\p{L}\p{N}\p{M}_.-]{1,64}$`) — Values saved on each submitter with no field shown: fixed, or taken from the link.
  - One of: **With value**
    - `source` ("fixed", required)
    - `value` (string, required, up to 1,000 characters) — Saved as it is on every submitter.
  - One of: **With param**
    - `source` ("link", required)
    - `param` (string, required, pattern `^[A-Za-z0-9_.-]{1,64}$`) — The link parameter it takes its value from, such as `utm_campaign`; left unset when the link has none.
- `share` (object | null, required) — How the form's link previews, as set; `null` falls back entirely.
  - `title` (string, optional)
  - `description` (string, optional)
  - `imageUrl` (string, optional, uri)
- `acceptingSubmissions` (boolean, required) — `false`: closed by hand, whatever its closing time and limit.
- `closesAt` (string | null, required, date-time) — When the form closes, if ever.
- `maxSubmissions` (integer | null, required) — The submissions it closes at, if any.
- `closedMessage` (string | null, required) — What a closed form shows; `null` shows the default wording.
- `submissionCount` (integer, required, min 0) — Submissions taken so far (honeypot catches aside), counted for the limit.
- `allowedOrigins` (string[], required) — Site origins the embed may load from.
- `refusedOrigins` (object[], required) — Origins an embed was refused from lately, newest first, not counting any since added to `allowedOrigins`; add one there to approve it.
  - `origin` (string, required) — The origin that was refused.
  - `lastRefusedAt` (string, required, date-time) — When it was last refused.
- `doubleOptIn` (boolean, required) — Whether submitters must confirm by email before they are subscribed.
- `notifyUserIds` (string[], required) — Users of this workspace told about each submission.
- `theme` (FormTheme, required)
  - `mode` (string, optional, one of `light`, `dark`) — Light or dark defaults for every colour left unset; light when unset.
  - `accentColor` (string, optional, pattern `^#[0-9a-fA-F]{6}$`) — Colour of the button and focus outlines, as `#RRGGBB`.
  - `backgroundColor` (string | null, optional, pattern `^#[0-9a-fA-F]{6}$`) — The page and form background, as `#RRGGBB`; unset or `null` for the mode's default.
  - `textColor` (string | null, optional, pattern `^#[0-9a-fA-F]{6}$`) — Headings, labels and text, as `#RRGGBB`; unset or `null` for the mode's default.
  - `buttonColor` (string | null, optional, pattern `^#[0-9a-fA-F]{6}$`) — The button's fill; unset, the accent colour, as `#RRGGBB`; unset or `null` for the mode's default.
  - `buttonTextColor` (string | null, optional, pattern `^#[0-9a-fA-F]{6}$`) — The button's text, as `#RRGGBB`; unset or `null` for the mode's default.
  - `fontFamily` (string, optional, one of `system`, `sans`, `serif`, `mono`, `inter`, `roboto`, `open-sans`, `lato`, `montserrat`, `poppins`, `source-sans-3`, `nunito`, `raleway`, `work-sans`, `dm-sans`, `manrope`, `ibm-plex-sans`, `noto-sans`, `rubik`, `karla`, `merriweather`, `lora`, `playfair-display`, `libre-baskerville`, `source-serif-4`, `eb-garamond`, `ibm-plex-mono`, `jetbrains-mono`) — The font's id: a generic style (`system`, `sans`, `serif`, `mono`) or a named font the forms host serves itself. A font emails also offer has the same id there (`EmailFont`).
  - `borderRadius` (integer, optional, min 0, max 24) — Corner rounding of inputs and the button, in px, where `inputs.borderRadius` or `button.borderRadius` does not set its own.
  - `buttonLabel` (string, optional, 1–40 characters) — Text on the submit button.
  - `inputs` (object, optional) — How fields look; each part optional.
    - `width` (string, optional, one of `fit`, `full`, `fixed`) — `fit` (unset): a short answer's field is 320px and a long one fills the form; `full`: every field fills the form; `fixed`: every field `widthPx` wide at most.
    - `widthPx` (integer, optional, min 200, max 720) — With `width` `fixed`: the width, in px.
    - `height` (integer, optional, min 28, max 64) — In px.
    - `backgroundColor` (string | null, optional, pattern `^#[0-9a-fA-F]{6}$`) — The field background, as `#RRGGBB`; unset or `null` for the mode's default.
    - `placeholderColor` (string | null, optional, pattern `^#[0-9a-fA-F]{6}$`) — Placeholder text, as `#RRGGBB`; unset or `null` for the mode's default.
    - `borderColor` (string | null, optional, pattern `^#[0-9a-fA-F]{6}$`) — The field border, as `#RRGGBB`; unset or `null` for the mode's default.
    - `borderWidth` (integer, optional, min 0, max 4) — In px.
    - `borderRadius` (integer, optional, min 0, max 32) — In px; unset, the top-level `borderRadius`.
    - `spacing` (integer, optional, min 0, max 48) — The gap below each field, in px.
  - `button` (object, optional) — How the submit button looks; each part optional.
    - `width` (string, optional, one of `auto`, `full`) — Fit its label, or the form width.
    - `height` (integer, optional, min 28, max 64) — In px.
    - `align` (string, optional, one of `start`, `center`, `end`)
    - `fontSize` (integer, optional, min 12, max 24) — In px.
    - `borderRadius` (integer, optional, min 0, max 32) — In px; unset, the top-level `borderRadius`.
  - `layout` (object, optional)
    - `pageWidth` (integer, optional, min 360, max 960) — The form's widest, in px.
    - `baseFontSize` (integer, optional, min 12, max 20) — Body text size, in px.
- `display` (object, required)
  - One of: **Inline**
    - `mode` ("inline", required)
  - One of: **Popup**
    - `mode` ("popup", required)
    - `delay` (DurationOrZero, optional) — How long after the page loads it opens, at most 10 minutes. Defaults to 5 seconds.
      - `value` (integer, required, min 0)
      - `unit` (string, required, one of `seconds`, `minutes`, `hours`, `days`)
    - `scrollPercent` (integer, optional, min 1, max 100) — Opens once the visitor has scrolled this far down the page.
    - `exitIntent` (boolean, optional) — Opens when the visitor moves to leave the page (desktop only).
    - `urlMatch` (object, optional) — Limits which pages the popup opens on.
      - `contains` (string[], optional, at most 20 items) — Show only on pages whose URL contains one of these.
      - `excludes` (string[], optional, at most 20 items) — Never show on pages whose URL contains one of these.
    - `hideFor` (DurationOrZero, optional) — How long it stays away once closed, at most 365 days. Defaults to 7 days.
      - `value` (integer, required, min 0)
      - `unit` (string, required, one of `seconds`, `minutes`, `hours`, `days`)
    - `hideAfterSubmit` (boolean, optional) — Whether it stays away after a signup. Defaults to true.
- `captcha` (object | null, required) — Where the CAPTCHA comes from; `null` while it is off. Secrets never show.
  - One of: **KiroraCaptchaSetting**
    - `mode` ("kirora", required)
  - One of: **OwnCaptchaSetting**
    - `mode` ("own", required)
    - `siteKey` (string, required)
- `consent` (object, required)
  - `required` (true, required) — Always `true`: the visitor must tick the consent box.
  - `label` (string, required) — The consent text shown beside the box.
- `success` (object, required)
  - One of: **Show a message**
    - `type` ("message", required)
    - `message` (string, required) — Shown in place of the form.
  - One of: **Redirect**
    - `type` ("redirect", required)
    - `redirectUrl` (string, required, uri) — An http(s) address the visitor is sent to.
- `archivedAt` (string | null, required, date-time) — When it was archived; an archived form is left out of the list and stops serving.
- `createdAt` (string, required, date-time)
- `updatedAt` (string, required, date-time)

Statuses:

- `200 OK`: The form, a draft again.
- `409 Conflict`: The request conflicts with current state — a duplicate unique value, or a resource whose status does not allow this operation.

Headers on `200 OK`:

- `Idempotency-Replay` (string, optional, one of `true`) — `true` when this is the stored response to an earlier request with the same key.

## Example

```bash
curl -X POST https://api.kirora.io/v1/forms/018f2a4e-6b1c-7d3e-9f40-37386ae05a6b/unpublish \
  -H "Authorization: Bearer $KIRORA_API_KEY"
```

Response `200`:

```json
{
  "data": {
    "id": "0192f3e1-2b3c-7d4e-9f50-6b7c8d9e0f1a",
    "publicId": "frm7k2m9q4x1c8v3b6n5t0za",
    "slug": "acme-newsletter",
    "name": "Newsletter signup",
    "heading": "Join the Acme Shop newsletter",
    "intro": "New arrivals and offers, once a week.",
    "location": "website-footer",
    "url": "https://forms.kirora.io/f/acme-newsletter",
    "status": "draft",
    "listId": "0192f3a2-1b3c-7e5d-8a21-4f6a7b8c9d0e",
    "fields": [
      {
        "key": "email",
        "type": "email",
        "label": "Email address",
        "placeholder": "you@example.com",
        "required": true
      },
      {
        "key": "firstName",
        "type": "text",
        "label": "First name",
        "required": false
      }
    ],
    "hiddenFields": {
      "signup_campaign": {
        "source": "link",
        "param": "utm_campaign"
      }
    },
    "share": {
      "title": "Get the Acme Shop newsletter"
    },
    "acceptingSubmissions": true,
    "closesAt": null,
    "maxSubmissions": null,
    "closedMessage": null,
    "submissionCount": 1240,
    "allowedOrigins": [
      "https://acme-shop.com"
    ],
    "refusedOrigins": [],
    "doubleOptIn": true,
    "notifyUserIds": [],
    "theme": {
      "accentColor": "#b4461f",
      "fontFamily": "sans",
      "buttonLabel": "Subscribe"
    },
    "display": {
      "mode": "inline"
    },
    "captcha": null,
    "consent": {
      "required": true,
      "label": "I agree to receive emails from Acme Shop and can unsubscribe at any time."
    },
    "success": {
      "type": "message",
      "message": "Thanks. Check your inbox to confirm."
    },
    "archivedAt": null,
    "createdAt": "2026-10-01T09:30:00.000Z",
    "updatedAt": "2026-10-02T14:05:00.000Z"
  }
}
```
