slsh.me API v1
Bearer-token auth, JSON in/out, real-time clicks. Base URL: https://api.slsh.me/v1.
Authentication
Every request to /v1/* must include an Authorization: Bearer <token> header. Create tokens at app.slsh.me/settings/api. Tokens are named PATs; revoke any of them individually.
Missing or invalid tokens return 401 {"error": "invalid_token"}.
Pagination
List endpoints (GET /v1/links, GET /v1/links/:slug/clicks) return at most 100 items per page. Pass ?page=N (1-indexed; default 1) to navigate.
Response headers
| Name | Description |
|---|---|
| X-Total-Count | Total items across all pages. |
| Link | RFC 5988 link header with pre-built URLs for next, prev, first, and last — parse this rather than computing page numbers yourself. |
Example. When you ask for page 1 of a 250-item resource, the server replies with:
X-Total-Count: 250
Link: <https://api.slsh.me/v1/links?page=2>; rel="next", <https://api.slsh.me/v1/links?page=1>; rel="first", <https://api.slsh.me/v1/links?page=3>; rel="last"
Walk forward by following rel="next" until it's no longer present.
List your campaigns
GET
/v1/campaigns
Returns the authenticated organisation's campaigns, newest first. Archived campaigns are hidden; pass ?archived=true to list those instead. Paginated with ?page=N (1-indexed, 100/page); the response carries X-Total-Count and a standard Link header (RFC 5988).
Responses
| 200 | Success |
| 401 | Missing or invalid token |
Create a campaign
POST
/v1/campaigns
The slug is generated from the name when omitted, and becomes the default utm_campaign every link in the campaign inherits. A campaign's default custom domain is set in the web app — this endpoint always creates a campaign on the default domain.
Body parameters
| Name | Type | Required | Description |
|---|---|---|---|
| name | String | required | Display name |
| slug | String | Lowercase slug, unique within your organisation | |
| color | String | One of orange, blue, emerald, violet, rose | |
| icon | String | Phosphor icon name (fill weight) | |
| destination_url | String | Shared destination for the campaign's channel links | |
| title | String | Open Graph title | |
| description | String | Open Graph description | |
| goal_clicks | Integer | Click target for the campaign report | |
| starts_at | String | ISO 8601 start timestamp | |
| ends_at | String | ISO 8601 end timestamp; a campaign past it stops counting against your plan cap | |
| utm_source | String | utm_source every link in the campaign inherits | |
| utm_medium | String | utm_medium every link in the campaign inherits | |
| utm_campaign | String | Overrides the slug as the inherited utm_campaign | |
| utm_term | String | utm_term every link in the campaign inherits | |
| utm_content | String | utm_content every link in the campaign inherits | |
| custom_utms | Hash | Arbitrary UTM defaults keyed by bare suffix, e.g. {"office": "acme"} | |
Responses
| 201 | Created |
| 401 | Missing or invalid token |
| 422 | Invalid params, or your plan's active-campaign cap is reached |
Fetch a campaign by id
GET
/v1/campaigns/:id
clicks_count is the sum across every link in the campaign, links_count the number of links filed under it.
Responses
| 200 | Success |
| 401 | Missing or invalid token |
| 404 | Unknown campaign, or it belongs to another organisation |
Update a campaign
PATCH
/v1/campaigns/:id
Every create field can be changed. Changing the UTM preset re-composes the destination of every link in the campaign that hasn't overridden the key being changed. Pass archived: false to restore an archived campaign.
Body parameters
| Name | Type | Required | Description |
|---|---|---|---|
| name | String | Display name | |
| slug | String | Lowercase slug, unique within your organisation | |
| color | String | One of orange, blue, emerald, violet, rose | |
| icon | String | Phosphor icon name (fill weight) | |
| destination_url | String | Shared destination for the campaign's channel links | |
| title | String | Open Graph title | |
| description | String | Open Graph description | |
| goal_clicks | Integer | Click target for the campaign report | |
| starts_at | String | ISO 8601 start timestamp | |
| ends_at | String | ISO 8601 end timestamp | |
| utm_source | String | utm_source every link in the campaign inherits | |
| utm_medium | String | utm_medium every link in the campaign inherits | |
| utm_campaign | String | Overrides the slug as the inherited utm_campaign | |
| utm_term | String | utm_term every link in the campaign inherits | |
| utm_content | String | utm_content every link in the campaign inherits | |
| custom_utms | Hash | Arbitrary UTM defaults keyed by bare suffix | |
| archived | boolean | false restores an archived campaign; true archives it | |
Responses
| 200 | Updated |
| 401 | Missing or invalid token |
| 404 | Unknown campaign, or it belongs to another organisation |
| 422 | Invalid params |
Archive a campaign
DELETE
/v1/campaigns/:id
Archiving, not deletion: the campaign's links stay attached and its click history stays readable, so the report a finished campaign produced doesn't evaporate. An archived campaign stops counting against your plan's active-campaign cap, and drops out of GET /v1/campaigns unless you pass ?archived=true. Restore it with PATCH /v1/campaigns/:id and archived: false. Returns the archived campaign, and is idempotent.
Responses
| 200 | Archived |
| 401 | Missing or invalid token |
| 404 | Unknown campaign, or it belongs to another organisation |
Sample event payloads
GET
/v1/events/samples
Returns up to 3 recent events from your account for ?event= (link.clicked, link.created or link.expired), shaped exactly like the corresponding webhook deliveries — canned sample data when your account has no matching activity yet. Built for integrations that need example data before any real event has fired (Zapier's sample step reads this endpoint).
Responses
| 200 | Success |
| 401 | Missing or invalid token |
| 422 | Missing or unknown event type |
List your webhooks
GET
/v1/webhooks
Returns the authenticated organisation's webhooks, newest first. The signing secret is never included here — it is shown only once, in the response to the create call.
Responses
| 200 | Success |
| 401 | Missing or invalid token |
Create a webhook
POST
/v1/webhooks
Registers an endpoint that receives a signed POST on each matching event. link.clicked fires on every recorded visit with the click's geo / device / referer / UTM payload; link.created fires when a link is created, though not for links created by a CSV import — a bulk import would fan out one event per row and overrun the per-webhook delivery rate limit, so imports are skipped entirely rather than delivered in part; link.expired fires once when a link passes its expires_at or max_clicks threshold. The response includes the signing secret — store it now, it is never returned again. Verify deliveries with the X-Slsh-Signature header (HMAC-SHA256 of the raw body, keyed on the secret).
Body parameters
| Name | Type | Required | Description |
|---|---|---|---|
| url | String | required | HTTPS endpoint that will receive the POST |
| event_types | Array | Events to subscribe to. Defaults to ["link.clicked"]. Supported: link.clicked, link.created, link.expired. | |
Responses
| 201 | Created |
| 401 | Missing or invalid token |
| 422 | Invalid params (non-https url, unknown event type, or per-org cap reached) |
Fetch a webhook
GET
/v1/webhooks/:id
Returns a single webhook by id. The signing secret is never included.
Responses
| 200 | Success |
| 401 | Missing or invalid token |
| 404 | Unknown webhook, or it belongs to another organisation |
Delete a webhook
DELETE
/v1/webhooks/:id
Permanently removes the webhook. No further deliveries are attempted.
Responses
| 204 | Deleted |
| 401 | Missing or invalid token |
| 404 | Unknown webhook, or it belongs to another organisation |
List your short links
GET
/v1/links
Returns the authenticated user's default-domain links, newest first. Paginated with ?page=N (1-indexed, 100/page). The response carries X-Total-Count and a standard Link header (RFC 5988) with next, prev, first, last URLs. Links on your custom domains aren't returned here — those are managed via the web app and live in a separate per-domain namespace. Pass ?campaign_id=<uuid> to return only the links filed under one campaign.
Responses
| 200 | Success |
| 401 | Missing or invalid token |
| 422 | Unknown campaign_id |
Create a short link
POST
/v1/links
Creates a short link from a long URL. Slug is auto-generated if omitted.
Body parameters
| Name | Type | Required | Description |
|---|---|---|---|
| url | String | required | The destination URL (http or https) |
| slug | String | Custom slug; must be globally unique on slsh.me. (Links on your custom domains have their own per-domain namespace, but those are managed via the web app — this endpoint always creates a default-domain link.) | |
| password | String | Password-protect the redirect | |
| expires_at | String | ISO 8601 expiry timestamp | |
| max_clicks | Integer | Auto-expire after N clicks | |
| og_title | String | Open Graph title | |
| og_description | String | Open Graph description | |
| campaign_id | String | Id of a campaign of yours to file this link under. The campaign's UTM preset is composed into the destination URL, with any utm_* you pass here winning per key. Archived campaigns can't take new links. | |
| mask | boolean | Serve the destination inside a masked iframe (the short URL stays in the address bar). Pro plans only. | |
| notify_on_mask_break | boolean | Email the author if a masked destination later starts blocking framing. | |
Responses
| 201 | Created |
| 401 | Missing or invalid token |
| 422 | Invalid params, unknown campaign_id, or masking requested without a Pro plan |
Fetch a link by slug
GET
/v1/links/:slug
Responses
| 200 | Success |
| 401 | Missing or invalid token |
| 403 | Slug belongs to another user |
| 404 | Unknown slug |
Update a link
PATCH
/v1/links/:slug
Updates the destination URL or metadata. The slug is immutable; any slug in the body is ignored.
Body parameters
| Name | Type | Required | Description |
|---|---|---|---|
| url | String | Replace the destination URL | |
| password | String | Set or change the password | |
| expires_at | String | ISO 8601 expiry timestamp | |
| max_clicks | Integer | Auto-expire after N clicks | |
| og_title | String | Open Graph title | |
| og_description | String | Open Graph description | |
| campaign_id | String | Move the link into one of your campaigns; `null` detaches it. Omit the key to leave it alone. Archived campaigns can't take new links. | |
| mask | boolean | Serve the destination inside a masked iframe (the short URL stays in the address bar). Pro plans only. | |
| notify_on_mask_break | boolean | Email the author if a masked destination later starts blocking framing. | |
Responses
| 200 | Updated |
| 401 | Missing or invalid token |
| 403 | Slug belongs to another user |
| 404 | Unknown slug |
| 422 | Invalid params, unknown campaign_id, or masking requested without a Pro plan |
Delete a link
DELETE
/v1/links/:slug
Permanently deletes the link and cascades to its click history.
Responses
| 204 | Deleted |
| 401 | Missing or invalid token |
| 403 | Slug belongs to another user |
| 404 | Unknown slug |
Who am I
GET
/v1/me
Returns the user and organisation this token is scoped to. The cheapest way to verify a token — integrations (Zapier among them) test credentials against this endpoint.
Responses
| 200 | Success |
| 401 | Missing or invalid token |
List clicks for a link
GET
/v1/links/:link_slug/clicks
Returns the click stream (visits) for a default-domain link you own, newest first. Paginated with ?page=N (1-indexed, 100/page). The response carries X-Total-Count and a standard Link header (RFC 5988) with next, prev, first, last URLs.
Responses
| 200 | Success |
| 401 | Missing or invalid token |
| 403 | Link belongs to another user |
| 404 | Unknown link slug |
Look up a link's stats
GET
/v1/links/:link_slug/stats
Aggregate click stats for a default-domain link you own: lifetime totals, human/bot split, last click, expiry state, and the top countries, referrers and devices (top 5 each, all clicks included).
Responses
| 200 | Success |
| 401 | Missing or invalid token |
| 403 | Link belongs to another user |
| 404 | Unknown link slug |