REST API v1 · manage short links

API guide

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

HTTPWhen
200Read, or a successful update
201A new link was created
204A link was deleted (no body returned)
401Missing token, or an invalid / expired one
403The token lacks the required ability, or the link is not yours
404Unknown route, or no link with that id
422Validation failed (see the errors object)
429Rate 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.

EndpointRequired 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

FieldDescription
qSearch across code, title and destination_url
statusFilter by status: active or disabled
per_pageItems per page, 1–100 (default 15)
pagePage 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)

FieldRules
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.

FieldRules
destination_urlTarget URL · http / https only · host required · ≤ 2048 chars
codeCustom code · [A-Za-z0-9_-], 3–40 chars · must be unique and not a reserved word · omit to auto-generate
titleLink title · ≤ 255
descriptionDescription · ≤ 2000
statusactive or disabled (default active) — disabled links do not redirect
available_fromActivation date-time (ISO-8601) · before this the link returns 404 "not active yet"
expires_atExpiry date-time (ISO-8601) · must be in the future and after available_from · after this the link returns 410
passwordVisitor 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).

FieldRulesDescription
idintegerNumeric link id — used in endpoint URLs
codestringThe short link's code
short_urlstringThe full, ready-to-use short URL
destination_urlstringThe target URL
titlestring · nullLink title (or null)
descriptionstring · nullDescription (or null)
statusstringactive or disabled
has_passwordbooleantrue if this link is password-protected
available_fromstring · nullActivation time (ISO-8601 or null)
is_pendingbooleantrue if available_from is still in the future
expires_atstring · nullExpiry time (ISO-8601 or null)
is_expiredbooleantrue if the expiry time has passed
click_countintegerTotal clicks recorded
last_clicked_atstring · nullTime of the last click (or null)
ownerobject · nullThe link owner { id, name } — present when loaded
created_atstringCreation time (ISO-8601)
updated_atstringLast update time (ISO-8601)

Availability window

available_from and expires_at bound when a link works. They affect GET /{code} as follows.

TimeGET /{code}
before available_from404 — "not active yet" page (is_pending: true)
within the windowredirects normally
after expires_at410 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": {} }