REST API v1 · จัดการลิงก์สั้น

คู่มือ API

เริ่มต้นใช้งาน

ทุกคำขอต้องแนบ Bearer token ของ Sanctum และตั้ง header Accept: application/json ทุกครั้ง โทเคนสร้างได้ที่หน้า Profile → API Tokens ในระบบ (ระบบจะแสดงค่าโทเคนแบบเต็มเพียงครั้งเดียวตอนสร้าง)

Base URL

https://app.isref.cc/api/v1

Header ที่ต้องส่ง

Authorization: Bearer <token>
Accept: application/json
Content-Type: application/json   # POST / PUT / PATCH

ผู้ใช้ทั่วไปเห็นและจัดการได้เฉพาะลิงก์ของตนเอง โทเคนของผู้ดูแลระบบจัดการลิงก์ของทุกคนได้

รูปแบบผลลัพธ์ (Response envelope)

ทุกผลลัพธ์ ทั้งสำเร็จและผิดพลาด (รวมถึง exception จากเฟรมเวิร์ก) ใช้โครงสร้างเดียวกัน

สำเร็จ

{
  "success": true,
  "data": {  },
  "message": "Short link created."
}

ผิดพลาด

{
  "success": false,
  "data": null,
  "message": "This action is unauthorized.",
  "errors": {}
}

รหัสสถานะและข้อผิดพลาด

HTTPเกิดเมื่อ
200อ่านข้อมูล หรือแก้ไขสำเร็จ
201สร้างลิงก์ใหม่สำเร็จ
204ลบลิงก์สำเร็จ (ไม่มีเนื้อหาตอบกลับ)
401ไม่มีโทเคน หรือโทเคนไม่ถูกต้อง/หมดอายุ
403โทเคนไม่มีสิทธิ์ที่ต้องใช้ หรือลิงก์ไม่ใช่ของเจ้าของโทเคน
404ไม่พบเส้นทาง หรือไม่พบ id ที่ระบุ
422ข้อมูลไม่ผ่านการตรวจสอบ (ดูรายละเอียดใน errors)
429เรียกเกินอัตราที่กำหนด (60 ครั้ง/นาที ต่อโทเคน)

สิทธิ์ของโทเคน

สิทธิ์ถูกตรวจสอบที่ฝั่งเซิร์ฟเวอร์ ไคลเอนต์ไม่สามารถกำหนดสิทธิ์ของตัวเองได้ เลือกสิทธิ์ตอนสร้างโทเคนในระบบ

Endpointสิทธิ์ที่ต้องมี
GET /shortlinks shorturl:read
GET /shortlinks/{id} shorturl:read
POST /shortlinks shorturl:create
PUT · PATCH /shortlinks/{id} shorturl:update
DELETE /shortlinks/{id} shorturl:delete

การจำกัดอัตรา

จำกัด 60 คำขอต่อนาที ต่อโทเคน (นับรวมกับ id ผู้ใช้) เมื่อเกินจะได้ 429 พร้อม header X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Endpoints

GET /shortlinks

คืนรายการลิงก์ของผู้ใช้แบบแบ่งหน้า เรียงจากใหม่ไปเก่า

พารามิเตอร์ query

ฟิลด์คำอธิบาย
qค้นหาจาก code, title หรือ destination_url
statusกรองตามสถานะ: active หรือ disabled
per_pageจำนวนต่อหน้า 1–100 (ค่าเริ่มต้น 15)
pageหน้าที่ต้องการ (เริ่มที่ 1)

ตัวอย่างคำขอ

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());

ตัวอย่างผลลัพธ์

{
  "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

สร้างลิงก์สั้นใหม่ ถ้าไม่ส่ง code ระบบจะสุ่มให้

เนื้อหาคำขอ (JSON)

ฟิลด์ข้อกำหนด
destination_url จำเป็น URL ปลายทาง · http/https เท่านั้น · ต้องมีโฮสต์ · ยาวไม่เกิน 2048 ตัวอักษร
code ไม่บังคับ โค้ดกำหนดเอง · [A-Za-z0-9_-] ยาว 3–40 ตัว · ต้องไม่ซ้ำและไม่ใช่คำสงวน · เว้นว่างเพื่อให้สุ่มอัตโนมัติ
title ไม่บังคับ ชื่อลิงก์ · ยาวไม่เกิน 255
description ไม่บังคับ คำอธิบาย · ยาวไม่เกิน 2000
status ไม่บังคับ active หรือ disabled (ค่าเริ่มต้น active) — disabled จะไม่ redirect
available_from ไม่บังคับ วันเวลาที่เริ่มใช้งานได้ (ISO-8601) · ก่อนเวลานี้ลิงก์จะตอบ 404 “ยังไม่เปิดใช้งาน”
expires_at ไม่บังคับ วันเวลาหมดอายุ (ISO-8601) · ต้องเป็นอนาคต และหลัง available_from · หลังเวลานี้ลิงก์ตอบ 410
password ไม่บังคับ รหัสผ่านสำหรับผู้เข้าชม · 4–255 ตัวอักษร · เก็บแบบแฮช ไม่คืนค่าผ่าน API

ตัวอย่างคำขอ

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());

ตัวอย่างผลลัพธ์ · 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}

คืนข้อมูลลิงก์เดียวตาม 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 · โครงสร้างที่ทุก endpoint คืนกลับมาในฟิลด์ data (หรือ data.items สำหรับรายการ)

PUT PATCH /shortlinks/{id}

แก้ไขลิงก์ ส่งเฉพาะฟิลด์ที่ต้องการเปลี่ยน (PATCH) หรือส่งครบ (PUT) การเปลี่ยน code จะตรวจความซ้ำและคำสงวนใหม่

ส่ง password ค่าใหม่เพื่อตั้ง/เปลี่ยนรหัส · ส่ง remove_password: true เพื่อยกเลิกการป้องกัน · ไม่ส่งทั้งคู่ = คงค่าเดิม

ฟิลด์ข้อกำหนด
destination_urlURL ปลายทาง · http/https เท่านั้น · ต้องมีโฮสต์ · ยาวไม่เกิน 2048 ตัวอักษร
codeโค้ดกำหนดเอง · [A-Za-z0-9_-] ยาว 3–40 ตัว · ต้องไม่ซ้ำและไม่ใช่คำสงวน · เว้นว่างเพื่อให้สุ่มอัตโนมัติ
titleชื่อลิงก์ · ยาวไม่เกิน 255
descriptionคำอธิบาย · ยาวไม่เกิน 2000
statusactive หรือ disabled (ค่าเริ่มต้น active) — disabled จะไม่ redirect
available_fromวันเวลาที่เริ่มใช้งานได้ (ISO-8601) · ก่อนเวลานี้ลิงก์จะตอบ 404 “ยังไม่เปิดใช้งาน”
expires_atวันเวลาหมดอายุ (ISO-8601) · ต้องเป็นอนาคต และหลัง available_from · หลังเวลานี้ลิงก์ตอบ 410
passwordรหัสผ่านสำหรับผู้เข้าชม · 4–255 ตัวอักษร · เก็บแบบแฮช ไม่คืนค่าผ่าน API
remove_password(เฉพาะตอนแก้ไข) ส่ง true เพื่อลบรหัสผ่านที่ตั้งไว้

ตัวอย่างคำขอ

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}

ลบลิงก์ถาวร

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

ออบเจ็กต์ Shortlink

โครงสร้างที่ทุก endpoint คืนกลับมาในฟิลด์ data (หรือ data.items สำหรับรายการ)

ฟิลด์ข้อกำหนดคำอธิบาย
idintegerรหัสตัวเลขของลิงก์ — ใช้อ้างอิงใน URL ของ endpoint
codestringโค้ดของลิงก์สั้น
short_urlstringURL สั้นแบบเต็มพร้อมใช้งาน
destination_urlstringURL ปลายทาง
titlestring · nullชื่อลิงก์ (หรือ null)
descriptionstring · nullคำอธิบาย (หรือ null)
statusstringactive หรือ disabled
has_passwordbooleantrue ถ้าลิงก์นี้ตั้งรหัสผ่านไว้
available_fromstring · nullเวลาเริ่มใช้งาน (ISO-8601 หรือ null)
is_pendingbooleantrue ถ้ายังไม่ถึงเวลา available_from
expires_atstring · nullเวลาหมดอายุ (ISO-8601 หรือ null)
is_expiredbooleantrue ถ้าเลยเวลาหมดอายุแล้ว
click_countintegerจำนวนคลิกสะสม
last_clicked_atstring · nullเวลาที่ถูกคลิกล่าสุด (หรือ null)
ownerobject · nullเจ้าของลิงก์ { id, name } — แสดงเมื่อโหลดข้อมูลมาด้วย
created_atstringเวลาที่สร้าง (ISO-8601)
updated_atstringเวลาที่แก้ไขล่าสุด (ISO-8601)

ช่วงเวลาเปิดใช้งาน

available_from และ expires_at กำหนดช่วงที่ลิงก์ทำงาน มีผลกับการเข้าผ่าน GET /{code} ดังนี้

สถานะเวลาผลลัพธ์เมื่อเปิดลิงก์
ก่อน available_from404 — หน้า “ยังไม่เปิดใช้งาน” (is_pending: true)
อยู่ในช่วงredirect ตามปกติ
หลัง expires_at410 Gone (is_expired: true)

ส่ง available_from: null (หรือ "") ตอนแก้ไขเพื่อล้างค่า

ลิงก์มีรหัสผ่าน

เมื่อตั้ง password ผู้เข้าชมจะเจอหน้าให้กรอกรหัสก่อน redirect (GET /{code} → ฟอร์ม → POST /{code}) กลไกนี้เป็นส่วนของฝั่งเบราว์เซอร์เท่านั้น API จะรายงานแค่ has_password และไม่คืนค่าแฮชของรหัสผ่าน

ตัวอย่างข้อผิดพลาด

โทเคนไม่มีสิทธิ์

POST /api/v1/shortlinks   (token: shorturl:read only)
403  { "success": false, "message": "Invalid ability provided.", "errors": {} }

ข้อมูลไม่ผ่านการตรวจสอบ

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://."] }
}

ไม่ใช่เจ้าของลิงก์

GET /api/v1/shortlinks/999   (belongs to another user)
403  { "success": false, "message": "This action is unauthorized.", "errors": {} }