# Manage widgets with the API

> List, add, change, rotate and delete Wicket widgets, and read their numbers, with the Client API.

Source: https://www.coritan.com/docs/wicket/manage-widgets-with-the-api/

Use these routes to manage your Wicket widgets from a script: add a widget when you deploy a site, keep its hostnames current, rotate its secret key, and read how it does. Every route is under `https://api.coritan.com/api/v1/client/wicket`.

## Before you begin

- You need an access token or an account API key ([Authentication](/docs/api/authentication/)). The examples send it as `$CORITAN_TOKEN`.
- Reading needs any role on the account. Adding, changing and deleting widgets and rotating keys need the account owner, or a team member with the Admin or Technical role ([Team members](/docs/account/team-members/)). An API key with only Read permissions can read and cannot change anything ([Authentication](/docs/api/authentication/#account-api-keys)).
- A widget that belongs to another account answers `404`, the same as one that does not exist.
- The account may make 60 changes in 10 minutes, counting every add, change, delete and rotation. The next one answers `429` with a `Retry-After` header.

## List your widgets

```bash
curl "https://api.coritan.com/api/v1/client/wicket/widgets?status=attention&sort=-challenges" \
  -H "Authorization: Bearer $CORITAN_TOKEN"
```

All four query words are optional.

`q`
: Only widgets whose name, site key or one of whose hostnames contains this text, ignoring case.

`status`
: `all` (the default), `active`, `disabled` (our team turned it off) or `attention` (the widget needs attention).

`sort`
: `name`, `created`, `challenges` or `solve_rate`, each with a leading `-` for descending. The default is `-created`, newest first. `challenges` and `solve_rate` use the last 24 hours, and a widget with no challenges has no solve rate and comes last.

`page`, `per_page`
: The page, starting at 1, and how many widgets it holds. The default is 24 and the most is 100.

A `status` or `sort` word that is not in the lists answers `422`, and the message names the words that are.

```json
{
  "items": [
    {
      "id": 12,
      "name": "Sign-up form",
      "site_key": "wk_6fJ2kQ9xT1mZ8bP4rL0aVc",
      "mode": "managed",
      "status": "active",
      "branded": true,
      "hostnames": ["example.com", "app.example.com"],
      "hostname_count": 2,
      "created_at": "2026-10-17T15:14:30Z",
      "last_challenge_at": "2026-10-17T16:02:00Z",
      "stats_24h": {"issued": 1204, "passed": 1180, "failed": 9, "solve_rate": 0.98},
      "attention": ["not_verifying"],
      "disabled_reason": null
    }
  ],
  "total": 1,
  "page": 1,
  "per_page": 24,
  "limit": {"used": 1, "limit": 25},
  "stats": {
    "issued_24h": 1204,
    "passed_24h": 1180,
    "solve_rate_24h": 0.98,
    "attention": [{"widget_id": 12, "name": "Sign-up form", "reason": "not_verifying", "disabled_reason": null}]
  }
}
```

| Field | Meaning |
| --- | --- |
| `hostnames`, `hostname_count` | The first three hostnames, and how many the widget lists. |
| `stats_24h` | Challenges started, passed and failed in the last 24 hours. `solve_rate` is `passed` divided by `issued`, or `null` when nothing was issued. |
| `attention` | Why the widget needs attention, if it does: `disabled`, `not_verifying` or `hostname_refused` ([Analytics and what needs attention](/docs/wicket/#analytics-and-what-needs-attention)). |
| `disabled_reason` | Why our team turned the widget off, for a widget whose `status` is `disabled`, and `null` for any other. |
| `total` | How many widgets match `q` and `status`, over every page. |
| `limit` | How many widgets the account has and how many its plan allows. `-1` is unlimited. |
| `stats` | Totals for the whole account, not only this page. `attention` has one entry for each reason of each widget that needs attention. Each entry has `widget_id`, `name`, `reason` and `disabled_reason`, which is the reason our team gave when `reason` is `disabled` and `null` for any other reason. |

## Add a widget

```bash
curl -X POST https://api.coritan.com/api/v1/client/wicket/widgets \
  -H "Authorization: Bearer $CORITAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Sign-up form", "hostnames": ["example.com", "app.example.com"], "mode": "managed"}'
```

`name` is 1 to 100 characters, and `hostnames` lists at least one. `mode` is `managed` (the default), `non_interactive` or `invisible`. You can also send `appearance`, with any of `theme` (`auto`, `light` or `dark`), `size` (`normal`, `flexible` or `compact`) and `language` (`auto`, or a code such as `es`), and `branded: false` to leave Wicket's name off the widget.

The answer is `201`. It holds the widget and its secret key:

```json
{
  "widget": {
    "id": 12,
    "name": "Sign-up form",
    "site_key": "wk_6fJ2kQ9xT1mZ8bP4rL0aVc",
    "mode": "managed",
    "status": "active",
    "branded": true,
    "hostnames": ["example.com", "app.example.com"],
    "hostname_count": 2,
    "created_at": "2026-10-17T15:14:30Z",
    "last_challenge_at": null,
    "stats_24h": {"issued": 0, "passed": 0, "failed": 0, "solve_rate": null},
    "attention": [],
    "appearance": {"theme": "auto", "size": "normal", "language": "auto"},
    "secret_prefix": "wks_Qm4x7K",
    "secret_rotated_at": null,
    "previous_secret_expires_at": null,
    "disabled_reason": null,
    "limits": {"hostnames": 10},
    "embed": {
      "script_url": "https://wicket.coritan.com/v1/api.js",
      "siteverify_url": "https://api.coritan.com/api/v1/wicket/siteverify"
    }
  },
  "secret": "wks_Qm4x7KpR2dVbT9sLhW1cYe6NzA0uJfG3oXiBk5aMvH"
}
```

> [!IMPORTANT]
> The `secret` is shown once. Store it in your server's settings now. No route returns it again, and `secret_prefix` is only its first ten characters.

`embed` holds the two addresses you need: `script_url` is the script your page loads, and `siteverify_url` is where your server sends the token and the secret key.

Each hostname is checked and written in lower case, so `Example.COM.` is stored as `example.com`, and a repeated hostname is stored once. A hostname Wicket refuses answers `422` with the hostname and the reason:

```json
{"detail": "\"exa mple.com\" is not a hostname: a hostname has no spaces"}
```

## Read one widget

```bash
curl https://api.coritan.com/api/v1/client/wicket/widgets/12 \
  -H "Authorization: Bearer $CORITAN_TOKEN"
```

The answer has the fields of the widget above, with every hostname in `hostnames`. `limits.hostnames` is how many hostnames your plan lets one widget list. While a rotation's grace period is running, `previous_secret_expires_at` says when the old secret key stops working. For a widget our team turned off, `status` is `disabled` and `disabled_reason` says why. The list and the summary give the same `disabled_reason`, so you can show it without reading each widget.

## Change a widget

Send only what changes. `hostnames` is the complete new list, so it replaces the current one, and a setting you leave out of `appearance` keeps its value.

```bash
curl -X PATCH https://api.coritan.com/api/v1/client/wicket/widgets/12 \
  -H "Authorization: Bearer $CORITAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"hostnames": ["example.com", "app.example.com", "shop.example.com"], "appearance": {"theme": "dark"}}'
```

The answer is the widget as in [Read one widget](#read-one-widget). A request that sends nothing to change answers `422`. The plan's hostname limit counts the hostnames the widget keeps and the ones the request adds, so swapping one hostname for another at the limit works, and adding one more does not.

## Rotate the secret key

Rotate a secret key when it may have leaked, or when you lose it. `grace` says what happens to the old key:

`2h`
: The old secret key keeps working for 2 hours, so you can change your server without a gap. This is the default.

`none`
: The old secret key stops working at once.

```bash
curl -X POST https://api.coritan.com/api/v1/client/wicket/widgets/12/secret \
  -H "Authorization: Bearer $CORITAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"grace": "2h"}'
```

The answer has the same shape as adding a widget: the widget, with `secret_rotated_at` and, for `2h`, `previous_secret_expires_at`, and the new `secret`, shown once. A widget remembers one old secret key, so rotating again during a grace period ends the earlier key at once. We email the account owner each time a secret key is rotated.

## Delete a widget

```bash
curl -X DELETE https://api.coritan.com/api/v1/client/wicket/widgets/12 \
  -H "Authorization: Bearer $CORITAN_TOKEN"
```

```json
{"status": "deleted"}
```

> [!WARNING]
> The widget's site key and secret key stop working at once, so a form that uses the widget stops passing its check. A deleted widget cannot be brought back.

## Read a widget's numbers {#read-a-widgets-numbers}

```bash
curl "https://api.coritan.com/api/v1/client/wicket/widgets/12/analytics?range=7d" \
  -H "Authorization: Bearer $CORITAN_TOKEN"
```

`range` is `24h` (the default), `7d`, `30d`, `90d` or `365d`. Any other word answers `422`, and the message names the five allowed ones, such as `range must be one of: 24h, 7d, 30d, 90d, 365d, not '1y'`.

```json
{
  "range": "7d",
  "from": "2026-10-11T16:00:00Z",
  "to": "2026-10-17T16:21:09Z",
  "granularity": "hour",
  "allowed_days": 7,
  "clamped": false,
  "upgrade": null,
  "series": [
    {"t": "2026-10-17T15:00:00Z", "issued": 52, "passed": 50, "passed_interactive": 3, "failed": 1, "interactive_shown": 4}
  ],
  "totals": {
    "issued": 1204,
    "passed": 1180,
    "passed_interactive": 40,
    "failed": 9,
    "interactive_shown": 51,
    "unsolved": 15,
    "solve_rate": 0.98,
    "verify": {"valid": 1150, "invalid": 4, "expired": 12, "duplicate": 2}
  },
  "top": {
    "hostname": [{"value": "example.com", "issued": 900, "passed": 890, "failed": 3}],
    "action": [],
    "country": [],
    "browser": [],
    "os": []
  }
}
```

| Field | Meaning |
| --- | --- |
| `series` | One point for each hour (`granularity` is `hour`, for `24h` and `7d`) or each day (`day`, for `30d`, `90d` and `365d`), with a point of zeros for a period with no challenges. `t` is the start of the period. |
| `totals` | The sums over the series. `unsolved` is `issued` minus `passed` minus `failed`: challenges nobody finished. `passed_interactive` counts passes that needed a tick. |
| `totals.verify` | What your server's verify calls found: tokens that were `valid`, `invalid`, `expired` or used twice (`duplicate`). A `verify` of zeros while `passed` is high means your server is not verifying tokens. |
| `top` | The ten values with the most challenges for each of `hostname`, `action`, `country`, `browser` and `os`, over the same span. |

When the range is longer than your plan reads back, the answer is still `200`. It covers the days your plan allows, `clamped` is `true`, `allowed_days` is that number of days, and `upgrade.plan` names the plan that allows the range you asked for:

```json
{"range": "90d", "allowed_days": 7, "clamped": true, "upgrade": {"plan": "plan-business"}}
```

The plan is the smallest one above yours that allows the range. `365d` on any plan but Enterprise names `plan-enterprise`, and Enterprise reads every range in full.

## Read the summary

```bash
curl https://api.coritan.com/api/v1/client/wicket/summary \
  -H "Authorization: Bearer $CORITAN_TOKEN"
```

```json
{
  "widgets": 3,
  "limit": 25,
  "issued_24h": 1580,
  "passed_24h": 1522,
  "solve_rate_24h": 0.9633,
  "attention": [{"widget_id": 12, "name": "Sign-up form", "reason": "not_verifying", "disabled_reason": null}]
}
```

`widgets` is how many widgets the account has and `limit` how many its plan allows (`-1` is unlimited). The counts cover the last 24 hours and every widget. `attention` has an entry for each widget that needs attention and each reason, in the same shape as in the list: `widget_id`, `name`, `reason` and `disabled_reason`, which is set only when `reason` is `disabled`.

## Result

The widget exists, lists the hostnames you gave it, and answers challenges from pages on those hostnames. Your server verifies the tokens at `embed.siteverify_url` with the secret key.

## Troubleshooting

`402` with `"code": "entitlement_exceeded"`
: The request would take the account past a limit of its plan. `detail.entitlement` is `wicket_widgets` (widgets on the account), `wicket_hostnames` (hostnames on one widget) or `wicket_unbranded` (a widget without Wicket's name). `detail.upgrade.plan` is the plan that raises it. No add-on does, so `upgrade.addon` is `null`.

`403` with `"code": "role_refused"`
: You acted for another account as a team member whose role cannot make changes. Only the Admin and Technical roles can.

`404`
: The widget does not exist or belongs to another account. Check the `id`, and the `X-Coritan-Account` header if you act for another account.

`422`
: A value was refused. `detail` is a sentence that names the value and the reason, such as a hostname with a port or an `appearance` setting that does not exist.

`429`
: The account made more than 60 changes in 10 minutes. Wait for the number of seconds in the `Retry-After` header.

## Related

- [Wicket](/docs/wicket/)
- [Errors](/docs/api/errors/)
- [Manage API keys](/docs/account/api-keys/)
- [Wicket limits](/docs/wicket/limits/)

## Next steps

- [Verify a token on your server](/docs/wicket/verify-tokens/) with the secret key the call returned.
- [Add Wicket to your site](/docs/wicket/add-wicket-to-your-site/) to put the widget's `embed.script_url` on a page.
- [Read a widget's analytics](/docs/wicket/widget-analytics/) in the dashboard.

## API

- `GET /api/v1/client/wicket/widgets`: List your widgets, with how each did in the last 24 hours and why any needs a look (https://www.coritan.com/docs/api/reference/client/wicket/#op-get-api-v1-client-wicket-widgets)
- `POST /api/v1/client/wicket/widgets`: Add a widget (https://www.coritan.com/docs/api/reference/client/wicket/#op-post-api-v1-client-wicket-widgets)
- `GET /api/v1/client/wicket/widgets/{widget_id}`: Get widget (https://www.coritan.com/docs/api/reference/client/wicket/#op-get-api-v1-client-wicket-widgets-widget-id)
- `PATCH /api/v1/client/wicket/widgets/{widget_id}`: Change a widget's name, hostnames, mode, appearance or branding (https://www.coritan.com/docs/api/reference/client/wicket/#op-patch-api-v1-client-wicket-widgets-widget-id)
- `DELETE /api/v1/client/wicket/widgets/{widget_id}`: Delete a widget (https://www.coritan.com/docs/api/reference/client/wicket/#op-delete-api-v1-client-wicket-widgets-widget-id)
- `POST /api/v1/client/wicket/widgets/{widget_id}/secret`: Make a new secret key for a widget (https://www.coritan.com/docs/api/reference/client/wicket/#op-post-api-v1-client-wicket-widgets-widget-id-secret)
- `GET /api/v1/client/wicket/widgets/{widget_id}/analytics`: Widget analytics (https://www.coritan.com/docs/api/reference/client/wicket/#op-get-api-v1-client-wicket-widgets-widget-id-analytics)
- `GET /api/v1/client/wicket/summary`: Widgets summary (https://www.coritan.com/docs/api/reference/client/wicket/#op-get-api-v1-client-wicket-summary)
