Getting started
Every request must carry a Sanctum Bearer token and set the Accept: application/json header. Create tokens in the app at Profile → API Tokens (the full token value is shown once, on creation).
Base URL
https://app.isref.cc/api/v1
Required headers
Authorization: Bearer <token> Accept: application/json Content-Type: application/json # POST / PUT / PATCH
A normal user only sees and manages their own links. An administrator token can act on any link.
Response envelope
Every response — success or error, including framework exceptions — uses the same shape.
Success
{
"success": true,
"data": { … },
"message": "Short link created."
}
Error
{
"success": false,
"data": null,
"message": "This action is unauthorized.",
"errors": {}
}
Status codes & errors
| HTTP | When |
|---|---|
200 | Read, or a successful update |
201 | A new link was created |
204 | A link was deleted (no body returned) |
401 | Missing token, or an invalid / expired one |
403 | The token lacks the required ability, or the link is not yours |
404 | Unknown route, or no link with that id |
422 | Validation failed (see the errors object) |
429 | Rate limit exceeded (60 req/min per token) |
Token abilities
Abilities are checked server-side; the client cannot assert its own permissions. Pick them when you create the token in the app.
| Endpoint | Required ability |
|---|---|
GET /shortlinks |
shorturl:read |
GET /shortlinks/{id} |
shorturl:read |
POST /shortlinks |
shorturl:create |
PUT · PATCH /shortlinks/{id} |
shorturl:update |
DELETE /shortlinks/{id} |
shorturl:delete |
Rate limiting
60 requests per minute per token (keyed with the user id). Exceeding it returns 429 with the X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After headers.
Endpoints
GET /shortlinks
Returns the user's links, paginated, newest first.
Query parameters
| Field | Description |
|---|---|
q | Search across code, title and destination_url |
status | Filter by status: active or disabled |
per_page | Items per page, 1–100 (default 15) |
page | Page number (1-based) |
Example request
curl "https://app.isref.cc/api/v1/shortlinks?status=active&per_page=20" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"
import requests
resp = requests.get(
"https://app.isref.cc/api/v1/shortlinks?status=active&per_page=20",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/json",
},
)
print(resp.status_code, resp.json())
<?php
$ch = curl_init("https://app.isref.cc/api/v1/shortlinks?status=active&per_page=20");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer {$token}",
"Accept: application/json",
],
]);
$response = curl_exec($ch);
printf("%d\n%s\n", curl_getinfo($ch, CURLINFO_HTTP_CODE), $response);
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://app.isref.cc/api/v1/shortlinks?status=active&per_page=20", nil)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(resp.StatusCode, string(out))
}
const resp = await fetch("https://app.isref.cc/api/v1/shortlinks?status=active&per_page=20", {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
});
console.log(resp.status, await resp.json());
Example response
{
"success": true,
"data": {
"items": [
{
"id": 12,
"code": "launch",
"short_url": "https://isref.cc/launch",
"destination_url": "https://example.com/page",
"title": null,
"description": null,
"status": "active",
"has_password": false,
"available_from": null,
"is_pending": false,
"expires_at": null,
"is_expired": false,
"click_count": 34,
"last_clicked_at": "2026-09-01T12:00:00+00:00",
"owner": { "id": 1, "name": "Ada" },
"created_at": "2026-08-01T09:00:00+00:00",
"updated_at": "2026-08-01T09:00:00+00:00"
}
],
"meta": { "current_page": 1, "per_page": 15, "total": 1, "last_page": 1 }
},
"message": null
}
POST /shortlinks
Create a new short link. When code is omitted the server generates one.
Request body (JSON)
| Field | Rules |
|---|---|
destination_url
required
|
Target URL · http / https only · host required · ≤ 2048 chars |
code
optional
|
Custom code · [A-Za-z0-9_-], 3–40 chars · must be unique and not a reserved word · omit to auto-generate |
title
optional
|
Link title · ≤ 255 |
description
optional
|
Description · ≤ 2000 |
status
optional
|
active or disabled (default active) — disabled links do not redirect |
available_from
optional
|
Activation date-time (ISO-8601) · before this the link returns 404 "not active yet" |
expires_at
optional
|
Expiry date-time (ISO-8601) · must be in the future and after available_from · after this the link returns 410 |
password
optional
|
Visitor password · 4–255 chars · stored hashed, never returned by the API |
Example request
curl -X POST "https://app.isref.cc/api/v1/shortlinks" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"destination_url":"https://example.com/linux-course","code":"linux-course"}'
import requests
resp = requests.post(
"https://app.isref.cc/api/v1/shortlinks",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/json",
},
json={
"destination_url": "https://example.com/linux-course",
"code": "linux-course",
},
)
print(resp.status_code, resp.json())
<?php
$ch = curl_init("https://app.isref.cc/api/v1/shortlinks");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer {$token}",
"Accept: application/json",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"destination_url" => "https://example.com/linux-course",
"code" => "linux-course",
]),
]);
$response = curl_exec($ch);
printf("%d\n%s\n", curl_getinfo($ch, CURLINFO_HTTP_CODE), $response);
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
payload := strings.NewReader(`{"destination_url":"https://example.com/linux-course","code":"linux-course"}`)
req, _ := http.NewRequest("POST", "https://app.isref.cc/api/v1/shortlinks", payload)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(resp.StatusCode, string(out))
}
const resp = await fetch("https://app.isref.cc/api/v1/shortlinks", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
destination_url: "https://example.com/linux-course",
code: "linux-course",
}),
});
console.log(resp.status, await resp.json());
Example response · 201
{
"success": true,
"data": {
"id": 41,
"code": "linux-course",
"short_url": "https://isref.cc/linux-course",
"destination_url": "https://example.com/linux-course",
"status": "active",
"has_password": false,
…
},
"message": "Short link created."
}
GET /shortlinks/{id}
Return a single link by id.
curl "https://app.isref.cc/api/v1/shortlinks/41" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"
import requests
resp = requests.get(
"https://app.isref.cc/api/v1/shortlinks/41",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/json",
},
)
print(resp.status_code, resp.json())
<?php
$ch = curl_init("https://app.isref.cc/api/v1/shortlinks/41");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer {$token}",
"Accept: application/json",
],
]);
$response = curl_exec($ch);
printf("%d\n%s\n", curl_getinfo($ch, CURLINFO_HTTP_CODE), $response);
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://app.isref.cc/api/v1/shortlinks/41", nil)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(resp.StatusCode, string(out))
}
const resp = await fetch("https://app.isref.cc/api/v1/shortlinks/41", {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
});
console.log(resp.status, await resp.json());
200 · The shape every endpoint returns under data (or data.items for the list).
PUT PATCH /shortlinks/{id}
Update a link. Send only the fields you want to change (PATCH) or all of them (PUT). Changing code re-checks uniqueness and reserved words.
Send a new password value to set / replace it · send remove_password: true to clear protection · send neither to keep the current one.
| Field | Rules |
|---|---|
destination_url | Target URL · http / https only · host required · ≤ 2048 chars |
code | Custom code · [A-Za-z0-9_-], 3–40 chars · must be unique and not a reserved word · omit to auto-generate |
title | Link title · ≤ 255 |
description | Description · ≤ 2000 |
status | active or disabled (default active) — disabled links do not redirect |
available_from | Activation date-time (ISO-8601) · before this the link returns 404 "not active yet" |
expires_at | Expiry date-time (ISO-8601) · must be in the future and after available_from · after this the link returns 410 |
password | Visitor password · 4–255 chars · stored hashed, never returned by the API |
remove_password | (update only) send true to clear a password that was set |
Example request
curl -X PATCH "https://app.isref.cc/api/v1/shortlinks/41" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"title":"Linux course 2026","status":"disabled"}'
import requests
resp = requests.patch(
"https://app.isref.cc/api/v1/shortlinks/41",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/json",
},
json={
"title": "Linux course 2026",
"status": "disabled",
},
)
print(resp.status_code, resp.json())
<?php
$ch = curl_init("https://app.isref.cc/api/v1/shortlinks/41");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "PATCH",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer {$token}",
"Accept: application/json",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"title" => "Linux course 2026",
"status" => "disabled",
]),
]);
$response = curl_exec($ch);
printf("%d\n%s\n", curl_getinfo($ch, CURLINFO_HTTP_CODE), $response);
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
payload := strings.NewReader(`{"title":"Linux course 2026","status":"disabled"}`)
req, _ := http.NewRequest("PATCH", "https://app.isref.cc/api/v1/shortlinks/41", payload)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(resp.StatusCode, string(out))
}
const resp = await fetch("https://app.isref.cc/api/v1/shortlinks/41", {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Linux course 2026",
status: "disabled",
}),
});
console.log(resp.status, await resp.json());
200 · "message": "Short link updated."
DELETE /shortlinks/{id}
Permanently delete a link.
curl -X DELETE "https://app.isref.cc/api/v1/shortlinks/41" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"
import requests
resp = requests.delete(
"https://app.isref.cc/api/v1/shortlinks/41",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/json",
},
)
print(resp.status_code, resp.json())
<?php
$ch = curl_init("https://app.isref.cc/api/v1/shortlinks/41");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer {$token}",
"Accept: application/json",
],
]);
$response = curl_exec($ch);
printf("%d\n%s\n", curl_getinfo($ch, CURLINFO_HTTP_CODE), $response);
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("DELETE", "https://app.isref.cc/api/v1/shortlinks/41", nil)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(resp.StatusCode, string(out))
}
const resp = await fetch("https://app.isref.cc/api/v1/shortlinks/41", {
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
});
console.log(resp.status, await resp.json());
204 No Content
The Shortlink object
The shape every endpoint returns under data (or data.items for the list).
| Field | Rules | Description |
|---|---|---|
id | integer | Numeric link id — used in endpoint URLs |
code | string | The short link's code |
short_url | string | The full, ready-to-use short URL |
destination_url | string | The target URL |
title | string · null | Link title (or null) |
description | string · null | Description (or null) |
status | string | active or disabled |
has_password | boolean | true if this link is password-protected |
available_from | string · null | Activation time (ISO-8601 or null) |
is_pending | boolean | true if available_from is still in the future |
expires_at | string · null | Expiry time (ISO-8601 or null) |
is_expired | boolean | true if the expiry time has passed |
click_count | integer | Total clicks recorded |
last_clicked_at | string · null | Time of the last click (or null) |
owner | object · null | The link owner { id, name } — present when loaded |
created_at | string | Creation time (ISO-8601) |
updated_at | string | Last update time (ISO-8601) |
Availability window
available_from and expires_at bound when a link works. They affect GET /{code} as follows.
| Time | GET /{code} |
|---|---|
before available_from | 404 — "not active yet" page (is_pending: true) |
within the window | redirects normally |
after expires_at | 410 Gone (is_expired: true) |
Send available_from: null (or "") on update to clear it.
Password-protected links
Setting password makes the public redirect show an unlock page before redirecting (GET /{code} → form → POST /{code}). This gate is part of the browser flow only; the API just reports has_password and never returns the hash.
Error examples
Token lacks the ability
POST /api/v1/shortlinks (token: shorturl:read only) 403 { "success": false, "message": "Invalid ability provided.", "errors": {} }
Validation failed
POST /api/v1/shortlinks {"destination_url":"javascript:alert(1)"} 422 { "success": false, "message": "The given data was invalid.", "errors": { "destination_url": ["The destination url must start with http:// or https://."] } }
Not the owner
GET /api/v1/shortlinks/999 (belongs to another user) 403 { "success": false, "message": "This action is unauthorized.", "errors": {} }