StatusThai API Documentation
Weather, news and counter API for HTML/JavaScript and PHP developers.
1. Overview
Base URL: https://thai.craftcloudnet.com/api.php
All responses are JSON (UTF-8). Every request needs an API key, which is free.
| Resource | Method | Purpose |
|---|---|---|
r=weather | GET | Current weather from all TMD stations (data refreshed hourly) |
r=news | GET | Latest news from the Thai Meteorological Department feed (refreshed every 2 minutes) |
r=counter | GET / POST | Read or increment a named counter |
2. Getting an API key
- Register or log in at /login.php.
- Open /dashboard.php and create a key. By default each member can have 3 keys.
- Copy the key immediately. It is shown only once, because only a hash is stored.
Key format: st_ followed by 40 hex characters, for example st_0123456789abcdef0123456789abcdef01234567.
Domain lock (optional): you can lock a key to a domain, for example mysite.com, which also allows www.mysite.com and other subdomains. Browser requests from any other domain are rejected with 403. Server-side requests (PHP, curl) send no Origin header, so they are not filtered by the lock.
3. Authentication
Send the key in the X-API-Key header (recommended) or as a key query parameter.
X-API-Key: st_your_key_here
GET https://thai.craftcloudnet.com/api.php?r=weather&key=st_your_key_here
- The header is cleaner, because query strings appear in server logs and browser history.
- A custom header makes browsers send a CORS preflight (OPTIONS) request. The API answers it automatically.
- Requests without a key are accepted only from the StatusThai website itself, and never for
counter.
4. Endpoints
4.1 Weather
GET /api.php?r=weather
| Parameter | Required | Description |
|---|---|---|
prov | No | Case-insensitive text filter on province and station name, for example prov=เชียงใหม่. Always URL-encode it. |
Example response:
{
"updated": 1794221485,
"source": "กรมอุตุนิยมวิทยา",
"stations": [
{
"id": "48327",
"name": "เชียงใหม่",
"en": "Chiang Mai",
"prov": "เชียงใหม่",
"lat": 18.77,
"lon": 98.97,
"temp": 30.1,
"max": 33.0,
"min": 24.5,
"rh": 70,
"wind": 8.5,
"wdir": 180,
"rain": 0.0,
"pres": 1010.2,
"time": "2026-11-09 10:00:00"
}
]
}
| Field | Meaning |
|---|---|
updated | Unix timestamp (seconds) of the last refresh from TMD |
id | WMO station number |
name / en | Station name in Thai / English |
prov | Province |
lat, lon | Coordinates |
temp, max, min | Temperature now, daily maximum, daily minimum (°C) |
rh | Relative humidity (%) |
wind, wdir | Wind speed and direction (degrees) |
rain | Rainfall (mm) |
pres | Sea-level pressure |
time | Observation time as reported by TMD |
Any measurement can be null when the station did not report it. Units follow the TMD source data.
4.2 News
GET /api.php?r=news&limit=5
| Parameter | Required | Description |
|---|---|---|
limit | No | Number of items, 1 to 30. Without it you get everything the feed has (up to 30). |
{
"updated": 1794221485,
"items": [
{
"title": "Headline",
"link": "https://example.com/article",
"img": "https://example.com/image.jpg",
"desc": "Short plain-text summary (max about 220 characters)",
"ts": 1794220000
}
]
}
link and img are empty strings when the feed item has none. ts is the publish time as a Unix timestamp.
4.3 Counter
A counter is a named number belonging to your account. Typical use: page views.
| Method | Effect |
|---|---|
GET /api.php?r=counter&name=home | Read the value. Returns 404 if the counter does not exist yet. |
POST /api.php?r=counter&name=home | Add 1. Creates the counter on first use. |
name: lowercase letters, digits,-and_; 2 to 32 characters; it must start with a letter or digit.- Each member can have up to 20 counters by default. Creating more returns 403
counter_limit.
{ "name": "home", "n": 42 }
5. Errors
Errors return JSON with error (a code for your program) and message (Thai text for people).
{ "error": "invalid_key", "message": "API key ไม่ถูกต้องหรือถูกยกเลิก" }
| HTTP | error | Cause |
|---|---|---|
| 304 | none | Data unchanged since your If-None-Match ETag (empty body) |
| 400 | bad_request | r is not weather, news or counter |
| 400 | bad_name | Invalid counter name |
| 401 | invalid_key | Key is wrong, revoked, or the owner is suspended |
| 401 | key_required | No key sent (or counter without a key) |
| 403 | domain | Browser request from a domain the key is not locked to |
| 403 | counter_limit | Counter quota reached |
| 404 | not_found | Counter does not exist yet (send a POST to create it) |
| 429 | limit | Rate limit exceeded. Read the Retry-After header or the retry field (seconds). |
| 503 | no_data | No cached data yet. Retry after 30 seconds. |
6. Rate limits and caching
- 60 requests per minute per key by default. The admin can change this per key.
- Sending wrong keys too often is limited per IP (10 per minute by default), and repeat offenders are banned for 1 hour.
- There is also a global limit shared by all keys. If it is hit, everyone waits until the minute ends.
- Weather data refreshes from TMD at most once per hour and news every 2 minutes. Calling more often returns the same cached data, so poll every 5 to 10 minutes at most.
- Responses include an
ETagandCache-Control: private, max-age=20. SendIf-None-Matchto get a cheap 304 when nothing changed.
7. HTML / JavaScript examples
7.1 Minimal fetch
<pre id="out">Loading...</pre>
<script>
const API = 'https://thai.craftcloudnet.com/api.php';
const KEY = 'st_your_key_here';
fetch(API + '?r=weather&prov=' + encodeURIComponent('เชียงใหม่'), {
headers: { 'X-API-Key': KEY }
})
.then(r => r.json())
.then(d => { document.getElementById('out').textContent = JSON.stringify(d, null, 2); })
.catch(e => { document.getElementById('out').textContent = e.message; });
</script>
7.2 Reusable helper with error handling
<script>
const API = 'https://thai.craftcloudnet.com/api.php';
const KEY = 'st_your_key_here';
async function stApi(params, method = 'GET') {
const url = API + '?' + new URLSearchParams(params).toString();
const res = await fetch(url, { method, headers: { 'X-API-Key': KEY } });
const body = await res.json().catch(() => ({}));
if (!res.ok) {
const err = new Error(res.status + ' ' + (body.error || '') + ': ' + (body.message || ''));
err.status = res.status;
err.retry = body.retry || Number(res.headers.get('Retry-After')) || 0;
throw err;
}
return body;
}
// Weather for one province, shown as a list
stApi({ r: 'weather', prov: 'เชียงใหม่' })
.then(d => {
const ul = document.createElement('ul');
d.stations.forEach(s => {
const li = document.createElement('li');
li.textContent = s.name + ': ' + (s.temp ?? '-') + ' °C';
ul.appendChild(li);
});
document.body.appendChild(ul);
})
.catch(e => console.error(e.message));
// Latest 5 headlines
stApi({ r: 'news', limit: 5 }).then(d => console.log(d.items));
// Count a page view
stApi({ r: 'counter', name: 'home' }, 'POST').then(d => console.log('views:', d.n));
</script>
textContent is used on purpose. Use it instead of innerHTML when showing API data, so text from the news feed can never inject HTML.
7.3 Without a custom header (no preflight)
If a proxy or firewall blocks the preflight request, pass the key in the URL instead:
fetch(API + '?r=weather&key=' + KEY).then(r => r.json()).then(console.log);
8. PHP examples
PHP calls run on your server, so the key stays private. You need the curl extension.
8.1 Helper function (st_api.php)
<?php
const ST_API_URL = 'https://thai.craftcloudnet.com/api.php';
const ST_API_KEY = 'st_your_key_here'; // better: environment variable or a config file outside the web root
/**
* Call the StatusThai API. Returns the decoded JSON array.
* Throws RuntimeException on network errors or non-200 responses.
*/
function st_api(array $params, string $method = 'GET'): array
{
$ch = curl_init(ST_API_URL . '?' . http_build_query($params));
$opts = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 6,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => ['X-API-Key: ' . ST_API_KEY, 'Accept: application/json'],
];
if ($method === 'POST') {
$opts[CURLOPT_POST] = true;
$opts[CURLOPT_POSTFIELDS] = '';
}
curl_setopt_array($ch, $opts);
$body = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($body === false) {
throw new RuntimeException('cURL error: ' . $err);
}
$json = json_decode($body, true);
if ($code !== 200 || !is_array($json)) {
$msg = is_array($json) ? (($json['error'] ?? '') . ': ' . ($json['message'] ?? '')) : 'invalid response';
throw new RuntimeException('HTTP ' . $code . ' ' . $msg, $code);
}
return $json;
}
8.2 Weather
<?php
require 'st_api.php';
try {
$data = st_api(['r' => 'weather', 'prov' => 'เชียงใหม่']);
foreach ($data['stations'] as $s) {
echo htmlspecialchars($s['name']) . ': ' . ($s['temp'] ?? '-') . " °C\n";
}
} catch (RuntimeException $e) {
echo 'Error: ' . htmlspecialchars($e->getMessage());
}
8.3 News
<?php
$news = st_api(['r' => 'news', 'limit' => 5]);
foreach ($news['items'] as $n) {
echo '<a href="' . htmlspecialchars($n['link']) . '">' . htmlspecialchars($n['title']) . "</a><br>\n";
}
8.4 Counter
<?php
$views = st_api(['r' => 'counter', 'name' => 'home'], 'POST'); // +1
echo 'Page views: ' . (int) $views['n'];
8.5 Safe pattern for a public page: PHP proxy with a cache
Browser JavaScript on your own site calls this file, so the key never reaches the visitor. The file cache keeps you far below the rate limit.
<?php
// weather-proxy.php on YOUR site
require 'st_api.php';
header('Content-Type: application/json; charset=utf-8');
$prov = substr(trim((string) ($_GET['prov'] ?? '')), 0, 60);
$cache = sys_get_temp_dir() . '/weather_' . md5($prov) . '.json';
if (is_file($cache) && time() - filemtime($cache) < 300) { // 5 minutes
readfile($cache);
exit;
}
try {
$params = ['r' => 'weather'] + ($prov !== '' ? ['prov' => $prov] : []);
$json = json_encode(st_api($params), JSON_UNESCAPED_UNICODE);
file_put_contents($cache, $json, LOCK_EX);
echo $json;
} catch (RuntimeException $e) {
if (is_file($cache)) { readfile($cache); exit; } // serve stale data on failure
http_response_code(502);
echo json_encode(['error' => 'upstream']);
}
<script>
fetch('/weather-proxy.php?prov=' + encodeURIComponent('เชียงใหม่'))
.then(r => r.json())
.then(console.log);
</script>
9. Security best practices
- Never put a key in a public page unless it is locked to your domain. Anyone can read JavaScript. A leaked key lets others use your quota.
- Prefer the PHP proxy pattern (8.5) for public sites.
- Domain locks only apply to browsers. A leaked key still works from a server, so revoke and recreate it if it leaks.
- Do not paste keys into chats, screenshots, public repositories or support tickets.
- Keep PHP keys outside the web root or in environment variables.
- Always escape API data when printing:
htmlspecialchars()in PHP,textContentin JavaScript.
10. Troubleshooting
| Symptom | Likely cause and fix |
|---|---|
Browser shows NetworkError / CORS error with status 404 | Wrong host. Use thai.craftcloudnet.com, not craftcloudnet.com. |
401 invalid_key | Key typed wrongly, revoked, or the account is suspended. Create a new key in the dashboard. |
401 key_required | The header was not received. Check the header name X-API-Key, or use ?key=. |
403 domain | The key is locked to another domain. A page opened from disk (file://) sends Origin: null, which fails a domain lock. Test from your real domain or clear the lock. |
429 limit | Too many requests. Wait for Retry-After seconds and cache results on your side. |
503 no_data | The server has no cached data yet. Retry after 30 seconds. |
| Browser blocks the request before it is sent | Page is http:// while the API is https:// (mixed content), or an ad blocker or privacy setting is interfering. |
| Works in the address bar but not in your script | Open developer tools (F12), check the Network tab for the status code and read the JSON message in the response body. |