Workat 電子發票 API

透過 API 開立財政部電子發票(MIG 4.1 平台存證),由系統負責取號、產生 XML、交給 Turnkey 上傳,並回報平台的處理結果。

API 列表

連線位址

https://invoice.workat.studio/api/v1

驗證方式

每個請求都必須以 HMAC-SHA256 簽章驗證身分。系統管理者會發給你兩樣東西:

項目例說明
Key ID(公開)wk_k3j9x2m7q1p8r4t6v0w5每個請求放在 X-Api-Key Header
Secret(私密)48 碼英數字只用來計算簽章,永遠不要放進請求裡。只會在建立時顯示一次
Secret 等同於開立發票的權限,請只放在伺服器端,不要寫進網頁前端、手機 App 或程式碼倉庫。懷疑外洩時請立即聯絡系統管理者停用並換發新金鑰。

每個請求必須帶的 Header

Header內容
X-Api-KeyKey ID
X-Timestamp目前的 Unix 時間(秒)。與伺服器時間相差超過 300 秒即拒絕,請確認伺服器有校時(NTP)
X-Nonce每個請求都不同的亂數,16–64 碼英數字(可含 _、-)。用過就不能再用
X-Signature簽章(64 碼小寫十六進位),算法見下方
Content-Typeapplication/json
Acceptapplication/json

簽章算法

把以下 5 個值用換行字元 \n 串起來,以 Secret 為金鑰計算 HMAC-SHA256,輸出小寫十六進位:

StringToSign = X-Timestamp + "\n"
             + X-Nonce     + "\n"
             + HTTP 方法(大寫,例 POST) + "\n"
             + 路徑(含查詢字串,例 /api/v1/invoices) + "\n"
             + SHA256(請求內容)(小寫十六進位;GET 沒有內容時為空字串的 SHA256)

X-Signature  = HMAC-SHA256(Secret, StringToSign)   → 小寫十六進位

測試向量

寫好簽章程式後,先用這組固定值驗證結果是否一致:

Secret0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL
X-Timestamp1791119073
X-Nonce3f9c1e7a2b8d4c6e9f0a1b2c3d4e5f60
方法、路徑POST、/api/v1/invoices
請求內容{"items":[{"description":"綠茶","quantity":1,"unit_price":25}]}(UTF-8,無空白)
SHA256(內容)eaf495d6aa88357a628b3d8ef0dfea11d00c5dd50d593af06568dc0dfaefad1c
X-Signaturebad751f03cef7b620b58a34f6acccec89ba25d8390dc617033c40a538b41d3e3

範例:PHP

function einvoiceRequest(string $method, string $path, ?array $data): array
{
    $keyId  = getenv('EINVOICE_KEY_ID');
    $secret = getenv('EINVOICE_SECRET');

    $body      = $data === null ? '' : json_encode($data, JSON_UNESCAPED_UNICODE);
    $timestamp = (string) time();
    $nonce     = bin2hex(random_bytes(16));
    $toSign    = implode("\n", [$timestamp, $nonce, strtoupper($method), $path, hash('sha256', $body)]);

    $ch = curl_init('https://invoice.workat.studio' . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => strtoupper($method),
        CURLOPT_POSTFIELDS     => $body === '' ? null : $body,   // 送出的就是簽章用的同一個字串
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json',
            'Accept: application/json',
            "X-Api-Key: {$keyId}",
            "X-Timestamp: {$timestamp}",
            "X-Nonce: {$nonce}",
            'X-Signature: ' . hash_hmac('sha256', $toSign, $secret),
        ],
    ]);
    $response = curl_exec($ch);
    $status   = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);

    return [$status, json_decode($response, true)];
}

[$status, $result] = einvoiceRequest('POST', '/api/v1/invoices', [
    'external_id' => 'ORDER-20261004-0001',
    'items'       => [['description' => '綠茶', 'quantity' => 1, 'unit_price' => 25]],
]);

範例:bash(curl + openssl)

KEY_ID='wk_…'; SECRET='…'
BODY='{"items":[{"description":"綠茶","quantity":1,"unit_price":25}]}'
TS=$(date +%s)
NONCE=$(openssl rand -hex 16)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.* //')
SIG=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" "$NONCE" POST /api/v1/invoices "$BODY_HASH" \
      | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')

curl -X POST https://invoice.workat.studio/api/v1/invoices \
  -H "Content-Type: application/json" -H "Accept: application/json" \
  -H "X-Api-Key: $KEY_ID" -H "X-Timestamp: $TS" -H "X-Nonce: $NONCE" -H "X-Signature: $SIG" \
  --data-binary "$BODY"

回應格式

成功時,資料放在 data 底下:

{
  "data": {
    "invoice_number": "KZ10000000",
    "status": "sent",
    ...
  }
}

錯誤回應

HTTP 狀態碼意義處理方式
401驗證失敗,原因在 error(見下表)依原因修正
403來源 IP 不在金鑰的允許清單(ip_not_allowed)確認伺服器的對外 IP,請系統管理者更新清單
429請求過於頻繁(rate_limited)稍後再試
404找不到資料(例如查詢不存在、或不屬於你公司的發票)確認發票號碼
409沒有可用的字軌(當期字軌尚未匯入,或號碼已用完)聯絡系統管理者匯入字軌後再重送
422輸入資料不符合規則依 errors 中各欄位的訊息修正後重送
500系統錯誤可用相同的 external_id 安全重送(見下方「防止重複開立」)

驗證失敗時的回應格式與 error 代碼:

{
  "message": "簽章不正確",
  "error": "invalid_signature"
}
error原因
missing_headers缺少 X-Api-Key、X-Timestamp、X-Nonce、X-Signature 其中之一
invalid_keyKey ID 不存在、金鑰已停用,或公司已停用
ip_not_allowed來源 IP 不在允許清單(HTTP 403)
timestamp_out_of_range時間戳格式錯誤,或與伺服器時間相差太多
invalid_nonceNonce 格式錯誤
invalid_signature簽章不符。常見原因:Secret 錯誤、送出的內容與簽章時不同、路徑未含查詢字串
nonce_reusedNonce 已用過(請求被重送)

422 的回應格式:

{
  "message": "買方統一編號檢查碼錯誤",
  "errors": {
    "buyer_ban": ["買方統一編號檢查碼錯誤"]
  }
}

防止重複開立

開立發票時建議帶上 external_id(例如你系統裡的訂單編號)。同一間公司用相同的 external_id 再次呼叫時,不會開出第二張發票,而是回傳第一次開立的那張(HTTP 200,第一次為 201)。

因此遇到逾時、網路中斷或 500 時,可以放心用同一個 external_id 重送。

發票狀態

發票開立後,會依序經過以下狀態。status 會隨著上傳進度更新,可透過查詢發票取得最新狀態。

status意義
issued已開立(已取號並存檔),尚未交給 Turnkey。通常幾秒內就會變成 sent;若 Turnkey 暫時無法寫入,系統每分鐘自動重試。
sent已交給 Turnkey,等待財政部平台回覆處理結果。
uploaded財政部平台已存證成功,流程完成。
failedTurnkey 或財政部平台回報錯誤,原因在 upload_error。需要由系統管理者處理。
cancelling註銷重開進行中:內容已改為新的,等財政部確認註銷後,系統會自動以同一個號碼重新開立,再回到 sent → uploaded。
發票在 issued 時就已經合法開立、號碼已確定,可以先交付給消費者;sent → uploaded 只是上傳財政部的進度。

發票被作廢後,status 不會改變(仍表示開立的上傳進度),作廢資訊與作廢的上傳進度放在 void 欄位;未作廢時 void 為 null。

欄位長度與格式

各欄位的長度上限依財政部 MIG 4.1 規格,詳見各 API 頁面。金額一律為新台幣。