Included
100
new API links each month, at no charge
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.
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.
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:
POST /api/v1/links and it returns 200.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.
| Field | Header | Meaning |
|---|---|---|
| included | X-Quota-Included | Always 100. The free links in this UTC month. |
| used | X-Quota-Used | Links this caller has counted so far this month, including this call when it counted. |
| extra | max(0, used − 100). Links that the rate applies to. Not sent as a header. | |
| currency | X-Quota-Currency | Always EUR. |
| amount | X-Quota-Amount | Euros for extra, as a string with two decimals, such as 0.00 or 1.45. |
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.
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;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 -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.
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-Type | Where the address is |
|---|---|
| application/json | String 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-urlencoded | Field url. |
| multipart/form-data | Field url. |
| text/plain | The 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/pagePOST /api/v1/links
Content-Type: text/plain
https://example.com/pageThe address is rewritten before it is saved or looked up. Two writings of the same address become one link.
https. example.com/docs is stored as https://example.com/docs.http and https are stored. Any other scheme is rejected.https://user:pass@example.com is a 400.localhost. https://intranet is rejected. The host is stored in lowercase.#section) is removed. https://example.com/docs#install is stored as https://example.com/docs./. Query strings stay, and their order stays.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.
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"
}
}| Field | When it is present |
|---|---|
| link | Always. The short URL. Show this, store this, or redirect to this. |
| statsKey | Only 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. |
| usage | Always 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/tk8cErrors 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.
| Status | When | Message |
|---|---|---|
| 400 | The 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 |
| 400 | The address is isha.re or another URL shortener. | isha.re links and links from other URL shorteners can't be shortened. |
| 401 | The key was sent and it is unknown or revoked. | That API key is not valid. |
| 500 | The shortener could not store the link. | The shortener is temporarily unavailable. |
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.
api or support, returns 404. It does not redirect.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'| Query | Values |
|---|---|
| range | 24h, 7d, 30d, or all. Anything else, including a missing value, is 7d. all starts at the first recorded open. |
| tz | An 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 }]
}| Field | Meaning |
|---|---|
| unit | hour for 24h, day for 7d and 30d. For all, hour under two days, day under 120 days, otherwise week. |
| opens | Recorded opens in the range. One person can open the link more than once, 30 seconds apart. |
| visitors | Distinct visitors in the range. A visitor is a hash of the link, the IP, and the user agent. |
| previousOpens | Opens in the window of the same length immediately before this range. null when range is all. previousVisitors is the same window. |
| active | Distinct visitors in the last 5 minutes. This ignores range. |
| fresh | Visitors 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. |
| series | One 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. |
| sources | Up 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. |
| countries | ISO country codes, most opens first. An empty code means the country was unknown. |
| devices | mobile, tablet, desktop, or unknown. |
| browsers | Up to six browser names. systems is up to six operating systems, the same way. |
| url | The 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.