Linky logoLinky
Docs navigation

Launch bundles — create

Create a Linky

POST /api/links is public. Anonymous callers get a claim token back; signed-in callers get a Linky attributed to their active Linky organization (or their user account, when no organization is active).

Basic create

POST /api/links

curl -X POST "https://getalinky.com/api/links" \
  -H "content-type: application/json" \
  --data-binary '{
    "urls": [
      "https://example.com",
      "https://example.org"
    ],
    "source": "agent",
    "title": "Release review bundle"
  }'

source is free-form; `web`, `cli`, `sdk`, `agent`, `unknown` are the conventional values.

Request body

FieldTypeNotes
urlsstring[]Required. 1–25 http/https URLs, length ≤ 2048 each.
sourcestringOptional. Free-form caller label used for ops.
title, descriptionstringOptional labels stored with the Linky.
urlMetadata{ note?, tags?, openPolicy? }[]Optional per-URL metadata aligned with urls.
emailstringOptional. Anonymous only. Flags the claim token for the named recipient.
resolutionPolicyResolutionPolicyOptional. Lock the Linky down from the first click. See Personalize.
metadataRecord<string, unknown>Optional free-form caller metadata. The _linky.* namespace is reserved for server-injected fields and is stripped from callers.

Optional header: Linky-Client: <tool>/<version> (e.g. cursor/skill-v1). Malformed values are silently dropped.

Create with a policy

Want the Linky personalized from the first click? Attach the policy in the create request. There's no window where an unrestricted version of the Linky exists — it's born with the rules in place.

POST /api/links with resolutionPolicy

curl -X POST "https://getalinky.com/api/links" \
  -H "content-type: application/json" \
  -H "Linky-Client: cursor/skill-v1" \
  --data-binary '{
    "urls": ["https://acme.com/docs", "https://acme.com/status"],
    "source": "agent",
    "title": "Acme standup",
    "email": "alice@acme.com",
    "resolutionPolicy": {
      "version": 1,
      "rules": [
        {
          "name": "Engineering team",
          "when": {
            "op": "endsWith",
            "field": "emailDomain",
            "value": "acme.com"
          },
          "tabs": [
            { "url": "https://linear.app/acme/my-issues" }
          ]
        }
      ]
    }
  }'

Anonymous creates with a policy are immutable until claimed — pass `email` so the claim URL reaches the eventual human owner.

Response — anonymous

{
  "slug": "x8q2m4k",
  "url": "https://getalinky.com/l/x8q2m4k",
  "claimUrl": "https://getalinky.com/claim/B6p…",
  "claimToken": "B6p…",
  "claimExpiresAt": "2026-05-16T12:00:00.000Z",
  "warning": "Save claimToken and claimUrl now — they are returned only once and cannot be recovered."
}

The claimToken is the raw secret. claimUrl wraps it for convenience. Returned once, cannot be recovered.

Response — signed-in

{
  "slug": "x8q2m4k",
  "url": "https://getalinky.com/l/x8q2m4k"
}

Signed-in creates omit every claim* field and the warning. Ownership is already resolved (org context wins, otherwise user).

Error codes

StatusCodeCause
400INVALID_URLSEmpty list, unsupported protocol, URL too long, > 25 URLs.
400BAD_REQUESTMalformed JSON, bad policy shape, email invalid.
400INVALID_JSONRequest body was not parsable JSON.
429RATE_LIMITEDAnonymous IP exceeded the create window. Defaults: 30 per 60s per IP.
500INTERNAL_ERRORServer / database issue; safe to retry.