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
| Field | Type | Notes |
|---|---|---|
urls | string[] | Required. 1–25 http/https URLs, length ≤ 2048 each. |
source | string | Optional. Free-form caller label used for ops. |
title, description | string | Optional labels stored with the Linky. |
urlMetadata | { note?, tags?, openPolicy? }[] | Optional per-URL metadata aligned with urls. |
email | string | Optional. Anonymous only. Flags the claim token for the named recipient. |
resolutionPolicy | ResolutionPolicy | Optional. Lock the Linky down from the first click. See Personalize. |
metadata | Record<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
| Status | Code | Cause |
|---|---|---|
| 400 | INVALID_URLS | Empty list, unsupported protocol, URL too long, > 25 URLs. |
| 400 | BAD_REQUEST | Malformed JSON, bad policy shape, email invalid. |
| 400 | INVALID_JSON | Request body was not parsable JSON. |
| 429 | RATE_LIMITED | Anonymous IP exceeded the create window. Defaults: 30 per 60s per IP. |
| 500 | INTERNAL_ERROR | Server / database issue; safe to retry. |