Pagewing API

Create short links, change where they go, set device and country redirects and read click stats from your own code, a spreadsheet script or an automation tool. The API is part of Pagewing Pro and works on the same links as the side panel and your links page.

Keys

Make a key under API keys on your account page and send it with every request as a bearer token. The key is shown once; keep it secret, since anyone with it can change your links. Give each app its own key so you can delete one without breaking the others. A key stops working while your account isn't on Pro, and you can have up to 10.

curl https://pagewing.appzlr.com/api/v1/account \
  -H "Authorization: Bearer $PAGEWING_KEY"
{
  "id": "Xy12…",
  "email": "you@example.com",
  "plan": "pro"
}

Every address below starts with https://pagewing.appzlr.com/api/v1. Requests and answers are JSON.

Limits

Errors

A failed request answers with an HTTP error status and a JSON body: a message you can show and a code you can check.

{
  "error": "That alias is taken. Try another.",
  "code": "CONFLICT"
}
StatusCodeWhen
400INVALIDSomething in the request isn't right: a bad address, alias, date or rule. The message says what.
401UNAUTHORIZEDNo key, a mistyped key, or a deleted one.
402PRO_REQUIREDThe account isn't on Pro (every request), or the change needs a feature of a plan it doesn't have. The answer names the feature and the plan.
404NOT_FOUNDThere's no link with that code on your account.
409CONFLICTThat alias is taken.
429LIMITOver 60 requests a minute (wait for the Retry-After seconds), or the day's 200 new links.
FieldWhat it is
codeThe part after the slash: an alias you chose, or seven random letters and digits.
urlWhere the link goes.
shortUrlThe short link to share.
clicksEvery click since it was made.
createdAt, lastClickedAtISO 8601 times. lastClickedAt is missing until the first click.
titleA name for the link, optional.
tagsLowercase labels, up to 10.
disabledTurned off: the short link says it isn't available instead of redirecting.
expiresAtWhen it stops redirecting, if it has an expiry.
blockedTurned off by Pagewing after an abuse report. Only present when true.
rulesDevice and country redirects, in the order they're checked. Missing when there are none (see Redirect rules).
{
  "code": "spring-sale",
  "url": "https://example.com/sale",
  "shortUrl": "https://pagewing.appzlr.com/spring-sale",
  "clicks": 42,
  "createdAt": "2026-10-29T09:30:00.000Z",
  "title": "Spring sale",
  "lastClickedAt": "2026-10-29T15:02:11.000Z",
  "tags": [
    "campaign"
  ],
  "disabled": false,
  "expiresAt": "2026-12-31T23:00:00.000Z",
  "rules": [
    {
      "id": "k3m9qa",
      "kind": "device",
      "values": [
        "ios"
      ],
      "url": "https://apps.apple.com/app/id123"
    },
    {
      "id": "p7d2xw",
      "kind": "country",
      "values": [
        "DE",
        "AT"
      ],
      "url": "https://example.de/sale"
    }
  ]
}

Create a link

POST /links. Only url is required; the answer is the new link, with status 201.

curl -X POST https://pagewing.appzlr.com/api/v1/links \
  -H "Authorization: Bearer $PAGEWING_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/sale","alias":"spring-sale","title":"Spring sale","tags":["campaign"],"expiresAt":"2026-12-31T23:00:00Z"}'

List links

GET /links, newest first, 100 at most per page. Every parameter is optional:

curl https://pagewing.appzlr.com/api/v1/links?tag=campaign&sort=clicks&limit=20 \
  -H "Authorization: Bearer $PAGEWING_KEY"
{
  "links": [
    "…link objects…"
  ],
  "total": 240,
  "matching": 12,
  "cursor": null,
  "tags": [
    {
      "tag": "campaign",
      "count": 12
    }
  ]
}

Get a link

GET /links/:code answers with the link object.

curl https://pagewing.appzlr.com/api/v1/links/spring-sale \
  -H "Authorization: Bearer $PAGEWING_KEY"

Update a link

PATCH /links/:code with the fields to change: url, title (null clears it), tags(replaces them), disabled, expiresAt (null removes it) and rules (replaces them; [] removes them). The code, clicks and creation date stay. The answer is the link as it is now.

curl -X PATCH https://pagewing.appzlr.com/api/v1/links/spring-sale \
  -H "Authorization: Bearer $PAGEWING_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/sale-extended","expiresAt":null}'

Delete a link

DELETE /links/:code answers 204. The short link stops working right away and its stats are deleted; this can't be undone.

curl -X DELETE https://pagewing.appzlr.com/api/v1/links/spring-sale \
  -H "Authorization: Bearer $PAGEWING_KEY"

Bulk create

POST /links/batch makes up to 100 links in one request. Each one that fails (a bad address, a taken alias, the day's limit) gets an error of its own, and the rest are still made. tags go on every link; skipExisting: true answers with the link an address already has instead of making a second one.

curl -X POST https://pagewing.appzlr.com/api/v1/links/batch \
  -H "Authorization: Bearer $PAGEWING_KEY" \
  -H "Content-Type: application/json" \
  -d '{"links":[{"url":"https://example.com/a"},{"url":"https://example.com/b","alias":"launch-b","title":"B"}],"tags":["launch"],"skipExisting":true}'
{
  "results": [
    {
      "url": "https://example.com/a",
      "link": "…link object…"
    },
    {
      "url": "https://example.com/b",
      "error": "That alias is taken. Try another.",
      "errorCode": "CONFLICT"
    }
  ],
  "created": 1
}

Click stats

GET /links/:code/stats?from=YYYY-MM-DD&to=YYYY-MM-DD (UTC days, both included; the last 30 days when left out) answers with clicks for every day in the range and the top referrers, countries (two-letter codes, "" for unknown), devices and destinations. No IP address or full referrer is ever kept.

curl https://pagewing.appzlr.com/api/v1/links/spring-sale/stats?from=2026-10-01&to=2026-10-29 \
  -H "Authorization: Bearer $PAGEWING_KEY"
{
  "code": "spring-sale",
  "from": "2026-10-01",
  "to": "2026-10-29",
  "days": [
    {
      "day": "2026-10-01",
      "clicks": 3
    },
    "…"
  ],
  "clicks": 42,
  "allTime": 42,
  "untracked": 0,
  "referrers": [
    {
      "key": "news.ycombinator.com",
      "clicks": 20
    },
    {
      "key": "",
      "clicks": 9
    }
  ],
  "countries": [
    {
      "key": "US",
      "clicks": 25
    },
    {
      "key": "DE",
      "clicks": 11
    }
  ],
  "devices": [
    {
      "key": "desktop",
      "clicks": 30
    },
    {
      "key": "mobile",
      "clicks": 12
    }
  ],
  "destinations": [
    {
      "key": "",
      "clicks": 26
    },
    {
      "key": "k3m9qa",
      "clicks": 9
    },
    {
      "key": "p7d2xw",
      "clicks": 7
    }
  ],
  "limited": false
}

destinations counts where clicks were sent: "" is the link's own address, anything else a rule's id. Device classes are desktop, mobile, tablet, bot (link previews and crawlers) and other.

Redirect rules

A rule sends some visitors to another address, by their device or their country. Rules are checked in order and the first one that matches wins; everyone else goes to the link's url, and link previews always do. Each rule has a kind, itsvalues and a url. Send rules without an id and each gets one; send the id back when you update, so the rule keeps its stats.

curl -X PATCH https://pagewing.appzlr.com/api/v1/links/spring-sale \
  -H "Authorization: Bearer $PAGEWING_KEY" \
  -H "Content-Type: application/json" \
  -d '{"rules":[{"kind":"device","values":["ios"],"url":"https://apps.apple.com/app/id123"},{"kind":"device","values":["android"],"url":"https://play.google.com/store/apps/details?id=com.example"},{"kind":"country","values":["DE","AT"],"url":"https://example.de/sale"}]}'

An iPad that asks for desktop websites (Safari's default) looks like a Mac, so it counts as a computer. Questions or something missing? Tell us.