# Links

Source: https://developers.leonai.io/docs/guides/links/

Everything you can do with short links, shown as REST endpoints next to the planned
[TypeScript SDK](https://developers.leonai.io/docs/sdks/) interface (not published yet). Requires `links:read` / `links:write` scopes.

## Create

```ts
const created = await leonai.links.create({
  longUrl: 'https://example.com/spring-sale',
  shortCode: 'spring-sale', // optional custom back-half (Pro+); omit for an auto code
  title: 'Spring Sale',
  domainId: 'dom_...', // optional — a verified custom domain you own
  password: 's3cret', // Pro+ — password-protect the link
  tags: ['marketing', 'q2'], // up to 10 tags, normalized (lowercased, dashed)
  expiresAt: '2026-08-01T00:00:00Z', // Pro+
  conversionTracking: true, // Business+ — appends ?lc_id=<clickId>
  utmSource: 'newsletter',
  utmMedium: 'email',
  utmCampaign: 'spring',
  utmTerm: 'shoes', // optional
  utmContent: 'hero', // optional
});
```

Only `longUrl` is required. `REST: POST /api/v1/links` → `201 Created`.

:::note[Field names & tier gates]
The API uses `shortCode` for a custom back-half on **create** (3–50 chars,
`[a-zA-Z0-9_-]`). Bulk import rows use `customCode` (see below). URLs must be
`http`/`https` and up to 2048 characters. Several fields are tier-gated: a custom
`shortCode`, `password`, and `expiresAt` require **Pro+**; `conversionTracking` requires
**Business+**. Requesting a gated field on a lower tier returns `403`.
:::

## Get by id or short code

```ts
const link = await leonai.links.get('spring-sale');
```

`REST: GET /api/v1/links/:id` — the path segment accepts **either** the link id **or**
the short code. (The `/stats` and analytics endpoints, by contrast, expect the link id.)

## List with filtering + pagination

```ts
const page = await leonai.links.list({
  search: 'sale',
  tags: ['marketing'], // matches links containing ALL given tags
  limit: 50,
  offset: 0,
});
console.log(page.pagination); // { total, limit, offset, hasMore }
```

`REST: GET /api/v1/links?search=sale&tags=marketing&limit=50&offset=0`

### Iterate every matching link

The planned SDK fetches pages transparently:

```ts
for await (const l of leonai.links.listAll({ tags: ['marketing'] })) {
  console.log(l.shortUrl, l.clicks);
}
```

## Update

Partial updates are allowed. `REST: PATCH /api/v1/links/:id`.

```ts
await leonai.links.update(link.data.id, {
  title: 'Spring Sale',
  expiresAt: '2026-08-01T00:00:00Z',
});
```

## Delete

```ts
await leonai.links.delete(link.data.id);
```

`REST: DELETE /api/v1/links/:id`. The short URL stops redirecting, the cache is
invalidated, and a `link.deleted` [webhook](https://developers.leonai.io/docs/guides/webhooks/) fires.

## Lifetime stats

```ts
const { data: stats } = await leonai.links.stats(link.data.id);
```

## Bulk import

Import up to **1000** links per request — as JSON rows or a raw CSV string.

```ts
const bulk = await leonai.links.bulkCreate({
  format: 'json',
  data: [
    { longUrl: 'https://example.com/a' },
    { longUrl: 'https://example.com/b', customCode: 'b' },
  ],
});
console.log(bulk.data.successful, bulk.data.failed);
```

`REST: POST /api/v1/links/bulk`

## Export

Export everything as JSON (default) or a CSV string:

```ts
const json = await leonai.links.export();
const csv = await leonai.links.export({ format: 'csv' });
```

## Related

- [Analytics](https://developers.leonai.io/docs/guides/analytics/) — clicks and conversions per link
- [QR codes](https://developers.leonai.io/docs/guides/qr-codes/) — a scannable image for any link
- [Smart routing](https://developers.leonai.io/docs/guides/smart-routing/) — geo/device/time/A-B rules
