簡訊驗證碼 API 文件
know-phone 用「反向簡訊」驗證手機號碼:你建立一筆驗證,使用者從那支手機把六位數驗證碼傳簡訊到 0958381426,或撥打同一支號碼按 2 輸入驗證碼。我們收到後簽發一個 ES256 JWT,你用公開的 JWKS 驗簽,就能確定使用者持有這個號碼。
| Base URL | https://phone.know.tw |
|---|---|
| 格式 | 請求與回應皆為 JSON,請帶 Accept: application/json |
| 驗證碼有效期 | 10 分鐘,錯誤 5 次即失效 |
| token 有效期 | 驗證完成後 15 分鐘 |
完整流程
- 你的後端帶 API 金鑰
POST /api/v1/verifications。 - 把回傳的
instructions(或code與sms_to)顯示給使用者。手機上可以用sms:0958381426?body=驗證碼連結直接開啟簡訊 App。 - 前端或後端帶
secret輪詢GET /api/v1/verifications/{id},直到status變成verified。 - 把
token交給你的後端,用 JWKS 驗簽並檢查aud,再把號碼存成已驗證。
認證
建立驗證需要 API 金鑰,以 Bearer token 帶在標頭。金鑰格式為 kp_ 開頭,只會在核發時顯示一次,請存在伺服器端,不要放進前端或 App。
Authorization: Bearer kp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
查詢狀態不需要 API 金鑰,改用每筆驗證專屬的 secret,所以可以讓使用者的瀏覽器或 App 直接輪詢。
需要金鑰請寄信到 [email protected]。
建立驗證
POST /api/v1/verifications
| 參數 | 型別 | 說明 |
|---|---|---|
phone | string | 要驗證的號碼,接受 0912345678 或 +886912345678 |
curl -X POST https://phone.know.tw/api/v1/verifications \ -H "Authorization: Bearer $KNOW_PHONE_API_KEY" \ -H "Accept: application/json" \ -d phone=0912345678
回應 201
{
"id": "01JD7Q4X2Y8ZK3N5R6S7T8V9W0",
"phone": "+886912345678",
"status": "pending",
"code": "482913",
"secret": "b1f0c2...",
"expires_at": "2026-10-30T12:10:00+08:00",
"sms_to": "0958381426",
"call_to": "0958381426",
"instructions": "請用 0912345678 傳送簡訊「482913」到 0958381426,或用這支手機撥打 0958381426,按 2 後輸入 482913。驗證碼 10 分鐘內有效。"
}
使用者必須從 phone 這支號碼發出簡訊或來電,換別支手機傳同樣的驗證碼不會通過。
查詢狀態
GET /api/v1/verifications/{id},標頭帶 X-Verification-Secret。建議每 2 到 3 秒輪詢一次。
curl https://phone.know.tw/api/v1/verifications/01JD7Q4X2Y8ZK3N5R6S7T8V9W0 \ -H "X-Verification-Secret: b1f0c2..." \ -H "Accept: application/json"
回應 200
{
"id": "01JD7Q4X2Y8ZK3N5R6S7T8V9W0",
"phone": "+886912345678",
"status": "verified",
"verified_via": "sms",
"verified_at": "2026-10-30T12:03:41+08:00",
"token": "eyJhbGciOiJFUzI1NiIsImtpZCI6Ii4uLiJ9..."
}
| status | 意義 |
|---|---|
pending | 等待使用者傳簡訊或來電 |
verified | 驗證成功,token 有值(過了 token 有效期後回 null) |
failed | 輸入錯誤次數過多 |
expired | 超過有效期仍未驗證,請重新建立 |
verified_via 為 sms 或 call。id 或 secret 錯誤一律回 404。
驗證 token
token 是 ES256 簽章的 JWT。不要只相信前端回報的狀態,請在後端驗簽:
- 公鑰:
https://phone.know.tw/.well-known/jwks.json,依kid選鑰,可快取並在遇到未知kid時重抓 iss必須是https://phone.know.twaud必須是你的 audience(核發金鑰時告知)jti是驗證 id,建議記錄下來避免同一個 token 被重複使用
Claims
{
"iss": "https://phone.know.tw",
"sub": "+886912345678",
"aud": "your-audience",
"iat": 1793333021,
"exp": 1793333921,
"jti": "01JD7Q4X2Y8ZK3N5R6S7T8V9W0",
"phone_number": "+886912345678",
"phone_number_verified": true,
"verification_method": "sms",
"verified_at": 1793333021
}
Node.js(jose)
import { createRemoteJWKSet, jwtVerify } from 'jose';
const jwks = createRemoteJWKSet(new URL('https://phone.know.tw/.well-known/jwks.json'));
const { payload } = await jwtVerify(token, jwks, {
issuer: 'https://phone.know.tw',
audience: 'your-audience',
algorithms: ['ES256'],
});
// payload.phone_number === '+886912345678'
PHP(firebase/php-jwt)
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
$keys = JWK::parseKeySet(json_decode(file_get_contents('https://phone.know.tw/.well-known/jwks.json'), true), 'ES256');
$claims = JWT::decode($token, $keys);
if ($claims->iss !== 'https://phone.know.tw' || $claims->aud !== 'your-audience') {
throw new RuntimeException('token 不是發給我們的');
}
Webhook
若申請時提供 webhook 網址,驗證完成時我們會 POST 事件過去,不必一直輪詢。回應任何 2xx 視為成功;其他狀態或逾時(10 秒)會在 10 秒、30 秒、1 分、5 分、15 分、30 分、1 小時後重試,共 8 次。
POST /your/webhook
Content-Type: application/json
X-KnowPhone-Event: verification.completed
X-KnowPhone-Signature: t=1793333021,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
{
"id": "evt_01JD7Q5A...",
"type": "verification.completed",
"created_at": "2026-10-30T12:03:41+08:00",
"data": {
"id": "01JD7Q4X2Y8ZK3N5R6S7T8V9W0",
"phone": "+886912345678",
"verified_via": "sms",
"verified_at": "2026-10-30T12:03:41+08:00"
}
}
同一事件重試時 id 不變,請以它去重。
驗證簽章
簽章為 HMAC-SHA256(webhook 密鑰, "{t}.{原始 body}") 的十六進位字串。請用收到的原始 body 計算(不要先 parse 再序列化),以常數時間比較,並拒絕 t 與現在相差超過 5 分鐘的請求。
$header = $request->header('X-KnowPhone-Signature');
parse_str(str_replace(',', '&', $header), $parts);
$expected = hash_hmac('sha256', $parts['t'].'.'.$request->getContent(), $webhookSecret);
$valid = abs(time() - (int) $parts['t']) <= 300
&& hash_equals($expected, $parts['v1'] ?? '');
import crypto from 'node:crypto';
const { t, v1 } = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const valid = Math.abs(Date.now() / 1000 - Number(t)) <= 300
&& crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1 ?? ''));
事件
| 事件 | 說明 |
|---|---|
verification.completed | 你建立的驗證完成時送出,只會送給建立它的客戶 |
錯誤與限制
| 狀態碼 | 原因 |
|---|---|
401 | API 金鑰缺少、錯誤或已停用 |
404 | 驗證不存在或 secret 錯誤 |
422 | 參數錯誤,例如號碼格式不正確,詳見 errors |
429 | 請求過於頻繁 |
錯誤回應格式為 {"message": "...", "errors": {...}}。
同一個號碼每小時最多建立 5 筆驗證(跨所有客戶計算),每個客戶另有每小時總量上限,預設 1,000 筆,可來信調整。