know-phone

簡訊驗證碼 API 文件

know-phone 用「反向簡訊」驗證手機號碼:你建立一筆驗證,使用者從那支手機把六位數驗證碼傳簡訊到 0958381426,或撥打同一支號碼按 2 輸入驗證碼。我們收到後簽發一個 ES256 JWT,你用公開的 JWKS 驗簽,就能確定使用者持有這個號碼。

Base URLhttps://phone.know.tw
格式請求與回應皆為 JSON,請帶 Accept: application/json
驗證碼有效期10 分鐘,錯誤 5 次即失效
token 有效期驗證完成後 15 分鐘

完整流程

  1. 你的後端帶 API 金鑰 POST /api/v1/verifications。
  2. 把回傳的 instructions(或 code 與 sms_to)顯示給使用者。手機上可以用 sms:0958381426?body=驗證碼 連結直接開啟簡訊 App。
  3. 前端或後端帶 secret 輪詢 GET /api/v1/verifications/{id},直到 status 變成 verified。
  4. 把 token 交給你的後端,用 JWKS 驗簽並檢查 aud,再把號碼存成已驗證。

認證

建立驗證需要 API 金鑰,以 Bearer token 帶在標頭。金鑰格式為 kp_ 開頭,只會在核發時顯示一次,請存在伺服器端,不要放進前端或 App。

Authorization: Bearer kp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

查詢狀態不需要 API 金鑰,改用每筆驗證專屬的 secret,所以可以讓使用者的瀏覽器或 App 直接輪詢。

需要金鑰請寄信到 [email protected]。

建立驗證

POST /api/v1/verifications

參數型別說明
phonestring要驗證的號碼,接受 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.tw
  • aud 必須是你的 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你建立的驗證完成時送出,只會送給建立它的客戶

錯誤與限制

狀態碼原因
401API 金鑰缺少、錯誤或已停用
404驗證不存在或 secret 錯誤
422參數錯誤,例如號碼格式不正確,詳見 errors
429請求過於頻繁

錯誤回應格式為 {"message": "...", "errors": {...}}。

同一個號碼每小時最多建立 5 筆驗證(跨所有客戶計算),每個客戶另有每小時總量上限,預設 1,000 筆,可來信調整。