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

# Shorten Link

> Create a trackable short link for any URL or offer.

Create a trackable short link for any URL or an offer from your catalog. Unlike `POST /v2/links` (which requires Offer Management and only supports PartnershipOffers), this endpoint works with any URL and with SMS Offers.

<Note>
  **Requires SMS product.** This endpoint is available to any account with the SMS product enabled. Returns `403 product_not_enabled` otherwise.
</Note>

## How It Works

There are two modes:

1. **URL mode** — provide a raw `url` and get a short link pointing directly at it. No affiliate tracking.
2. **Offer mode** — provide an `offerId` (from your SMS Offers, not PartnershipOffers) and the endpoint resolves the offer's tracking URL with proper TUNE/Everflow parameters, preserving affiliate attribution.

When a recipient clicks the short link:

```
Short link (list domain) -> Link tracking service -> Destination URL
```

## Body Parameters

<ParamField body="url" type="string">
  Raw destination URL to shorten. Must use `http` or `https` scheme. **Required** unless `offerId` is provided.
</ParamField>

<ParamField body="offerId" type="string">
  SMS Offer ID (from the `offers` collection). If provided, the endpoint resolves the offer's tracking URL with proper TUNE/Everflow params. `url` and `offerId` are mutually exclusive — provide exactly one; sending both returns `400 mutually_exclusive`.
</ParamField>

<ParamField body="listId" type="integer">
  Sending list ID — determines which shortener domain to use. **Required** unless `phoneNumber` is provided.
</ParamField>

<ParamField body="phoneNumber" type="string">
  Sending list phone number in E.164 format (e.g. `+18005551234`). Alternative to `listId`. **Required** unless `listId` is provided.
</ParamField>

<ParamField body="contactPhone" type="string">
  Contact phone number in E.164 format. Passed as a macro to offer URL templates (`{{phone}}`, also available as `{{phone_number}}`). Optional.
</ParamField>

<ParamField body="metadata" type="object">
  Key-value pairs stored on the short link for attribution. For example, `{"source": "ai_agent", "conversation_id": "abc123"}`. Optional. Constraints: at most 10 keys; keys must be strings and cannot start with `$`; values are coerced to strings and cannot exceed 500 characters. Violations return `400 invalid_metadata`.
</ParamField>

## Response Fields

<ResponseField name="shortUrl" type="string">
  Full short URL to include in your SMS message body. Uses the list's configured link shortener domain.
</ResponseField>

<ResponseField name="linkId" type="string">
  Unique short link ID. Appended to the domain to form the `shortUrl`.
</ResponseField>

<ResponseField name="destinationUrl" type="string">
  The final destination URL that the recipient lands on after redirect.
</ResponseField>

<ResponseField name="domain" type="string">
  The link shortener domain used for this short link.
</ResponseField>

<ResponseField name="offerId" type="string">
  The SMS offer ID the link is associated with, if an offer was used.
</ResponseField>

<ResponseField name="filterBots" type="boolean">
  Whether bot filtering is enabled for this link. Inherited from the offer's `filter_bots` setting when `offerId` is provided.
</ResponseField>

## Examples

### Shorten a raw URL

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://api.tracklysms.com/api/v2/links/shorten \
    -H "X-Api-Key: trk_your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://example.com/landing-page",
      "listId": 42
    }'
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api.tracklysms.com/api/v2/links/shorten",
      headers={
          "X-Api-Key": "trk_your_api_key_here",
          "Content-Type": "application/json",
      },
      json={
          "url": "https://example.com/landing-page",
          "listId": 42,
      },
  )

  data = response.json()
  print(data["shortUrl"])  # e.g. https://yourdomain.com/Ab3kX9q
  ```

  ```javascript Node.js theme={null}
  const response = await fetch("https://api.tracklysms.com/api/v2/links/shorten", {
    method: "POST",
    headers: {
      "X-Api-Key": "trk_your_api_key_here",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      url: "https://example.com/landing-page",
      listId: 42,
    }),
  });

  const data = await response.json();
  console.log(data.shortUrl);
  ```
</RequestExample>

### Shorten an offer link with metadata

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.tracklysms.com/api/v2/links/shorten \
    -H "X-Api-Key: trk_your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "offerId": "6651a3ef1234567890abcdef",
      "phoneNumber": "+18005551234",
      "contactPhone": "+15551234567",
      "metadata": {
        "source": "ai_agent",
        "conversation_id": "conv_abc123"
      }
    }'
  ```

  ```python Python theme={null}
  response = requests.post(
      "https://api.tracklysms.com/api/v2/links/shorten",
      headers={
          "X-Api-Key": "trk_your_api_key_here",
          "Content-Type": "application/json",
      },
      json={
          "offerId": "6651a3ef1234567890abcdef",
          "phoneNumber": "+18005551234",
          "contactPhone": "+15551234567",
          "metadata": {
              "source": "ai_agent",
              "conversation_id": "conv_abc123",
          },
      },
  )
  ```

  ```javascript Node.js theme={null}
  const response = await fetch("https://api.tracklysms.com/api/v2/links/shorten", {
    method: "POST",
    headers: {
      "X-Api-Key": "trk_your_api_key_here",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      offerId: "6651a3ef1234567890abcdef",
      phoneNumber: "+18005551234",
      contactPhone: "+15551234567",
      metadata: {
        source: "ai_agent",
        conversation_id: "conv_abc123",
      },
    }),
  });

  const data = await response.json();
  console.log(data.shortUrl);
  ```
</CodeGroup>

<ResponseExample>
  ```json Success (201) theme={null}
  {
    "shortUrl": "https://yourdomain.com/Ab3kX9q",
    "linkId": "Ab3kX9q",
    "destinationUrl": "https://example.com/offer?aff_sub=12345",
    "domain": "https://yourdomain.com",
    "offerId": "6651a3ef1234567890abcdef",
    "filterBots": false
  }
  ```

  ```json Error - Missing URL or Offer (400) theme={null}
  {
    "error": "url or offerId is required",
    "code": "missing_url_or_offer"
  }
  ```

  ```json Error - Invalid URL Scheme (400) theme={null}
  {
    "error": "URL must use http or https scheme",
    "code": "invalid_url_scheme"
  }
  ```

  ```json Error - Invalid Metadata (400) theme={null}
  {
    "error": "metadata cannot exceed 10 keys",
    "code": "invalid_metadata"
  }
  ```

  ```json Error - Unsafe URL (400) theme={null}
  {
    "error": "Blocked URL: resolves to a private IP address",
    "code": "unsafe_url"
  }
  ```

  ```json Error - Offer Not Found (404) theme={null}
  {
    "error": "Offer not found",
    "code": "offer_not_found"
  }
  ```

  ```json Error - No Domain Configured (409) theme={null}
  {
    "error": "No link domain configured for this list",
    "code": "no_domain_configured"
  }
  ```
</ResponseExample>

## Error Codes

| HTTP Status | Error Code                | Description                                                                                                                        |
| ----------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| 400         | `missing_url_or_offer`    | Neither `url` nor `offerId` was provided.                                                                                          |
| 400         | `mutually_exclusive`      | Both `url` and `offerId` were provided; supply exactly one.                                                                        |
| 400         | `missing_list_identifier` | Neither `listId` nor `phoneNumber` was provided.                                                                                   |
| 400         | `invalid_url_scheme`      | The `url` does not use `http` or `https` scheme.                                                                                   |
| 400         | `invalid_metadata`        | The `metadata` object violates a constraint (not an object, >10 keys, non-string key, key starting with `$`, or value >500 chars). |
| 400         | `unsafe_url`              | The `url` is blocked by SSRF safety checks (e.g. resolves to a private IP address).                                                |
| 400         | `invalid_phone`           | The `phoneNumber` is not a valid E.164 phone number.                                                                               |
| 403         | `product_not_enabled`     | Your account does not have the SMS product enabled.                                                                                |
| 404         | `offer_not_found`         | No active offer found matching the provided `offerId`.                                                                             |
| 404         | `list_not_found`          | The sending list was not found or does not belong to your account.                                                                 |
| 409         | `no_domain_configured`    | The sending list does not have a link shortener domain configured.                                                                 |
| 500         | `id_collision`            | Failed to generate a unique link ID after multiple attempts. Retry the request.                                                    |

## Notes

* The `metadata` field is stored on the `MessageShortLink` document and can be used for attribution tracking in analytics.
* When `offerId` is provided, the offer's `filter_bots` setting is inherited by the short link. The link tracking service handles bot filtering on click.
* Each call creates a new unique short link. There is no deduplication.
* This endpoint is separate from `POST /v2/links`, which continues to serve PartnershipOffers only.

## Next Steps

<CardGroup cols={2}>
  <Card title="Create Link (Offers)" icon="tag" href="/api-reference/v2/links/create-link">
    Create links for partnership offers
  </Card>

  <Card title="Link Tracking" icon="link" href="/guides/link-tracking/overview">
    Track clicks and attribute conversions
  </Card>
</CardGroup>
