# Create links

Create Clikso short links with the API, with a random or custom slug, on a free Clikso domain or your own verified domain.

> **In short:**
>
> Send `POST /links` with a `destination_url` and you get a short link back. You can also pick the slug, the domain, a title, a folder and tags in the same request.

## You need

* **API key:** A key with **Write** permission. See [Authentication](https://clik.so/docs/authentication).
* **Role:** Editor or admin in the key’s workspace.
* **Plan:** Any plan, including Free. Each plan has a monthly link limit — see [Limits](https://clik.so/docs/limits).

## Make a link with a random slug

The slug is the part after the domain, like `summer-sale` in `clik.is/summer-sale`. If you leave it out, Clikso picks a random one with 7 letters and digits.

```bash
curl -X POST "https://app.clik.so/api/links" \
  -H "Authorization: Bearer $CLIKSO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "destination_url": "https://example.com/summer-sale"
  }'
```

You get a `201` with the new link. Its `short_code` is the random slug, for example `aB3x9Kq`.

## Choose your own slug

A custom slug can use letters, digits and hyphens. It must be 3 to 30 characters long. Underscores and spaces are not allowed.

Slugs are case-sensitive. `Summer` and `summer` are two different links.

On Starter and up, a slug can also contain `/` to group links, like `acme/launch`. This is a nested slug. The rules:

* Up to 5 parts, so at most four slashes.
* The 30 characters include the slashes.
* No `/` at the start or the end, and no `//`.
* Each part needs at least one letter or digit.

On Free, a slug with `/` gets `402 UPGRADE_REQUIRED`. Nested slugs work on Clikso domains and on your own domain.

### Step 1. Check that the slug is free

Ask before you create, so you can offer another slug at once.

```bash
curl "https://app.clik.so/api/links/slug-check?slug=summer-sale&domain=clik.is" \
  -H "Authorization: Bearer $CLIKSO_API_KEY"
```

Response 200:

```json
{
  "status": 200,
  "success": true,
  "message": "Success",
  "data": {
    "available": true,
    "slug": "summer-sale"
  }
}
```

`available: true` means nobody holds the slug on that domain and a create would accept it. When it is `false`, `reason` may say why. `invalid_format` means it breaks the rules above. `reserved` means Clikso keeps that name, like `pricing` on clik.is. Then `message` holds a short sentence you can show. A slug that is already taken comes back as just `available: false`.

### Step 2. Create the link with it

```bash
curl -X POST "https://app.clik.so/api/links" \
  -H "Authorization: Bearer $CLIKSO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "destination_url": "https://example.com/summer-sale",
    "short_code": "summer-sale"
  }'
```

If someone takes the slug between your two calls, you get `409 CONFLICT`. Pick another one and try again.

Need a random slug before you create the link, to show it in your app? Call `GET /links/generate-slug`. It returns a free slug for the domain you name.

## Pick a domain

Every link lives on one domain. If you send no domain, the link uses `clik.is`. The API does not use a default domain picked in the dashboard.

### Use a free Clikso domain

Clikso has a few free domains, such as `watch.clik.is` and `buy.clik.is`. They are free on every plan. List them first:

```bash
curl "https://app.clik.so/api/system-domains" \
  -H "Authorization: Bearer $CLIKSO_API_KEY"
```

Response 200:

```json
{
  "status": 200,
  "success": true,
  "message": "Success",
  "data": {
    "default_domain": "clik.is",
    "domains": [
      {
        "domain_name": "watch.clik.is",
        "display_order": 1
      },
      {
        "domain_name": "link.clik.is",
        "display_order": 2
      },
      {
        "domain_name": "buy.clik.is",
        "display_order": 3
      }
    ]
  }
}
```

Then send the name in `domain`:

```bash
curl -X POST "https://app.clik.so/api/links" \
  -H "Authorization: Bearer $CLIKSO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "destination_url": "https://example.com/summer-sale",
    "short_code": "summer-sale",
    "domain": "watch.clik.is"
  }'
```

### Use your own domain

A custom domain is one you own, like `links.example.com`. Add and verify it in the dashboard first. The API can read domains but cannot add them.

Find the domain’s `id` with this call. Only domains with `is_verified: true` can hold links.

```bash
curl "https://app.clik.so/api/domains" \
  -H "Authorization: Bearer $CLIKSO_API_KEY"
```

Response 200:

```json
{
  "status": 200,
  "success": true,
  "message": "Success",
  "data": [
    {
      "id": "9e8d7c6b-5a4f-4e3d-2c1b-0a9f8e7d6c5b",
      "workspace_id": "8a1f2c3d-4b5e-4f60-9a7b-1c2d3e4f5a6b",
      "domain_name": "links.example.com",
      "cloudflare_hostname_id": "0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
      "verification_details": null,
      "is_verified": true,
      "ssl_status": "active",
      "is_default": false,
      "created_at": "2026-10-02T10:00:00.000Z",
      "updated_at": "2026-10-02T10:20:00.000Z"
    }
  ]
}
```

Then send that id as `custom_domain_id`. Do not send the name in `domain` — `domain` only takes Clikso’s own domains.

```bash
curl -X POST "https://app.clik.so/api/links" \
  -H "Authorization: Bearer $CLIKSO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "destination_url": "https://example.com/summer-sale",
    "short_code": "summer-sale",
    "custom_domain_id": "9e8d7c6b-5a4f-4e3d-2c1b-0a9f8e7d6c5b"
  }'
```

The new link has `domain: "links.example.com"` and `domain_status: "active"`.

## Add a title, folder and tags

You can label and file the link in the same request. `folder_id` and `tags` take ids. Get them from [Folders and tags](https://clik.so/docs/guides/folders-and-tags).

Keep `title` to 60 characters and `description` to 250.

```bash
curl -X POST "https://app.clik.so/api/links" \
  -H "Authorization: Bearer $CLIKSO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "destination_url": "https://example.com/summer-sale",
    "short_code": "summer-sale",
    "title": "Summer sale",
    "folder_id": "6b7c8d9e-0f1a-4b2c-8d3e-4f5a6b7c8d9e",
    "tags": [
      "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
    ]
  }'
```

Response 201:

```json
{
  "status": 201,
  "success": true,
  "message": "Link created successfully",
  "data": {
    "id": "3f6c1a2e-9b4d-4e8f-a1c2-7d9e0f1a2b3c",
    "workspace_id": "8a1f2c3d-4b5e-4f60-9a7b-1c2d3e4f5a6b",
    "custom_domain_id": null,
    "domain": "clik.is",
    "domain_status": "default",
    "title": "Summer sale",
    "description": "",
    "short_code": "summer-sale",
    "destination_url": "https://example.com/summer-sale",
    "status": "active",
    "expires_at": null,
    "expired_redirect_url": null,
    "folder_id": "6b7c8d9e-0f1a-4b2c-8d3e-4f5a6b7c8d9e",
    "ab_testing": [],
    "geo_targeting": null,
    "device_targeting": null,
    "qr_metadata": null,
    "og_preview": null,
    "meta_pixel_id": null,
    "tiktok_pixel_id": null,
    "ga4_measurement_id": null,
    "gtm_container_id": null,
    "has_password": false,
    "click_count": 0,
    "created_by": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
    "created_at": "2026-10-09T09:30:00.000Z",
    "updated_at": "2026-10-09T09:30:00.000Z",
    "tags": [
      {
        "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
        "name": "launch",
        "color": "#3b82f6"
      }
    ],
    "creator": {
      "id": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
      "display_name": "Asha Rao",
      "email": "asha@example.com",
      "photo_url": ""
    }
  }
}
```

## What you get back

A `201` status and the new link in `data`. The fields you will use most:

* `id` — keep it. You need it to change, archive or delete the link.
* `domain` and `short_code` — the two parts of the short URL.
* `status` — `active` for a new link.
* `tags` — the full tag objects, not just the ids.

The API does not return a ready-made short URL. Build it yourself: `https://` + `domain` + `/` + `short_code`.

## Common errors

| Status | Code                  | What it means                                                                                                                                                                                                | What to do                                                                                               |
| ------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
| 402    | `UPGRADE_REQUIRED`    | You set a field your plan does not include, like `geo_targeting` on Free. `details` names the field and the plan you need.                                                                                   | Leave the field out, or upgrade the workspace.                                                           |
| 403    | `FORBIDDEN`           | The key has only Read permission, or its owner is a viewer in the workspace.                                                                                                                                 | Check the key’s permission and your role. If you send `Workspace-Id`, it must match the key’s workspace. |
| 404    | `NOT_FOUND`           | `custom_domain_id` is not one of this workspace’s domains.                                                                                                                                                   | Check the id. Ids from another workspace are never found.                                                |
| 409    | `CONFLICT`            | Another link already uses this `short_code` on this domain.                                                                                                                                                  | Pick another `short_code`, or leave it out to get a random one.                                          |
| 422    | `VALIDATION_ERROR`    | A field is wrong. For example, the slug has an underscore, the `title` is over 60 characters, `expires_at` is not a date, or the custom domain is not verified. `details.field` names the field when it can. | Fix the field and send the request again.                                                                |
| 422    | `DESTINATION_BLOCKED` | A URL in the body points at another link shortener or a blocked site. Show `error.message` to your user.                                                                                                     | Show `error.message` to the user as it is. Do not retry.                                                 |
| 429    | `USAGE_LIMIT_REACHED` | The workspace made all the links its plan allows this month.                                                                                                                                                 | Wait for the reset (analytics sends `details.resets_at`) or upgrade.                                     |

## Good to know

* There is no bulk create. Send one request per link.
* Every URL in the body is checked, not just `destination_url`. That includes the expiry, A/B, country and device URLs.
* Each new link counts toward the monthly limit. On Free that is 50 links. See [Limits](https://clik.so/docs/limits).
* Clikso also refuses some slugs on its own domains, such as ones with blocked words. You get `409 CONFLICT`, as if the slug were taken.
* You can change the slug, the domain and the destination later with [Update a link](https://clik.so/docs/api/links/update-link). The old short URL stops working at once.
* Every field you can send is listed on [Create a link](https://clik.so/docs/api/links/create-link).

---

Source: https://clik.so/docs/guides/create-links
