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

# Bulk operations

> Four batch endpoints, one response envelope, and one thing you must check.

Four endpoints take a batch instead of a single item. They share one response envelope, and one
habit: read `failed_count`, not the HTTP status.

| Operation           | Path                             | Response `object`      | Success array |
| ------------------- | -------------------------------- | ---------------------- | ------------- |
| Create redirects    | `POST /v1/redirects/bulk`        | `bulk_create_result`   | `created`     |
| Delete redirects    | `POST /v1/redirects/bulk/delete` | `bulk_delete_result`   | `deleted`     |
| Add to a group      | `POST /v1/groups/{id}/assign`    | `bulk_assign_result`   | `assigned`    |
| Remove from a group | `POST /v1/groups/{id}/unassign`  | `bulk_unassign_result` | `unassigned`  |

A batch costs **1** against your quota, however many items it contains. That is the main reason
to use these endpoints.

Every batch is 1–100 elements.

## The response

The status is always `200`, including when every element failed.

```json theme={null}
{
  "object": "bulk_create_result",
  "failed_count": 1,
  "created_count": 2,
  "created": [
    { "index": 0, "data": { "id": "rdr_9f2c…", "object": "redirect", "url": "https://teasy.link/promo" } },
    { "index": 2, "data": { "id": "rdr_1a2b…", "object": "redirect", "url": "https://teasy.link/sale" } }
  ],
  "failed": [
    {
      "index": 1,
      "error": {
        "code": "slug_already_taken",
        "message": "This address is already taken.",
        "value": "links.example.com/promo"
      }
    }
  ]
}
```

<Warning>
  **Check `failed_count`, not the HTTP status.** A client that only checks `response.ok` will read
  a `200` full of failures as a success.
</Warning>

**`index` is the position in the array you sent** — for creation it is the only way to match a
result back to your data.

Creation and group operations return the resource in the flat list form, not the card. Deletion
returns `{ id, object, deleted: true }`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const items = [
    { name: "Promo", target_url: "https://example.com/a", slug: "promo" },
    { name: "Sale", target_url: "https://example.com/b", slug: "sale" },
  ];

  const res = await fetch("https://api.teasy.link/v1/redirects/bulk", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.TEASY_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ items }),
  });

  const result = await res.json();

  for (const { index, data } of result.created) {
    console.log(`${items[index].name} -> ${data.url}`);
  }

  // A 200 can be full of failures, so this branch is not optional.
  for (const { index, error } of result.failed) {
    console.error(`${items[index].name} failed: ${error.code}`, error.value);
  }
  ```

  ```python Python theme={null}
  items = [
      {"name": "Promo", "target_url": "https://example.com/a", "slug": "promo"},
      {"name": "Sale", "target_url": "https://example.com/b", "slug": "sale"},
  ]

  res = requests.post(
      "https://api.teasy.link/v1/redirects/bulk",
      json={"items": items},
      headers={"Authorization": f"Bearer {os.environ['TEASY_KEY']}"},
  )
  result = res.json()

  for entry in result["created"]:
      print(items[entry["index"]]["name"], "->", entry["data"]["url"])

  # A 200 can be full of failures, so this branch is not optional.
  for entry in result["failed"]:
      error = entry["error"]
      print(items[entry["index"]]["name"], "failed:", error["code"], error["value"])
  ```
</CodeGroup>

## Per-element errors

`failed[].error` has no HTTP status — only `code`, `message` and `value`. `value` is always
present, `null` where there is nothing to point at. Offending data is not interpolated into
`message`; read it from `value`.

| `code`                                                                     | When                                             | `value`                     |
| -------------------------------------------------------------------------- | ------------------------------------------------ | --------------------------- |
| `duplicate_id_in_batch`                                                    | The id appears more than once in the batch       | `"rdr_9f2c…"`               |
| `duplicate_slug_in_batch`                                                  | Two elements claim the same address              | `"links.example.com/promo"` |
| `redirect_not_found` / `landing_not_found`                                 | Passed ID is not able to interact                | `"rdr_9f2c…"`               |
| `group_not_found`                                                          | The element's `group_id` is unknown              | `"grp_4d8e…"`               |
| `slug_already_taken`                                                       | The address is taken outside the batch           | `"links.example.com/promo"` |
| `plan_limit_reached`                                                       | Your plan ran out of slots                       | `null`                      |
| `analytics_temporarily_unavailable`                                        | Analytics could not be provisioned for this link | `null`                      |
| `domain_not_found`, `domain_not_active`, `slug_required_for_system_domain` | The element's domain                             | `null`                      |

### What fails the whole request instead

Malformed input is rejected wholesale with `400`: a bad prefix, a non-hex id, a batch that is
too large or empty. Two more cases go the same way:

* **An unknown country code.** Every unrecognised code is listed in `details`.
* **The group in the path.** Unknown or not yours returns `404 group_not_found`, since no
  element can be carried out without it.

Duplicates within a batch are treated per element, not as malformed input.

### Fitting plan limits

When creating entities in bulk that are subject to limits, it’s important to understand how we handle them. We don’t reject the entire request if your current number of links plus N exceeds your plan limit.

Instead, the system creates as many entities as allowed, in the order they were provided. The remaining ones will fail with `plan_limit_reached`.
