規格

回拋格式

App 每採集一個點,就對你的伺服器送出一個 HTTPS 請求。這一頁說明請求怎麼送、body 的每一個欄位,以及接收端常見的處理方式。

POST / PUThttps://你的伺服器/任何路徑

格式版本(ver)和 App 版本相同,目前是 1.0.0。這一頁一律是最新的格式。

想交給 AI 寫接收端?下載給 AI 用的 Skill

請求

建議:收到就先回應,處理放到之後

收到請求就立刻回 2xx(例如 204),寫入資料庫、計算這類處理放到回應之後再做。App 不等回應就送下一筆,最快每秒一筆;伺服器回得慢,還沒回應的請求就會累積,累積到 5 個時 App 只保留最新的一點,中間的點不會送出。

網址
只能是 https://。
方法
POST(預設)或 PUT,每台伺服器各自設定。
Header
Content-Type: application/json,再加上你在 App 裡設定的 Header。Content-Type、Content-Length、Host、Connection、Transfer-Encoding 由 App 決定,設定了同名的 Header 也不會生效。作業系統另外會自動加上 User-Agent,內容由系統決定(見隱私權政策)。
Body
一行 JSON,不縮排,不用 gzip。大約 750 bytes。
時機
每採集一個點送一次。設定了好幾台伺服器時(最多勾選 5 台),同一個點送到每一台,內容完全一樣。最快的情況:時間模式每 1 秒一筆;距離模式設 0 公尺時,系統每更新一次定位就送一筆。
順序
不等上一筆回應就送下一筆,請求可能亂序到達。
同時請求
每台伺服器最多 5 個請求還沒回應。滿了的話新的點先等著,有空位就送;等待中只留最新的一點,被換掉的點不送,算失敗。伺服器回應越慢,掉的點越多。
逾時
10 秒。
成功
建議直接回 204,不帶 body。HTTP 2xx 算成功,body 空白也算。例外:回應的 Content-Type 是 JSON(application/json 或 +json 結尾,看分號前面)但 body 解析不了,算失敗。JSON 的內容不會檢查;不是 JSON 的回應不看 body。
回應大小
最多讀 1 MB,超過就斷線,算失敗。
失敗
不重送,也沒有離線佇列,伺服器不會再收到那一點(手機上的紀錄仍然有,可以匯出)。伺服器可以從 pt.id 不連續看出掉了幾點。

通用規則

結構
外層分成 dev、perm、bat、net、rule、pt 六組,組內扁平,最多兩層。
沒有值
平台沒有的欄位、這一次拿不到的值,一律是 null,key 一定保留。不會用 -1、0 或空字串代替。
時間
epoch 毫秒,UTC 的整數。
列舉
整數,從 1 開始,不用 0。新的值往後接,舊的號碼不會改。
小數
送出前四捨五入:lat、lng 7 位(約 1 公分),spd、spdAcc 2 位,其他公尺、度、°C 1 位。
排序
依 pt.id。pt.time 可能和上一點相同,手機改時間時也可能倒退。
平台
看 dev.os。同一個 key 在兩個平台意思相同,差別只在有沒有值。
新欄位
之後的版本可能在各組加新的 key。不認得的 key 請略過,不要整筆拒收;還沒更新的舊版 App 不會送較新的 key,請當成 null。格式版本看 ver 的大、中版號。

沒有另外標示平台的欄位,iOS 與 Android 都有值。

接收範例

收下一筆定位的關鍵步驟:檢查 Method 與 Header、解析 JSON、存起來、回 2xx。範例用 X-Api-Key: change-me 當權杖,請換成你在 App 裡設定的 Header。正式使用還要加上 HTTPS、資料庫與錯誤處理。

<?php$apiKey = 'change-me'; // 預設是 POST,App 裡設定成 PUT 就是 PUT$method = $_SERVER['REQUEST_METHOD'];if ($method !== 'POST' && $method !== 'PUT') {  http_response_code(405);  exit;} // 你在 App 裡加的 Header(名稱和值自己決定)if (!hash_equals($apiKey, $_SERVER['HTTP_X_API_KEY'] ?? '')) {  http_response_code(401);  exit;} // Body 是一行 JSON,所以 $_POST 是空的$body = file_get_contents('php://input');$payload = json_decode($body, true);if (!is_array($payload['dev'] ?? null) || !is_array($payload['pt'] ?? null)) {  http_response_code(400);  exit;} $dev = $payload['dev'];$pt = $payload['pt']; // pt.trig 是 4 的是「發送測試」:pt.id 是 0,lat、lng 可能是 nullif ($pt['trig'] !== 4) {  file_put_contents('points.jsonl', $body . "\n", FILE_APPEND | LOCK_EX);  error_log("{$dev['id']} {$payload['startAt']} #{$pt['id']} {$pt['lat']},{$pt['lng']}");} // 回任何 2xx 都算成功(10 秒逾時,不重送)http_response_code(204);

不用手機也能測

正式版 App 只送 https://,本機開發時收不到。可以先用 curl 送一筆範例資料到本機的接收範例(port 8080),內容和 iOS 範例相同。

curl -i -X POST http://localhost:8080/ \  -H 'Content-Type: application/json' \  -H 'X-Api-Key: change-me' \  -d '{"ver":"1.0.0","sentAt":1759631403120,"startAt":1759630000000,"dev":{"id":"3F2A9C1E-7B4D-4E8A-9C21-5D6E7F809A1B","os":"ios","osVer":"15.8.8","appVer":"1.0.0","build":"1","tz":"Asia/Taipei","locale":"zh-TW","mfr":"Apple","model":"iPhone9,2","isSim":false},"perm":{"loc":2,"precise":true,"noBatOpt":null},"bat":{"lvl":85,"state":1,"plug":null,"temp":null,"volt":null,"lowPower":false,"thermal":1},"net":{"type":2,"metered":true,"lowData":false,"gen":4},"rule":{"name":"Survey","mode":1,"interval":30,"distance":null,"heartbeat":null,"power":1},"pt":{"id":1024,"time":1759631398000,"trig":1,"lat":23.5678901,"lng":120.3045672,"acc":4.8,"alt":12.3,"ellAlt":31.5,"altAcc":3.0,"spd":1.2,"spdAcc":0.5,"crs":87.5,"crsAcc":12.0,"satUsed":null,"satView":null,"isSim":false,"isExt":false,"held":false,"suspect":false}}'

交給 AI 寫

我們把這份規格、四個接收範例和測試腳本包成 Agent Skill。裝好之後跟 AI 說「幫我寫定位郵差的接收端」,它就會照規格寫,連容易漏掉的細節都會處理:發送測試、亂序、null、回 204。Skill 和規格一律是最新版,App 更新格式時重新下載即可。

一行指令安裝

解壓縮到兩個地方:~/.claude/skills 給 Claude Code,~/.agents/skills 給 Codex、GitHub Copilot、Cursor。要更新時再執行一次。

curl -fsSL https://tracker.iwi.tw/skill/geocourier-receiver.zip -o /tmp/geocourier-receiver.zipmkdir -p ~/.claude/skills ~/.agents/skillsunzip -oq /tmp/geocourier-receiver.zip -d ~/.claude/skillsunzip -oq /tmp/geocourier-receiver.zip -d ~/.agents/skills

各工具放在哪裡

工具個人(所有專案)專案(跟著版控)
Claude Code~/.claude/skills/.claude/skills/
Codex~/.agents/skills/.agents/skills/
GitHub Copilot~/.copilot/skills/.github/skills/
Cursor~/.cursor/skills/.cursor/skills/

路徑依各工具 2026 年 10 月的官方文件。其他支援 Agent Skills 的工具,把解壓縮出來的 geocourier-receiver 資料夾放進它的 skills 目錄即可。

Claude.ai、Claude Desktop

到「設定 → Capabilities」開啟 Code execution,再到 Skills 區塊按 Upload skill,上傳下載的 zip。

不支援 Skill 的 AI

把 https://tracker.iwi.tw/spec.md 貼給它,或下載後上傳。這是同一份規格的純文字版;這一頁是用 JavaScript 畫的,多數 AI 讀網頁時看不到內容。

可以這樣問

  • 幫我用 Laravel 寫定位郵差的接收端,存進 MySQL
  • 我的 Express 收不到定位郵差送來的資料,幫我找原因
  • 定位郵差的 rule.heartbeat 是什麼?要怎麼判斷手機斷線?
  • 幫我設計存定位點的資料表,之後要畫出每一次定位的路線

外層

verversionstring

格式版本,跟 App 版本走,值和 dev.appVer 相同,例如 1.0.0。看大、中版號判斷格式:回拋的結構有變動時,App 一定升大版號或中版號(例如 1.0 → 1.1);只有小版號不同(1.0.0、1.0.3)表示格式相同,只是 App 本身的修正。

sentAtsent atint(ms)

送出這個請求的時間。

startAtstart atint(ms)發送測試時可能 null

這次按下「開始定位」的時間。pt.id 不會因為重新開始而歸零,要分出是哪一次定位就看它。沒在定位時按發送測試,這裡是 null。

devdevice裝置,安裝後大致不變

idstring

安裝 App 時產生的 UUID。不跟著系統備份,刪 App 重裝、從備份還原到新手機都會換新的。不是硬體識別碼,也不是廣告 ID。

osoperating systemstring

平台。

  • iosiOS
  • androidAndroid
osVerOS versionstring

系統版本,例如 15.8.8(iOS)、15(Android)。

appVerapp versionstring

App 版本,例如 1.0.0。

buildstring

App 的 build 號。

tztime zonestring

IANA 時區,例如 Asia/Taipei。

localestring

手機的語言與地區,固定是「語言-地區」,例如 zh-TW、en-US、ja-JP;沒有地區時只有語言。語言是使用者為這個 App 選的語言,沒有另外選就是手機的第一個語言。不分繁體、簡體:簡體中文、地區香港的手機是 zh-HK。

mfrmanufacturerstring

廠牌。

iOS
固定 Apple
Android
例如 OPPO
modelstring

型號代號,不是行銷名稱。

iOS
例如 iPhone9,2
Android
例如 CPH2581
isSimis simulatorbool

App 是不是跑在模擬器上。

iOS
準確
Android
沒有官方 API,依系統特徵判斷,官方模擬器判斷得出來

permpermission權限,使用者隨時會改

loclocationint

定位權限。定位中 1 和 2 都是正常的:兩種都能在背景持續定位。沒給權限時也能按「發送測試」,這時是 3。

  • 1使用 App 期間
  • 2永遠
  • 3沒有權限(還沒問過、拒絕或受限)
precisebool

是否允許精確位置。false 時位置只精確到幾公里;loc 是 3 時一律 false。

noBatOptno battery optimizationbool可能 null

true 表示不受電池最佳化限制,在背景比較不會被系統停掉。

iOS
沒有,固定 null

batbattery電池與電源

lvllevelint可能 null

電量 0~100。

iOS
iOS 17 起以 5% 為一階
Android
以 1% 為一階
stateint可能 null

充電狀態。系統回報未知時是 null。

  • 1未插電
  • 2充電中
  • 3已充滿
  • 4插著電但沒在充電
iOS
只會出現 1~3
plugint可能 null

電源種類。沒插電時是 null。

  • 1AC
  • 2USB
  • 3無線
  • 4底座
iOS
沒有,固定 null
Android
底座要 Android 13 以上
temptemperaturenumber可能 null

電池溫度,°C。

iOS
沒有,固定 null
voltvoltageint可能 null

電池電壓,mV。

iOS
沒有,固定 null
Android
照系統的原值送出。部分手機給的是伏特(整數,例如 3、4),請自行判斷
lowPowerbool

低耗電模式(iOS)、省電模式(Android)。

thermalint可能 null

裝置的溫度等級,兩個平台用同一把 1~7 的尺。

  • 1正常
  • 2微熱
  • 3偏熱
  • 4高溫,效能受限
  • 5過熱,需要降溫
  • 6嚴重過熱,部分功能關閉
  • 7即將關機
iOS
只會出現 1、2、4、5
Android
Android 10 以上

netnetwork網路

typeint可能 null

網路類型。沒有連線時是 null。

  • 1Wi-Fi
  • 2行動網路
  • 3有線
  • 4其他
meteredbool可能 null

計量網路,例如行動網路、手機熱點。

lowDatalow data modebool可能 null

開了「低數據模式」。

Android
沒有,固定 null
gengenerationint可能 null

行動網路第幾代,2~5。只有 net.type 是 2 時才有值。

iOS
5G NSA(和 4G 共用基地台的 5G)記成 5
Android
Android 13 以上。5G NSA 系統回報的是 4G,記成 4

rule目前的採集規則

使用者可以存好幾組採集方式,定位中也能切換或修改,所以每一點都附上當下的規則,伺服器可以推算多久該收到一筆。

namestring

採集方式的名稱,只是給人看的。App 內建的幾組會跟著 App 的語言變(例如中文是「精準」、英文是「Precise」),使用者自己取的照原樣。要判斷規則請看 mode、interval、distance、heartbeat,不要用名稱判斷。

modeint

採集依據。

  • 1時間
  • 2距離
intervalint(秒)可能 null

時間模式的間隔。距離模式是 null。

distanceint(公尺)可能 null

距離模式的距離,可以是 0(每收到一筆定位就採集)。時間模式是 null。

heartbeatint(秒)可能 null

距離模式靜止時的強制採集間隔。時間模式或關閉時是 null。

powerint

App 的耗電設定,決定靜止時送出什麼位置。

  • 1耗電:全程最高精度,只有超過 2 分鐘沒有新定位時才送保留的位置
  • 2省電:靜止時少定位(iOS 降低精度、Android 降低頻率),改送保留的位置(pt.held 是 true)

ptpoint這次的定位點

Android 只允許大約位置(perm.precise 是 false)時,系統會把位置模糊化:acc 約 2000 公尺,ellAlt、spd、crs、satUsed、satView 是 null(模擬器實測,實機待確認)。

idint

流水號。安裝後從 1 開始(和 dev.id 一樣不跟著系統備份),每採集一點加 1,停止再開始不會歸零。送出前先寫進手機,App 當掉也不會重複。發送測試固定是 0。

timeint(ms)發送測試時可能 null

這個位置定到的時間,不是送出的時間。GPS 沒更新、或靜止時送保留的位置(pt.held 是 true)時,會和上一點相同。

trigtriggerint

為什麼採集這一點。

  • 1時間到
  • 2移動超過距離
  • 3靜止時的強制採集
  • 4發送測試
latlatitudenumber發送測試時可能 null

WGS84 緯度,小數 7 位。

lnglongitudenumber發送測試時可能 null

WGS84 經度,小數 7 位。

accaccuracynumber可能 null

水平準度,公尺(半徑)。

altaltitudenumber可能 null

海拔(平均海平面),公尺。

Android
由橢球高換算,換算失敗時是 null
ellAltellipsoidal altitudenumber可能 null

WGS84 橢球高,公尺。

altAccaltitude accuracynumber可能 null

高度準度,公尺。

spdspeednumber可能 null

速度,m/s。

spdAccspeed accuracynumber可能 null

速度準度,m/s。

crscoursenumber可能 null

移動方向,度,正北為 0、順時針。是往哪裡走,不是手機朝向。

crsAcccourse accuracynumber可能 null

方向準度,度。

satUsedsatellites usedint可能 null

定位用到的衛星數。Android 關螢幕時也有值。系統把 GPS 關掉之後(實測小米關螢幕約 33 分鐘),送的是最後一次的值,不是 null。

iOS
沒有,固定 null
satViewsatellites in viewint可能 null

看得到的衛星數。Android 關螢幕時也有值。系統把 GPS 關掉之後(實測小米關螢幕約 33 分鐘),送的是最後一次的值,不是 null。

iOS
沒有,固定 null
isSimis simulatedbool可能 null

這個位置是不是模擬或假的定位。實機裝了模擬定位 App 時,dev.isSim 是 false、這裡是 true。

isExtis externalbool可能 null

這個位置是不是來自外接 GPS 或 CarPlay。

Android
沒有,固定 null
heldbool

是不是保留的位置。省電模式(rule.power 是 2)下,手機靜止約 2 分鐘後,App 改送靜止期間最準的一筆,座標與 pt.time 會一直重複,直到再次移動。兩種模式下,超過 2 分鐘沒有新定位(例如手機長時間放著,系統暫停 GPS)時也是 true。

suspectbool

這一點是不是可疑。App 拿它和前面幾點比較:照前面的速度走,再加上最大的加速度,這段時間也走不到這麼遠(兩點的 pt.acc 也算進容許的範圍),就是 true。沒有速度、準度差的粗定位(多半是 Wi-Fi 估的)另外和最後一筆 GPS 定位比,離得比照原速走得到的還遠(至少 100 公尺)也是 true,等 GPS 回來確認;持續偏離 20 秒都沒有 GPS 才當成真的。可疑的點照樣送出,建議畫路線、算距離時略過。看的是速度的變化,不是固定的速度上限,所以開車、高鐵、飛機都不會因為快而被標。萬一被誤標,連續幾點都落在新位置附近時,App 會改用新位置當參考。

接收端常見情境

濾掉發送測試

pt.trig 是 4、pt.id 是 0 的是 App 裡按「發送測試」送的,不是真的移動。

排序與掉點

請求可能亂序到達,依 pt.id 排序。App 不重送,pt.id 不連續就是中間掉了點。

分出每一次定位

同一支手機看 dev.id,同一次定位看 startAt。停止再開始時 startAt 會換,pt.id 繼續往上加。

判斷是不是斷線

時間模式大約每 rule.interval 秒一筆;距離模式靜止時最久每 rule.heartbeat 秒一筆。heartbeat 是 null 時,靜止不動就不會有資料,不代表斷線。使用者可能在定位中切換採集方式,請用最新一點的 rule 推算。最後一點的 pt.held 是 true 時例外,見下一段。

Android 停著不動很久時,點會變少

Android 手機關螢幕、沒插電、放著不動約 30 分鐘以上,系統會讓手機深度睡眠。這段時間 App 只能等系統偶爾醒來時送出,就算 rule.interval 是 3 秒,也可能好幾分鐘才收到一筆(2026-10 小米、OPPO 實測最長約 10 分鐘,其他手機可能更久)。送的是保留的位置,pt.held 是 true。判斷斷線時請看最後收到的那一點:pt.held 是 false,照 rule.interval 判斷;pt.held 是 true,代表手機停著不動,請放寬到至少 15 分鐘,或不要只因為沒收到就判定斷線。手機一移動就會醒來,恢復照間隔送出。

靜止時座標不變

pt.held 是 true 時,座標與 pt.time 和上一點相同是正常的:手機沒在動,App 送的是保留的位置。pt.held 是 false、pt.time 卻很久沒變,才可能是 GPS 收不到訊號。

準度差的點

App 不依準度過濾,準度差的點照樣送。要不要留,看 pt.acc 由伺服器決定。省電模式靜止時,App 用來判斷有沒有移動的粗定位不送出,改送保留的位置(pt.held);粗定位偏很遠時,App 會先等 GPS 確認,不會直接當成出發。另外,iPhone 收不到 GPS 時,系統會用附近的 Wi-Fi 估位置,偶爾會估到幾百公尺到 1 公里外,pt.acc 卻只報幾十公尺(2026-10 實測)。這種和前面比起來跳得不合理的點,App 照送,但 pt.suspect 是 true,畫路線、算距離時建議略過。要自己另外過濾的話,用相鄰兩點的距離除以 pt.time 的差,速度明顯不合理就丟掉;只看 pt.spd 是 null 不夠,iPhone 在室內大部分的點都沒有速度。

假的位置

pt.isSim 是 true 表示這個位置是模擬的;dev.isSim 是 true 表示 App 跑在模擬器上。