> ## 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.

# Links and Groups

> Domains, addresses, landing pages and redirects — what they are and how to work with them.

With the Teasy API you can:

* **Create redirect links** — one at a time, or [in bulk](/api-v1/api-v1/bulk-operations), up to 100 per call.
* **Read landing pages** built in the dashboard. Creating and editing them is not available in v1.
* **Serve links from a domain of your own**, once you have [connected](/api-v1/api-v1/domains) it.
* **Group links** together, to keep campaigns apart and to read analytics per group.

## Your first redirect

A redirect needs a name, a target and an address. On our system domain `teasy.link` the `slug` is
what makes that address unique, so it is required:

```bash theme={null}
curl -X POST https://api.teasy.link/v1/redirects \
  -H "Authorization: Bearer tsl_live_<32 characters>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Promo",
    "target_url": "https://yourcontent.com/kate",
    "slug": "promo"
  }'
```

`201`, and the link is live:

```json theme={null}
{
  "id": "rdr_9f2c5a4b7d4c31a2f86e0d91b3c7aa10",
  "object": "redirect",
  "name": "Promo",
  "url": "https://teasy.link/promo",
  "target_url": "https://yourcontent.com/kate",
  "slug": "promo",
  "is_active": true,
  "note": null,
  "antibot_enabled": false,
  "group_id": null,
  "domain": "teasy.link",
  "created_at": "2026-08-14T10:00:00Z",
  "updated_at": "2026-08-14T10:00:00Z"
}
```

## Using your own domain

Before you didn't pass any domain (or passed `teasy.link` which are identical result). But you may to use your own.

```json theme={null}
{
  "name": "Summer promo",
  "target_url": "https://shop.com/kate",
  "slug": "summer",
  "domain": "links.yourbrand.com"
}
```

<Tip>
  A domain works here only once it is verified and `active`. See [Custom domains](/api-v1/api-v1/domains).
</Tip>

## Putting a link into a group

<Steps>
  <Step title="Create the group">
    ```bash theme={null}
    curl -X POST https://api.teasy.link/v1/groups \
      -H "Authorization: Bearer tsl_live_<32 characters>" \
      -H "Content-Type: application/json" \
      -d '{ "name": "Summer campaign" }'
    ```

    ```json theme={null}
    {
      "id": "grp_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
      "object": "group",
      "name": "Summer campaign",
      "landings_count": 0,
      "redirects_count": 0,
      "created_at": "2026-08-14T10:00:00Z",
      "updated_at": "2026-08-14T10:00:00Z"
    }
    ```
  </Step>

  <Step title="Assign the link to it">
    ```bash theme={null}
    curl -X PATCH https://api.teasy.link/v1/redirects/rdr_9f2c5a4b7d4c31a2f86e0d91b3c7aa10 \
      -H "Authorization: Bearer tsl_live_<32 characters>" \
      -H "Content-Type: application/json" \
      -d '{ "group_id": "grp_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" }'
    ```

    The updated redirect comes back with `group_id` set. To move several links at once, use
    [assign](/api-v1/api-v1/api-reference/groups/add-links-to-a-group).
  </Step>
</Steps>

## How be build URLs

```json theme={null}
{
  "url": "https://teasy.link/promo",
  "domain": "teasy.link",
  "slug": "promo"
}
```

`url` is computed from the other two. `domain` is a plain hostname string — the same value you
sent when creating a link. But what should you send? Let's see.

| What you send                      | How we handle this                                                                            | What will you get                 |
| ---------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------- |
| No `domain`, no `slug`             | If we don't get domain from you - we use our `teasy.link`. But in that case we require `slug` | `slug_required_for_system_domain` |
| No `domain`, `slug: promo`         | Well no domain provided so we take our, and yeah we got slug. Ok.                             | `teasy.link/promo`                |
| `domain: brand.com`, `slug: promo` | Okay, both provided. Domain is even active. Great.                                            | `brand.com/promo`                 |

<Warning>
  It’s important to remember that a slug is unique within a single domain. You can create a link without providing a slug for your domain and get a clean link like `yourbrand.com`, but this link will be unique. All other links on that domain will need to have a unique slug.
</Warning>

Slugs are 3–25 characters, lowercase letters and hyphens only — no digits.

## Creating a redirect

| Field             | Type    | Required           | Rules                                                                |
| ----------------- | ------- | ------------------ | -------------------------------------------------------------------- |
| `name`            | string  | yes                | 1–100                                                                |
| `target_url`      | string  | yes                | http or https only                                                   |
| `slug`            | string  | if our domain used | 3–25, `^[a-z-]+$`                                                    |
| `note`            | string  | no                 | Up to 256. Private, never shown to visitors                          |
| `domain`          | string  | no                 | Hostname; defaults to the system domain                              |
| `group_id`        | string  | no                 | `grp_…`                                                              |
| `is_active`       | boolean | no                 | Defaults to `true`                                                   |
| `antibot_enabled` | boolean | no                 | Defaults to `false`. Screens suspicious traffic                      |
| `geo_blocklists`  | object  | no                 | `{ "countries": ["US", "CA"] }`. Requires geo filtering on your plan |

`PATCH` takes the same fields, all optional; `null` clears. See
[Response shapes](/api-v1/api-v1/responses#null-and-omission-in-a-patch).

Deactivating a link is a `PATCH` with `is_active: false`. Inactive links stop resolving but keep
their statistics.

## Two shapes per link

Lists return a flat form. Retrieving one link returns the card, which adds `pixels` and
`geo_blocklists` — and for landing pages the page's own content. Bulk operations return the flat
form.

## Geo blocklists

Same format on input and output:

```json theme={null}
"geo_blocklists": { "countries": ["US", "CA", "RU"] }
```

* **Country level only.** Regions and cities configured in the dashboard are neither returned nor
  modified by this API.
* An empty array or `null` removes all country blocks.
* ISO 3166-1 alpha-2, normalised to upper case. An unknown or duplicated code fails the whole
  request with `400 invalid_request` and lists the offenders in `details`.

## Groups

```json theme={null}
{
  "id": "grp_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
  "object": "group",
  "name": "Summer campaign",
  "landings_count": 3,
  "redirects_count": 12,
  "created_at": "2026-08-14T10:00:00Z",
  "updated_at": "2026-08-14T10:00:00Z"
}
```

We don't provide nested links inside group's object. To get it's children you can request links using `?group_id=` [query](/api-v1/api-v1/pagination#filtering-by-group).

`name` is the only editable field, 1–100 characters.

### Membership

[`assign`](/api-v1/api-v1/api-reference/groups/add-links-to-a-group) and
[`unassign`](/api-v1/api-v1/api-reference/groups/remove-links-from-a-group) take redirects and landing pages
mixed together in one `link_ids` array. See [Bulk operations](/api-v1/api-v1/bulk-operations).

A link already in the target state counts as a **success**, not an error, so retrying a failed
batch converges instead of compounding.

Unassigning requires the group in the path to be the one the links currently belong to. Pass a
different group and the elements come back as failures.

### Deleting a group

After deleting group it's links will not be deleted. They will become ungrouped and will be able inside `?group_id=none` list.
