---
title: "Create a link"
method: POST
path: "/v1/projects/{project}/links"
tags: ["Smart Links"]
---

# Create a link

`POST /v1/projects/{project}/links`

## Request

- Base URL: `https://api.subscriby.net`
- URL: `https://api.subscriby.net/v1/projects/{project}/links`
- Auth: HTTP bearer

```bash
curl -X POST https://api.subscriby.net/v1/projects/7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13/links \
  -H "Authorization: Bearer sbt_..." \
  -H "Idempotency-Key: 6b1f0d2e-9c3a-4e8b-b2d1-7a9c5e3f1d04" \
  -H "Content-Type: application/json" \
  -d '{"name":"YouTube description","slug":"youtube","destination":"plan","plan_id":"0c9d8e7f-6a5b-4c3d-8e2f-1a2b3c4d5e6f"}'
```

Answers `201` with the new link, its code minted and its three addresses built. `destination` is `portal` (the portal home), `plan` (the checkout of one plan, which needs `plan_id`) or `bot` (the project's bot; the portal home when no bot is connected). `expires_at`, in the future, stops the link counting after that moment.

The slug is lowercased before it is stored and checked, so `Promo` when `promo` exists is `422 VALIDATION_FAILED`, not a second link; `go`, `embed`, `portal`, `bot`, `direct`, `new` and `edit` are reserved. Emits `link.created`.

## Path parameters

- `project` string, uuid, required

## Headers

- `Idempotency-Key` string, uuid, required

## Request body

- StoreSmartLinkRequest: A Smart Link to create: its name, its address segment, where it sends people and, optionally, when it stops counting.
  - `name` string, required: A creator-facing label, 2 to 120 characters, never shown to visitors; such as `YouTube description`.
  - `slug` string, required: The address segment after `/go/`: 2 to 32 letters, digits and hyphens, lowercased before it is stored and checked. Unique within the project; `go`, `embed`, `portal`, `bot`, `direct`, `new` and `edit` are reserved. Such as `youtube`.
  - `destination` 'portal' | 'plan' | 'bot', required: Where a Smart Link sends the person who follows it. The destination decides the redirect and the bot payload: the portal's plan list, the checkout of one plan, or a conversation with the project's bot. The click and the attribution are recorded the same way whichever it is, so a creator can move a link between destinations without losing its figures.
  - `plan_id` string, nullable: The plan a `plan` destination opens; one of this project's plans. Refused as meaningless on the other destinations.
  - `expires_at` string, date-time, nullable: When the link stops counting and crediting, ISO 8601, in the future. Omit for never; the address keeps landing on the portal home after it.

## Response `201`

The new link, `201`.

- object
  - `data` SmartLinkResource, required: A Smart Link: a tracked address a creator hands out per channel (a video description, a bio, a flyer), with where it sends people, the three addresses to share and its lifetime clicks. Every member who signs in within the attribution window of clicking it is credited to the link.
    - `id` string, required: The link's id.
    - `project_id` string, required: The project the link belongs to; slugs are unique within a project, not across projects.
    - `name` string, required: A creator-facing label, never shown to visitors.
    - `slug` string, required: The address segment after `/go/`: 2 to 32 lowercase letters, digits and hyphens. Renaming it changes the web address; the code and the bot link stay.
    - `code` string, required: The stable seven-character code the bot link and the attribution cookie carry. Never changes, so a printed QR and a shared bot link keep working after a rename.
    - `destination` object, required: Where the link sends people.
      - `type` string, required: `portal` (the portal home), `plan` (the checkout of one plan) or `bot` (the project's bot, the portal home when no bot is connected).
      - `plan_id` string, nullable, required: The plan a `plan` destination opens; null otherwise.
      - `plan_name` string, nullable: That plan's name, present when the relation is loaded.
    - `urls` object: The addresses to share, each crediting this link.
      - `web` string, required: The `/go/` address on the portal host. Share it anywhere; a visitor is counted once per hour and remembered for the attribution window.
      - `bot` string, nullable, required: The project bot's deep link carrying `via_<code>`; null when the project runs no bot that hands out start links.
      - `embed` string, required: The plan-cards page to frame on the creator's own website, with the code in `via`; every card opens the portal in the top window through this link.
    - `state` 'expired' | 'live' | 'archived', required: `live` (counts and credits), `expired` (past `expires_at`: the address still lands on the portal home, nothing is counted) or `archived` (put out of use by the creator; the figures stay).
    - `expires_at` string, nullable, required: When the link stops counting, ISO 8601; null for never.
    - `archived_at` string, nullable, required: When the creator archived it, ISO 8601; null while in use.
    - `clicks` object, required: Lifetime clicks on the web address, one per visitor address per hour.
      - `total` integer, required: The count.
      - `last_at` string, nullable, required: When the address was last followed, ISO 8601; null when never.
    - `created_at` string, nullable, required: When the link was created, ISO 8601.
    - `updated_at` string, nullable, required: When it last changed, ISO 8601.

## Other responses

- `400`: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation.
- `401`: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (an `sbt_test_` token on production).
- `403`: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who owns a row or which tier the account is on answers `FORBIDDEN`, `TEAM_TIER_REQUIRED` or `CONNECTOR_TIER_REQUIRED` with the same status, and says so in its own description.
- `404`: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred.
- `409`: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body.
- `422`: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a plan that cannot go on sale or a member who cannot be removed, uses the same code with `error.message` saying why and no `fields`.
- `425`: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed.
- `429`: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted.

---

[API](https://skmtc.dev/memberpass/apis/subscriby-rest-api.md) · [All operations](https://skmtc.dev/memberpass/apis/subscriby-rest-api/llms.txt) · [OpenAPI document](https://skmtc.dev/memberpass/apis/subscriby-rest-api/revisions/59c4df91d764?raw)
