jst.pw
Continue with Google

jst.pw API

Create short links, change where they go and read their clicks from your own code. JSON over HTTPS, one key per app.

Getting started

The base URL for every request is:

https://jst.pw/api/v1
  • Get a key. Sign in, open Dashboard → API and create a key. It is shown once, so copy it somewhere safe. Keys start with jst_live_.
  • Send JSON. Request bodies are JSON (Content-Type: application/json); form encoding also works. Every response is JSON, errors included.
  • Everything is yours. A key acts as the account that made it and only ever sees that account's links.
curl https://jst.pw/api/v1/me \
  -H "Authorization: Bearer jst_live_your_key_here"

Authentication

Send the key as a Bearer token. The X-Api-Key header works too.

Authorization: Bearer jst_live_your_key_here
X-Api-Key: jst_live_your_key_here

Abilities

Key typeCan
Read onlyEvery GET endpoint: your account, your links, their stats and clicks.
Read & writeEverything above, plus creating, updating and deleting links.

A key can expire (after 30, 90 or 365 days, or never) and can be revoked from the dashboard at any time; either way it stops working on the next request. Your plan sets how many keys can be active at once: no limit, no limit.

Keep keys on your server. Anyone holding one can act as your account.

Errors

Every error has the same shape, whatever the status:

{
  "error": {
    "code": "validation_failed",
    "message": "The given data was invalid.",
    "details": {
      "errors": { "url": ["This domain is not allowed."] }
    }
  }
}
StatusCodeWhen
401unauthenticatedNo key, or a key that is unknown, revoked or expired. details.reason says which: missing_key, invalid_key, revoked_key, expired_key.
403insufficient_abilityA read-only key called an endpoint that writes.
403api_access_disabledAPI access was switched off for this account. The key itself is fine.
403account_suspendedThe account has been suspended.
403link_disabled_by_adminYou tried to re-activate a link that jst.pw disabled.
404not_foundNo such endpoint, or no link with that slug on your account. A slug that belongs to someone else is also 404.
405method_not_allowedThe endpoint exists but not with this HTTP method.
422validation_failedA field is missing or wrong. details.errors lists messages per field.
429rate_limitedToo many requests this minute. Wait Retry-After seconds.
500server_errorSomething broke on our side. Safe to retry a GET.
503maintenancejst.pw is down for maintenance. Retry-After is sent when we know when it will be back.

Rate limits

Limits are per account, across all of its keys, counted per minute:

  • Free: 360 requests a minute.
  • Business: 600 requests a minute.
  • Requests without a valid key: 10 a minute per IP address.

Limited responses carry these headers. Past the limit you get 429 with Retry-After (seconds) and details.retry_after.

X-RateLimit-Limit: 360
X-RateLimit-Remaining: 359

GET /me tells you your current limit.

Pagination

Lists take page (from 1) and per_page (default 25, at most 100), newest first, and answer:

{
  "data": [ ... ],
  "meta": { "current_page": 1, "per_page": 25, "total": 132, "last_page": 6, "from": 1, "to": 25 },
  "links": { "next": "https://jst.pw/api/v1/links?page=2", "prev": null }
}

Follow links.next until it is null.

Endpoints

MethodPathKey
GET/meread
GET/linksread
POST/linkswrite
GET/links/{slug}read
PATCH/links/{slug}write
DELETE/links/{slug}write
GET/links/{slug}/statsread
GET/links/{slug}/clicksread

The link object

{
  "slug": "aB3xK",
  "short_url": "https://jst.pw/aB3xK",
  "name": "Spring launch",
  "destination": "https://example.com/spring",
  "direct": true,
  "password_protected": false,
  "active": true,
  "status": "active",
  "clicks": 1284,
  "last_clicked_at": "2026-09-17T10:42:03+00:00",
  "expires_at": null,
  "created_at": "2026-09-01T08:00:00+00:00",
  "updated_at": "2026-09-10T14:21:55+00:00",
  "qr": {
    "png": "https://jst.pw/aB3xK/qr.png",
    "svg": "https://jst.pw/aB3xK/qr.svg"
  }
}

direct is true when visitors go straight to the destination with a 301, false when they see the waiting page first. status is active, disabled or expired. A password is never returned, only whether one is set.

GET/me

Your account, plan, limits and what is left this month.

curl https://jst.pw/api/v1/me -H "Authorization: Bearer $JST_KEY"
{
  "data": {
    "account": { "id": 42, "name": "Mona", "email": "mona@example.com" },
    "plan": "free",
    "plan_expires_at": null,
    "limits": { "direct_monthly": null, "rules_per_link": 1, "dynamic_qr": 1, "pages": 1, "api_rate_per_minute": 360, "api_keys": null },
    "usage": {
      "direct_monthly": { "used": 12, "limit": null, "remaining": null, "resets_at": "2026-10-01T00:00:00+00:00" },
      "api_keys": { "used": 1, "limit": null }
    },
    "rate_limit": { "per_minute": 360 },
    "key": { "name": "Newsletter script", "prefix": "jst_live_aB3", "abilities": ["read", "write"], "expires_at": null }
  }
}

Your links, newest first, Pagination.

QueryDescription
qSearch the slug, name and destination.
activetrue or false.
page, per_pageSee Pagination.
curl "https://jst.pw/api/v1/links?q=spring&per_page=50" -H "Authorization: Bearer $JST_KEY"

Shorten a URL. Answers 201 with the link object and meta.direct_quota_reached.

FieldTypeDescription
urlstring, requiredThe destination, up to 2048 characters. example.com is read as https://example.com. Checked like the website: http or https only, no blocked domains, and the domain must exist.
namestringA label for you, up to 120 characters.
passwordstring3 to 72 characters. Visitors must enter it before they are forwarded.
directbooleanDefault true: an instant 301 while your monthly allowance lasts. false shows the waiting page. See Instant redirects.
curl -X POST https://jst.pw/api/v1/links \
  -H "Authorization: Bearer $JST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/spring", "name": "Spring launch"}'
HTTP/1.1 201 Created

{
  "data": { "slug": "aB3xK", "short_url": "https://jst.pw/aB3xK", "destination": "https://example.com/spring", "direct": true, ... },
  "meta": { "direct_quota_reached": false }
}

One link, as {"data": {link}}.

curl https://jst.pw/api/v1/links/aB3xK -H "Authorization: Bearer $JST_KEY"

Change any of these; leave out what should stay. Every change applies from the very next visit. Answers the link object and meta.direct_quota_reached.

FieldTypeDescription
urlstringA new destination, checked like on create.
namestring or nullnull clears it.
passwordstring or nullSets a new password; null removes protection.
directbooleanSwitch between the instant 301 and the waiting page. Turning it on spends an allowance slot unless this link already spent one this month.
activebooleanfalse pauses the link (visitors get 404). A link disabled by jst.pw cannot be re-activated here.
curl -X PATCH https://jst.pw/api/v1/links/aB3xK \
  -H "Authorization: Bearer $JST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/summer", "password": null}'

Deletes the link. Answers 204 No Content; the short link stops working immediately and its address is not given to anyone else.

curl -X DELETE https://jst.pw/api/v1/links/aB3xK -H "Authorization: Bearer $JST_KEY"

The same analytics as the dashboard, for the last days days (7, 30 or 90; default 30). Days are counted in UTC; by_hour and by_weekday are in the timezone given.

curl "https://jst.pw/api/v1/links/aB3xK/stats?days=7" -H "Authorization: Bearer $JST_KEY"
{
  "data": {
    "slug": "aB3xK",
    "days": 7,
    "since": "2026-09-11T00:00:00+00:00",
    "timezone": "Africa/Cairo",
    "totals": { "clicks": 311, "uniques": 204, "all_time_clicks": 1284 },
    "series": [ { "date": "2026-09-11", "clicks": 40, "uniques": 31 }, ... ],
    "breakdowns": {
      "countries": [ { "value": "EG", "clicks": 190, "share": 61 }, ... ],
      "cities": [ ... ], "referrers": [ ... ], "devices": [ ... ], "browsers": [ ... ], "os": [ ... ]
    },
    "by_hour": [ 3, 1, 0, ... 24 numbers ],
    "by_weekday": { "Sat": 52, "Sun": 48, "Mon": 44, "Tue": 40, "Wed": 41, "Thu": 46, "Fri": 40 }
  }
}

Individual clicks, newest first, Pagination. No IP address is stored per click, and the daily visitor hash used to count unique visitors is never returned.

curl "https://jst.pw/api/v1/links/aB3xK/clicks?per_page=100" -H "Authorization: Bearer $JST_KEY"
{
  "data": [
    { "created_at": "2026-09-17T10:42:03+00:00", "country": "EG", "city": "Cairo", "referrer": "t.co", "device": "mobile", "os": "Android", "browser": "Chrome", "routing_rule_id": null }
  ],
  "meta": { ... },
  "links": { "next": "...", "prev": null }
}

Instant redirects and the waiting page

A jst.pw link either forwards visitors straight away with a 301 (direct: true) or shows the waiting page first. Instant redirects are a monthly allowance: no limit Free on Free, no limit Business on Business.

  • The allowance is shared: links made on the website and through the API spend the same slots.
  • When it runs out, POST /links still creates the link and answers 201, with "direct": false and "meta": {"direct_quota_reached": true}. The link works; it opens the waiting page.
  • Deleting a link or switching it back to the waiting page does not give the slot back. The allowance starts again on the first of the month (UTC).

Not in v1

These are dashboard-only for now:

  • Routing rules (sending visitors to different places by country, device and so on).
  • Managed QR codes. Every link's QR code image is available at the qr URLs on the link object.
  • Link-in-bio pages.

If the API doesn't cover what you need yet, contact us.