> ## Documentation Index
> Fetch the complete documentation index at: https://docs.adrop.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors and Limits

> Learn how the Context Targeting API reports errors, how to retry requests safely, and which limits apply.

This page covers the error responses of the [Context Targeting API](/integrations/context-targeting/overview), how bulk requests report results, how to retry safely, and the limits that apply to requests.

***

## Error Responses

A failed request returns an HTTP error status and a JSON body in one of the forms below. Handle errors by the HTTP status and `errorCode`, not by the `message` text.

| Status | Form | `errorCode` |
| - | - | :-: |
| `400` | [Validation error](#validation-errors) | No |
| `400`, `404`, `409` | [Business error](#business-errors) | Yes |
| `401`, `403` | [Authentication or permission error](#authentication-and-permission-errors) | No |
| `413` | [Request body too large](#request-body-too-large) | No |
| `500`, `502` | [Server error](#server-errors) | No |

### Validation Errors

The request was rejected before processing. A field is missing or invalid, a name or Context ID breaks the rules, the request has an unknown field (such as `_id` in a create request), or a per-request limit is exceeded. Nothing in the request is applied, including bulk requests.

`message` is an array that lists every problem found.

```json theme={null}
{
  "message": [
    "property _id should not exist",
    "name must be 2-36 characters of Korean, English letters, digits, spaces or ~%&+- (no leading or trailing spaces)"
  ],
  "error": "Bad Request",
  "statusCode": 400
}
```

When an ID in the path isn't a valid ULID, or the body isn't valid JSON, `message` is a single string.

```json theme={null}
{
  "message": "Invalid ULID",
  "error": "Bad Request",
  "statusCode": 400
}
```

### Business Errors

The request is well-formed, but the server can't process it. For example, the name is already in use, or the targeting doesn't exist.

| Field | Type | Description |
| - | - | - |
| `statusCode` | number | HTTP status code |
| `errorCode` | string | Error code. See [Errors by Code](#errors-by-code). |
| `detail` | object | Extra information for some error codes. See [Detail Fields](#detail-fields). |
| `timestamp` | string | Time the error occurred. Included in some responses only. |

This is the response when you create a targeting with a name that another context targeting already uses:

```json theme={null}
{
  "statusCode": 409,
  "errorCode": "TARGETING_NAME_DUPLICATE",
  "detail": {
    "existingId": "01JAB3XK5Q8V7M2N4P6R9T0W1Y"
  }
}
```

A single-item endpoint returns the error of its item as the response. If the item fails because of an unexpected error, the endpoint returns `502` instead. Bulk endpoints report errors per item in `results`. See [Bulk Responses](#bulk-responses).

### Authentication and Permission Errors

`401` means the `x-api-key` header is missing, or the key is invalid or revoked. `403` means the key doesn't have the scope that the endpoint requires.

```json theme={null}
{
  "message": "Unauthorized",
  "statusCode": 401
}
```

```json theme={null}
{
  "message": "Forbidden",
  "statusCode": 403
}
```

### Request Body Too Large

`413` means the request body is larger than 8 MB. Nothing is applied.

```json theme={null}
{
  "statusCode": 413,
  "message": "request entity too large"
}
```

### Server Errors

`502` means the request couldn't be completed because of a temporary server problem, and `500` means an unexpected server error. In both cases, the request may or may not have been applied. The `message` text can vary. See [Retries and Duplicate Requests](#retries-and-duplicate-requests).

```json theme={null}
{
  "message": "Upstream service error",
  "error": "Bad Gateway",
  "statusCode": 502
}
```

***

## Error Codes

### Errors by Status

These errors have no `errorCode`.

| Status | When it occurs | What to do |
| - | - | - |
| `400` | The request failed validation. Nothing is applied. | Fix the request based on `message`. See [Limits](#limits) and [Name Rules](#name-rules). |
| `401` | The `x-api-key` header is missing, or the key is invalid or revoked. | Check the API key. |
| `403` | The key doesn't have the required scope: `context-targeting:read` for `GET` requests, and `context-targeting:write` for `POST`, `PATCH`, and `DELETE` requests. | Use a key that has the scope. You can't add a scope to an existing key, so create a new key in **\[Settings]** > **\[Integrations]**. See [Integrations & API Keys](/integrations/overview). |
| `413` | The request body is larger than 8 MB. | Split the request. Send non-ASCII text, such as Korean, as raw UTF-8 instead of `\uXXXX` escapes, for example with `ensure_ascii=False` in Python's `json.dumps` or `JSON_UNESCAPED_UNICODE` in PHP's `json_encode`. |
| `500`, `502` | A server problem. The request may or may not have been applied. | Wait a moment, then send the same request again. See [Retries and Duplicate Requests](#retries-and-duplicate-requests). |

### Errors by Code

| Status | `errorCode` | When it occurs | What to do |
| - | - | - | - |
| `400` | `AD_PRODUCT_TARGETING_POLICY_LIMIT_EXCEEDED` | The change would give an ad product more than 500 options set as **Required Include** or **Required Exclude**, across all targetings. This can happen when a new top-level option takes over a product's required setting, or when a sub-option takes over the option group's settings because you delete the option group or detach the sub-option. In an option bulk add, every new top-level option of that targeting fails. | In the console, reduce the required options of the products in `detail.products`, then send the request again. |
| `400` | `TARGETING_CATEGORY_LIMIT_EXCEEDED` | The targeting would have more than 100,000 options, including sub-options. | Delete options you no longer use, or use another targeting. |
| `400` | `TARGETING_CATEGORY_LOCKED` | You tried to move a top-level option into an option group while an ad product sets it as **Required Include** or **Required Exclude**, or while a campaign that hasn't ended includes or excludes it. | Remove those settings first. To put new options in an option group, add them with `parent` or `children` from the start. See [Move or Detach an Option](/integrations/context-targeting/option-groups#move-or-detach-an-option). |
| `400` | `TARGETING_CATEGORY_NOT_SUPPORTED` | The request doesn't match the option's type or the option group rules. See the cases below this table. | Fix the request. See [Constraints](/integrations/context-targeting/option-groups#constraints). |
| `400` | `TARGETING_CATEGORY_TREE_ITEMS_LIMIT_EXCEEDED` | An option bulk delete includes more than 150 option groups and sub-options. The whole request is rejected, and nothing is deleted. | Split the IDs into smaller requests, and send them one after another. |
| `400` | `TARGETING_COLLECTION_CONTEXT_DUPLICATED` | A Context ID that you're adding to an option group's own Context IDs is already an own Context ID of another option group in the project. This includes option groups that the same request creates or changes. | Remove the conflicting Context IDs. `detail.conflicts` lists up to 20 of them. Only the own Context IDs of option groups must be unique. Regular options and sub-options can share Context IDs. |
| `400` | `TARGETING_CONTEXT_LIMIT_EXCEEDED` | The change would exceed a Context ID limit: 50,000 Context IDs or 8 MB per option, or 200,000 Context IDs per project. | Remove Context IDs you no longer use. See [Limits](#limits). |
| `400` | `TARGETING_LOCKED` | You tried to delete a targeting while an approved campaign that hasn't ended includes or excludes one of its options. Nothing is deleted. | Delete the targeting after those campaigns end. |
| `404` | `PROJECT_NOT_FOUND` | The project that the API key belongs to no longer exists. | Use a key issued in an existing project. |
| `404` | `TARGETING_CATEGORY_NOT_FOUND` | The option doesn't exist or isn't in a context targeting of your project, or `parent` isn't an option of the same targeting. Resending a delete that already succeeded also returns this code. | Check the ID. For a resent delete, treat the option as deleted. |
| `404` | `TARGETING_NOT_FOUND` | The targeting doesn't exist, belongs to another project, or isn't a context targeting. Resending a delete that already succeeded also returns this code. | Check the ID. For a resent delete, treat the targeting as deleted. |
| `409` | `CONTEXT_TARGETING_WRITE_IN_PROGRESS` | Another API write for the same project is in progress, and nothing in the request was applied. In `results`, this error means that the item wasn't applied. | Wait `detail.retryAfterMs` milliseconds, then send it again. See [Concurrent Writes](#concurrent-writes). |
| `409` | `TARGETING_CATEGORY_NAME_DUPLICATE` | The targeting already has an option with the name, including option groups and sub-options, or the name repeats within the request. | Use another name. If you resent a create, use the ID in `detail.existingIds`. |
| `409` | `TARGETING_NAME_DUPLICATE` | The project already has a targeting with the name, of any kind including audience targetings, or the name repeats within the request. | Use another name. If you resent a create, use the ID in `detail.existingId`. |
| `500` | `UNCATCHED` | Bulk `results` only. An unexpected error occurred. In a create or delete bulk request, nothing in the request was applied. | Send the same request again. For an update bulk request, send the failed items again. |

`TARGETING_CATEGORY_NOT_SUPPORTED` occurs in these cases:

* An option group has neither sub-options nor Context IDs of its own.
* `children` is sent with `type: "atomic"`, or `exposeChildren` is sent for a regular option.
* When you add an option, `parent` is sent together with `type: "collection"`, `children`, or `exposeChildren`.
* `parent` points to an option that isn't a top-level option group.
* An update sends `parent` for an option group. Option groups can't be moved.
* A move, detach, or delete would remove the last sub-option of an option group that has no Context IDs of its own.
* An update would remove every Context ID of an option group that has no sub-options.
* The `parent` query parameter of [List Options](/integrations/context-targeting/options#list-options) points to a regular option.
* The option is stored in a form that the API can't change, such as an option group inside another option group.

### Detail Fields

| Field | Returned with | Description |
| - | - | - |
| `existingId` | `TARGETING_NAME_DUPLICATE` | ID of the existing targeting with the name. Included only when it's a context targeting. |
| `existingIds` | `TARGETING_CATEGORY_NAME_DUPLICATE` | IDs of the existing options with the name in the targeting. Usually one ID, but more if the targeting already has several options with that name. |
| `duplicateOfIndex` | `TARGETING_NAME_DUPLICATE`, `TARGETING_CATEGORY_NAME_DUPLICATE` | The name repeats within a create or option update request. This is the `index` of the item that used the name first, or the item's own `index` when the name repeats within one item. In a targeting rename request, a repeated name returns `existingId` instead, because the earlier rename is already applied. |
| `retryAfterMs` | `CONTEXT_TARGETING_WRITE_IN_PROGRESS` | How long to wait before sending the request again, in milliseconds. Between 0 and 2,000. |
| `targeting` | `TARGETING_CATEGORY_LIMIT_EXCEEDED` | ID of the targeting that would exceed the limit |
| `limit` | `TARGETING_CATEGORY_LIMIT_EXCEEDED`, `AD_PRODUCT_TARGETING_POLICY_LIMIT_EXCEEDED`, `TARGETING_CATEGORY_TREE_ITEMS_LIMIT_EXCEEDED` | The limit that would be exceeded |
| `count` | `AD_PRODUCT_TARGETING_POLICY_LIMIT_EXCEEDED`, `TARGETING_CATEGORY_TREE_ITEMS_LIMIT_EXCEEDED` | For the ad product limit, the number of required options that the change would leave on a product. If several products exceed the limit, the largest number. For the bulk delete limit, the number of option groups and sub-options in the request. |
| `products` | `AD_PRODUCT_TARGETING_POLICY_LIMIT_EXCEEDED` | IDs of the ad products that would exceed the limit |
| `conflicts` | `TARGETING_COLLECTION_CONTEXT_DUPLICATED` | Up to 20 conflicts. Each has `value` (the Context ID), `collection` (`_id` and `name` of the other option group), and `targeting` (`_id` and `name` of its targeting). If that option group is created or changed by the same request, `collection._id`, `targeting`, or `targeting.name` can be missing. |

***

## Bulk Responses

Bulk endpoints, such as `POST /context-targeting/bulk/create` and `POST /context-targeting/category/bulk/update`, return a result for each item.

### Response Fields

| Field | Type | Description |
| - | - | - |
| `results` | object\[] | Item results, in request order |
| `results[].index` | number | Position of the item in the request's `items` or `ids` array, starting at 0 |
| `results[].success` | boolean | `true` if the item succeeded |
| `results[].data` | object | Result of a successful item. The same object that the single-item endpoint returns. For deletes, only `_id`. |
| `results[].error` | object | Error of a failed item, with `statusCode`, `errorCode`, and, for some codes, `detail`. See [Errors by Code](#errors-by-code). |
| `succeeded` | number | Number of successful items |
| `failed` | number | Number of failed items |

### Response Status

When a bulk request is processed, it returns `200`, even if some or all items fail. Check `failed`, or `success` of each item.

In these cases, the whole request fails without `results`, and nothing in it is applied:

* `400` validation error. For example, the request has too many items, more than 10,000 Context IDs in total, or the same `_id` twice in an option bulk update.
* `400 TARGETING_CATEGORY_TREE_ITEMS_LIMIT_EXCEEDED` in an option bulk delete
* `401`, `403`, or `413`
* `404 PROJECT_NOT_FOUND`
* `409 CONTEXT_TARGETING_WRITE_IN_PROGRESS`

A `500` or `502` response also comes without `results`, but the request may or may not have been applied.

### How Items Are Applied

| Bulk request | How items are applied |
| - | - |
| Create targetings, delete targetings, add options, delete options | Together, as one unit. Items that fail a check before they're written, such as a duplicate name or an exceeded limit, fail on their own, and the other items are applied. If an unexpected error occurs, nothing in the request is applied: items that had already failed keep their errors, and every other item fails with `500 UNCATCHED`. Send the same request again. |
| Rename targetings, update options | Item by item. A failed item doesn't affect the others, and successful items stay applied. |

* An option bulk update applies items concurrently, but `results` stay in request order. Put all changes to one option in a single item. The same `_id` twice rejects the whole request.
* A later item in a rename or update request can use a name that an earlier item frees by renaming. If the earlier item fails, the later item fails with a name duplicate error.

### Example

The first item adds a Context ID to **Sports**. The second item tries to rename **Tech** to **Sports**, which is already in use.

```bash theme={null}
curl -X POST https://lake.adrop.io/context-targeting/category/bulk/update \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "_id": "01JAB3XK5RA1B2C3D4E5F6G7H8", "contexts": { "add": ["basketball"] } },
      { "_id": "01JAB3XK5RA1B2C3D4E5F6G7H9", "name": "Sports" }
    ]
  }'
```

The response status is `200`.

```json theme={null}
{
  "results": [
    {
      "index": 0,
      "success": true,
      "data": {
        "_id": "01JAB3XK5RA1B2C3D4E5F6G7H8",
        "name": "Sports",
        "type": "atomic",
        "contextCount": 4,
        "createdAt": "2026-10-13T01:23:45.000Z",
        "updatedAt": "2026-10-13T02:10:00.000Z"
      }
    },
    {
      "index": 1,
      "success": false,
      "error": {
        "statusCode": 409,
        "errorCode": "TARGETING_CATEGORY_NAME_DUPLICATE",
        "detail": {
          "existingIds": ["01JAB3XK5RA1B2C3D4E5F6G7H8"]
        }
      }
    }
  ],
  "succeeded": 1,
  "failed": 1
}
```

***

## Concurrent Writes

API writes for a project are processed one at a time. This covers every `POST`, `PATCH`, and `DELETE` request of the Context Targeting API, single-item and bulk, from any API key of the project. `GET` requests aren't affected, and writes for different projects don't wait for each other.

When a write arrives while another write for the project is in progress, the server waits briefly for it to finish. If it's still running, the request fails with `409 CONTEXT_TARGETING_WRITE_IN_PROGRESS`, and nothing in the request is applied. A bulk request fails as a whole, without `results`.

```json theme={null}
{
  "statusCode": 409,
  "errorCode": "CONTEXT_TARGETING_WRITE_IN_PROGRESS",
  "timestamp": "2026-10-13T10:23:45+09:00",
  "detail": {
    "retryAfterMs": 2000
  }
}
```

To handle this error:

1. Wait `detail.retryAfterMs` milliseconds. The value is between 0 and 2,000.
2. Send the same request again. If you get `409` again, repeat.

Send writes for a project in order: wait for the response to one request before you send the next. If several processes or workers write to the same project, each one should retry on `409`.

<Note>
  In rare cases, a request that runs unusually long is taken over by another request. Items that the first request hadn't applied fail with the same `409` in `results`, or as the response of a single-item endpoint. Items with this error weren't applied, so send only those items again.
</Note>

***

## Retries and Duplicate Requests

If a request times out or returns `502` or another `5xx` status, you may not know whether it was applied. The server may still be processing the original request. Wait a moment, then send the same request again. If the original request is still in progress, the resend fails with `409 CONTEXT_TARGETING_WRITE_IN_PROGRESS`. Wait and send it again.

What happens when you resend a request that had already succeeded depends on the operation.

| Operation | Result of the resend |
| - | - |
| Create a targeting or add an option, single or bulk | Not idempotent. Each item that the original request created fails with a name duplicate error, and `detail` has the existing ID. |
| Rename a targeting or option | Same result. Sending the current name isn't an error. |
| Change Context IDs with `add`, `remove`, or `set` | Same result. `add` skips Context IDs that already exist, and `remove` ignores Context IDs that don't exist. |
| Move or detach an option with `parent` | Same result. An option already in the requested position stays there. |
| Change `exposeChildren` | Same result |
| Delete a targeting or option | `404 TARGETING_NOT_FOUND` or `TARGETING_CATEGORY_NOT_FOUND`. Treat it as deleted. |

### Resending a Create

The server always generates IDs, and `_id` in a create request fails with `400`. If you resend a create that had already succeeded, use the IDs in the `409` response instead of creating the item again.

* **Targeting**: `409 TARGETING_NAME_DUPLICATE`. `detail.existingId` is the ID of the targeting. To get the IDs of its options, see [Get a Targeting](/integrations/context-targeting/targetings#get-a-targeting).
* **Option**: `409 TARGETING_CATEGORY_NAME_DUPLICATE`. `detail.existingIds` has the IDs of the options with that name.
* In a bulk create, only the items that the original request created fail this way. The other items are processed as usual.

`detail.existingId` is included only when the existing targeting is a context targeting. If an audience targeting uses the name, choose another name. A name that repeats within the same request returns `detail.duplicateOfIndex` instead. Fix the request in that case.

Because API writes for a project are processed one at a time, two identical create requests sent at the same time don't both create. The later one fails with a name duplicate error or with `409 CONTEXT_TARGETING_WRITE_IN_PROGRESS`.

### After a 500 or 502

* `500 UNCATCHED` in the `results` of a create or delete bulk request: nothing in the request was applied. Send the same request again.
* `500 UNCATCHED` in the `results` of an update bulk request: that item failed. Send it again.
* A `500` or `502` response: the request may or may not have been applied. Send the same request again, and handle the result as shown in the table above.

<Note>
  If an option update has both `add` and `remove` and fails, the `add` may already be applied. Sending the same update again is safe.
</Note>

***

## Limits

| Limit | Value | If exceeded |
| - | - | - |
| Items per targeting bulk request (create, rename, delete) | 1–100 | `400` validation error |
| Options per targeting create request, including sub-options, all items combined | 100 | `400` validation error |
| Items per option bulk request (add, update, delete) | 1–5,000 | `400` validation error |
| Options per option add request, including sub-options, all items combined | 5,000 | `400` validation error |
| Option group items per option bulk request: items that create an option group or have `parent` (add), items with `parent`, `null` included (update), IDs of option groups and sub-options (delete) | 150 | `400` validation error for add and update, `400 TARGETING_CATEGORY_TREE_ITEMS_LIMIT_EXCEEDED` for delete |
| Context IDs per request, all items combined, including `add`, `remove`, and `set` | 10,000 | `400` validation error |
| Length of a Context ID, after leading and trailing whitespace is trimmed | 1–256 bytes in UTF-8 | `400` validation error |
| Request body size | 8 MB | `413` |
| Context IDs per option (for an option group, its own Context IDs) | 50,000 | `400 TARGETING_CONTEXT_LIMIT_EXCEEDED` |
| Total size of an option's Context IDs (for an option group, its own Context IDs) | 8 MB | `400 TARGETING_CONTEXT_LIMIT_EXCEEDED` |
| Context IDs per project, across all options of all context targetings | 200,000 | `400 TARGETING_CONTEXT_LIMIT_EXCEEDED` |
| Options per targeting, including sub-options | 100,000 | `400 TARGETING_CATEGORY_LIMIT_EXCEEDED` |
| Options set as **Required Include** or **Required Exclude** per ad product, across all targetings | 500 | `400 AD_PRODUCT_TARGETING_POLICY_LIMIT_EXCEEDED` |
| Length of a targeting or option name | 2–36 characters | `400` validation error |
| `limit` of [List Options](/integrations/context-targeting/options#list-options) | 1–1,000 (default 1,000) | `400` validation error |
| `limit` of [Get an Option](/integrations/context-targeting/options#get-an-option) | 1–10,000 (default 10,000) | `400` validation error |
| `offset` of Get an Option | 0–50,000 (default 0) | `400` validation error |
| Sub-option IDs in an option group's `children` | First 50. `childCount` has the total. | — |
| Conflicts in `detail.conflicts` | Up to 20 | — |

* Limits on stored data (per option, per project, per targeting, and per ad product) are checked only when a change increases the amount. Changes that reduce it, such as removing Context IDs, aren't blocked even if the data is already over a limit.
* To upload more than one request can hold, split it into requests within these limits, and send them one after another. For example, add tens of thousands of options with option bulk add requests of up to 5,000 items each.
* To replace the Context IDs of an option that has more than 10,000, see [Updating Context IDs](/integrations/context-targeting/options#updating-context-ids).
* Send non-ASCII text, such as Korean names and Context IDs, as raw UTF-8 to keep the request body small.

***

## Name Rules

Targeting and option names, including option groups and sub-options, follow these rules.

| Rule | Targeting | Option |
| - | - | - |
| Length | 2–36 characters | 2–36 characters |
| Allowed characters | Korean, English letters, digits, spaces, and `~` `%` `&` `+` `-` | Same as targeting |
| Leading or trailing spaces | Not allowed. The API rejects the name instead of trimming it. | Same as targeting |
| Must be unique within | The project, across all targetings including audience targetings | The targeting, including option groups and sub-options |
| Error for an invalid name | `400` validation error | `400` validation error |
| Error for a duplicate name | `409 TARGETING_NAME_DUPLICATE` | `409 TARGETING_CATEGORY_NAME_DUPLICATE` |

* Other characters aren't allowed, such as Japanese or Chinese characters, `_`, `.`, and brackets.
* Names that repeat within one request are also duplicates. See `duplicateOfIndex` in [Detail Fields](#detail-fields).
* Renaming to the current name isn't an error.
* Options in different targetings can have the same name.

***

## Using the API with the Console

* **Same data**: The console and the API manage the same context targetings. Targetings and options that you create with the API appear in the console, and the API returns those created in the console.
* **Context targetings only**: Requests for an audience targeting fail with `404 TARGETING_NOT_FOUND`, and requests for its options fail with `404 TARGETING_CATEGORY_NOT_FOUND`. Audience targeting names still count for name uniqueness, so creating a context targeting with the same name fails with `409 TARGETING_NAME_DUPLICATE` without `detail.existingId`.
* **No write ordering for console edits**: The one-at-a-time processing applies only to API writes. A console edit doesn't wait for an API write and doesn't cause `409 CONTEXT_TARGETING_WRITE_IN_PROGRESS`, and an API write doesn't wait for a console edit.
* **Simultaneous changes can conflict**: When the console and the API change the same targeting at the same time, a change saved later can overwrite an earlier one, and two targetings or options can end up with the same name. Don't edit a targeting in the console while your integration writes to it. If it may have been changed in the console, get it again before you write.
* **Duplicate names**: If two targetings or options end up with the same name, rename or delete one of them. Before you delete an option, check [Effects on Ad Products and Campaigns](/integrations/context-targeting/options#effects-on-ad-products-and-campaigns).
* **Ad product settings**: To change which options an ad product sets as **Required Include** or **Required Exclude**, use the console. See [Selling Targeting](/targeting/sell).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.