API

POST /api/v1/links stores one address and returns https://isha.re/ plus a short code. Without a key, the call counts on the IP address. With a key, it counts on the account. Opening that short link sends the visitor to the original address. Shortening on the isha.re page is separate: it has no monthly limit and no price.

A key is optional. Send it as Authorization: Bearer or X-Api-Key. The call then counts on that account. The bill is in the API dashboard. Without a key, the call counts on the IP address. An unknown or revoked key is 401, and the message is That API key is not valid.

Pricing

The price applies only to new links this API stores. The page never charges, and it never adds to the count below.

Included

100

new API links each month, at no charge

After that

€1.45

per 1,000 further API links

In Stripe this payment is the product iShare API. There is no subscription. Pay the open amount for the month in the API dashboard. Stripe charges from €0.50.

Pay with Stripe

One caller is one IP address: the first value in X-Forwarded-For, otherwise X-Real-IP. iShare stores a hash of that address, not the address itself. Machines behind the same public IP share one allowance. The month is a calendar month in UTC. It starts again at 00:00 UTC on the first, and the count does not carry over.

A request counts as one link only when all of these are true:

  • The call is POST /api/v1/links and it returns 200.
  • The normalized address was not already stored. A repeat returns the existing code and leaves the count unchanged.
  • The address was not shortened on the website. Website links are stored, but they are not API usage.

Failed calls do not count: a 400 or a 500 leaves the month where it was. Each link past the included 100 is €0.00145. The charge for the month is that figure times the extra links, rounded to the nearest cent. The first three links past 100 still show 0.00. The fourth shows 0.01. 100 links are €0.00. 700 links are the included 100 plus 600, which is €0.87. 1,100 links are €1.45. 2,100 links are €2.90.

Every successful response carries the month so far. In JSON that object is usage. The same numbers are response headers, including on a text response.

FieldHeaderMeaning
includedX-Quota-IncludedAlways 100. The free links in this UTC month.
usedX-Quota-UsedLinks this caller has counted so far this month, including this call when it counted.
extramax(0, used − 100). Links that the rate applies to. Not sent as a header.
currencyX-Quota-CurrencyAlways EUR.
amountX-Quota-AmountEuros for extra, as a string with two decimals, such as 0.00 or 1.45.

Integration

Send the address you have. Do not add or strip a scheme, a trailing slash, or a fragment yourself. iShare normalizes the address before it stores it, and the stored form is what later calls match against. Read link and, on a first store, statsKey. The key is not part of the public URL. Keep it if you want the open stats later. A second call for the same address does not send it again.

JavaScript

const response = await fetch("https://isha.re/api/v1/links", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Accept: "application/json",
  },
  body: JSON.stringify({ url: "https://example.com/page" }),
});

const body = await response.json();
if (!response.ok) throw new Error(body.error);

const link = body.link;
const statsKey = body.statsKey ?? null;
const amount = body.usage.amount;

Python

import json
import urllib.request

body = json.dumps({"url": "https://example.com/page"}).encode()
request = urllib.request.Request(
    "https://isha.re/api/v1/links",
    data=body,
    headers={
        "Content-Type": "application/json",
        "Accept": "application/json",
    },
)
with urllib.request.urlopen(request) as response:
    payload = json.load(response)

link = payload["link"]
stats_key = payload.get("statsKey")
amount = payload["usage"]["amount"]

curl

curl -sS -D - -X POST https://isha.re/api/v1/links \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{"url":"https://example.com/page"}'

A first store of that address prints a body like this. The code will differ.

{"link":"https://isha.re/tk8c","statsKey":"q8mR2n…","usage":{"included":100,"used":1,"extra":0,"currency":"EUR","amount":"0.00"}}

Browsers on other sites can call the API directly. Responses include Access-Control-Allow-Origin: *, Access-Control-Allow-Methods: POST, OPTIONS, and Access-Control-Allow-Headers: Content-Type, Accept. A preflight OPTIONS request returns 204 and may be cached for 86400 seconds.

Reference

Create a link

POST https://isha.re/api/v1/links

The body is the address to shorten. JSON is the usual form. A form field, a multipart field, or a plain-text body uses the same address rules and returns the same link.

Content-TypeWhere the address is
application/jsonString property url. A body that starts with { is read as JSON even when the header is missing. Invalid JSON, or a url that is missing or not a string, is an empty address.
application/x-www-form-urlencodedField url.
multipart/form-dataField url.
text/plainThe whole body, trimmed. Any other type that is not JSON or a form is read this way.
POST /api/v1/links
Content-Type: application/json

{"url":"https://example.com/page"}
POST /api/v1/links
Content-Type: application/x-www-form-urlencoded

url=https://example.com/page
POST /api/v1/links
Content-Type: text/plain

https://example.com/page

What gets stored

The address is rewritten before it is saved or looked up. Two writings of the same address become one link.

  • A missing scheme becomes https. example.com/docs is stored as https://example.com/docs.
  • Only http and https are stored. Any other scheme is rejected.
  • A username or a password in the URL is rejected. https://user:pass@example.com is a 400.
  • The host must contain a dot, or be localhost. https://intranet is rejected. The host is stored in lowercase.
  • A fragment (#section) is removed. https://example.com/docs#install is stored as https://example.com/docs.
  • A trailing slash is removed, except when the path is only /. Query strings stay, and their order stays.
  • The address, before this rewrite, can be at most 4096 characters. A longer body is a 400.
  • isha.re itself, and other shorteners such as bit.ly or tinyurl.com, are rejected. The message is isha.re links and links from other URL shorteners can't be shortened.

Because lookup uses the stored form, HTTPS://Example.com/page/ and https://example.com/page are the same address. The second call returns the first code.

A new code is four characters from abcdefghijkmnpqrstuvwxyz23456789. After six collisions the next try is six characters. The host in link is the host you called when that host is isha.re, www.isha.re, or localhost. Any other host is returned as https://isha.re/ and the code.

Links created through this API do not expire. If the address was first shortened on the website and that link has an end date, this call returns that existing link and the end date stays. A later call does not clear it or extend it. Once that link has ended and been removed, shortening the address again creates a new code.

Response

Success is 200. JSON is the default. Ask for the bare link with Accept: text/plain or ?format=text. Force JSON with ?format=json. When both text/plain and application/json appear in Accept, the one written first wins, unless format is set.

HTTP/1.1 200 OK
Content-Type: application/json
X-Quota-Included: 100
X-Quota-Used: 1
X-Quota-Amount: 0.00
X-Quota-Currency: EUR

{
  "link": "https://isha.re/tk8c",
  "statsKey": "q8mR2nY7pL4sT1vW0xZaBcDeFgHiJkLm",
  "usage": {
    "included": 100,
    "used": 1,
    "extra": 0,
    "currency": "EUR",
    "amount": "0.00"
  }
}
FieldWhen it is present
linkAlways. The short URL. Show this, store this, or redirect to this.
statsKeyOnly on the call that first attaches a key to this address. A later call for an address that already has a key omits the field. There is no way to fetch it again.
usageAlways on JSON. The month for this caller, described under Pricing.

A text response is the short URL and a newline, with the quota headers above. When a key was just created, it is also in X-Stats-Key. The text body does not include usage or the key.

curl -sS -D - -X POST 'https://isha.re/api/v1/links?format=text' \
  -H 'Content-Type: text/plain' \
  --data 'https://example.com/page'

https://isha.re/tk8c

Errors

Errors use the format you asked for. JSON requests get {"error":"…"}. Text requests get the message as plain text and a newline. Error responses do not include quota headers. The messages in this table are the English text the API returns.

StatusWhenMessage
400The body was empty, the JSON could not be read, the value was not a URL, or it broke an address rule above. A blocked shortener uses the next row.Enter a valid URL, like https://example.com
400The address is isha.re or another URL shortener.isha.re links and links from other URL shorteners can't be shortened.
401The key was sent and it is unknown or revoked.That API key is not valid.
500The shortener could not store the link.The shortener is temporarily unavailable.

Open a short link

GET /{code} and HEAD /{code} answer with 302 and a Location header set to the stored address. The code is the last path segment of link. It is four to sixteen letters or digits. Matching is case-insensitive.

  • GET records the open: source, country, device, browser, and system. The same visitor, from the same IP and user agent, is counted once every 30 seconds. Prefetch, prerender, and known bots are not counted. HEAD redirects and does not record.
  • An unknown code stays on that address and says the short link is no longer available. A database failure says the link cannot be opened right now.
  • A code that is not four to sixteen letters or digits, or a reserved path such as api or support, returns 404. It does not redirect.

Stats

GET /api/links/{code}/stats returns opens for the link you created. Send the statsKey from the create call in the X-Stats-Key header. The short URL alone is not enough. A missing or wrong key is 403, and the body is {"error":"These stats stay in the browser that created the link."}.

curl -sS 'https://isha.re/api/links/tk8c/stats?range=7d&tz=Europe/Berlin' \
  -H 'X-Stats-Key: q8mR2nY7pL4sT1vW0xZaBcDeFgHiJkLm'
QueryValues
range24h, 7d, 30d, or all. Anything else, including a missing value, is 7d. all starts at the first recorded open.
tzAn IANA time zone, such as Europe/Berlin. A missing or unknown zone is UTC. Buckets are truncated in that zone.

A 200 body looks like this. Counts are for the requested range.

{
  "range": "7d",
  "unit": "day",
  "url": "https://example.com/page",
  "expiresAt": null,
  "opens": 12,
  "visitors": 9,
  "previousOpens": 4,
  "previousVisitors": 3,
  "active": 1,
  "fresh": 7,
  "returning": 2,
  "series": [{ "t": "2026-11-06T00:00:00", "opens": 3, "visitors": 2 }],
  "previousSeries": [{ "t": "2026-10-30T00:00:00", "opens": 1, "visitors": 1 }],
  "sources": [{ "name": "Direct", "kind": "direct", "opens": 12 }],
  "kinds": [{ "kind": "direct", "opens": 12 }],
  "countries": [{ "code": "DE", "opens": 12 }],
  "devices": [{ "name": "desktop", "opens": 12 }],
  "browsers": [{ "name": "Chrome", "opens": 12 }],
  "systems": [{ "name": "macOS", "opens": 12 }]
}
FieldMeaning
unithour for 24h, day for 7d and 30d. For all, hour under two days, day under 120 days, otherwise week.
opensRecorded opens in the range. One person can open the link more than once, 30 seconds apart.
visitorsDistinct visitors in the range. A visitor is a hash of the link, the IP, and the user agent.
previousOpensOpens in the window of the same length immediately before this range. null when range is all. previousVisitors is the same window.
activeDistinct visitors in the last 5 minutes. This ignores range.
freshVisitors whose first open of this link falls inside the range. returning is everyone else who opened it in the range. For all, fresh is visitors with exactly one open, and returning is visitors with more than one.
seriesOne point per bucket, including buckets with zero opens. t is the bucket start as YYYY-MM-DDTHH:mm:ss in the requested zone, with no offset. previousSeries is the previous window, aligned to the same number of points, or null when range is all.
sourcesUp to eight referrers. kind is direct, search, social, email, referral, campaign, or ad. utm_source on the short link overrides the referrer. kinds is the same opens grouped by kind.
countriesISO country codes, most opens first. An empty code means the country was unknown.
devicesmobile, tablet, desktop, or unknown.
browsersUp to six browser names. systems is up to six operating systems, the same way.
urlThe stored address. expiresAt is an ISO timestamp, or null when the link does not expire.

Stats errors are always JSON.

  • 403 — the header is missing or does not match. Message: These stats stay in the browser that created the link.
  • 404 — the code is the wrong shape or no such link is stored. Message: That short link does not exist.
  • 500 — stats could not be read. Message: Stats are temporarily unavailable.