{"openapi":"3.1.0","info":{"title":"通知中心 API","version":"1.0.0","description":"聯絡人收集與通知發送。所有端點以 API 金鑰驗證，公司由金鑰決定 —— 即使 body 帶了別家的 companyId 也不會生效。"},"servers":[{"url":"https://noti.allcares.app"}],"security":[{"apiKey":[]}],"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"後台「公司管理 → API 金鑰」產生，格式 nk_xxx。金鑰只在建立當下顯示一次。"}},"schemas":{"Identity":{"type":"object","required":["type","value"],"properties":{"type":{"type":"string","enum":["EMAIL","SMS","LINE","TELEGRAM","SLACK","TEAMS","FCM"],"description":"識別類型"},"value":{"type":"string","description":"識別值。Email 不分大小寫；手機可填 0912345678 或 +886912345678，一律存成 E.164。","example":"dawen@company.com"},"label":{"type":["string","null"],"maxLength":30,"description":"用途標籤，例如「公司信箱」"},"isPrimary":{"type":"boolean","description":"設為該類型的主要聯絡方式；同類型只會保留一個主要"}}},"ChannelType":{"type":"string","enum":["LINE","EMAIL","SMS","TELEGRAM","SLACK","TEAMS","SNS","FCM"]},"ContactInput":{"type":"object","required":["name"],"properties":{"externalId":{"type":["string","null"],"maxLength":100,"description":"呼叫端系統的使用者主鍵（員工編號、會員 ID）。帶了它，重複同步同一個人只會更新而不會新增。","example":"EMP-001"},"name":{"type":"string","minLength":1,"maxLength":60,"example":"陳大文"},"nickname":{"type":["string","null"],"maxLength":60},"note":{"type":["string","null"],"maxLength":2000},"identities":{"type":"array","items":{"type":"object","required":["type","value"],"properties":{"type":{"type":"string","enum":["EMAIL","SMS","LINE","TELEGRAM","SLACK","TEAMS","FCM"],"description":"識別類型"},"value":{"type":"string","description":"識別值。Email 不分大小寫；手機可填 0912345678 或 +886912345678，一律存成 E.164。","example":"dawen@company.com"},"label":{"type":["string","null"],"maxLength":30,"description":"用途標籤，例如「公司信箱」"},"isPrimary":{"type":"boolean","description":"設為該類型的主要聯絡方式；同類型只會保留一個主要"}}},"default":[]},"tags":{"type":"array","items":{"type":"string","maxLength":40},"description":"群組標籤名稱。不帶＝不動既有標籤，帶空陣列＝清空。沒有的標籤會自動建立。"}}},"Contact":{"type":"object","properties":{"id":{"type":"string"},"externalId":{"type":["string","null"]},"name":{"type":"string"},"nickname":{"type":["string","null"]},"note":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"tags":{"type":"array","items":{"type":"string"}},"identities":{"type":"array","items":{"type":"object","required":["type","value"],"properties":{"type":{"type":"string","enum":["EMAIL","SMS","LINE","TELEGRAM","SLACK","TEAMS","FCM"],"description":"識別類型"},"value":{"type":"string","description":"識別值。Email 不分大小寫；手機可填 0912345678 或 +886912345678，一律存成 E.164。","example":"dawen@company.com"},"label":{"type":["string","null"],"maxLength":30,"description":"用途標籤，例如「公司信箱」"},"isPrimary":{"type":"boolean","description":"設為該類型的主要聯絡方式；同類型只會保留一個主要"}}}}}}}},"paths":{"/api/contacts":{"get":{"summary":"查詢聯絡人","description":"可用姓名、暱稱、備註或任一識別值搜尋。","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"關鍵字，會比對姓名／暱稱／備註／識別值"},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"maximum":500}}],"responses":{"200":{"description":"聯絡人清單","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Contact"}}}}}}},"401":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}}}},"post":{"summary":"建立或更新一位聯絡人（upsert）","description":"重複呼叫同一個人不會產生第二筆。認人順序：\n1. 有帶 externalId → 用它找\n2. 沒帶 → 用送進來的任一識別（email／門號…）找\n3. 都找不到 → 建立新的\n\n識別預設為 merge：只補上新的，不會清掉其他來源系統先前寫入的資料。\n若送進來的識別已經屬於「另一個 externalId」的聯絡人，會回 422 並指出衝突對象，\n而不是把對方的資料覆蓋掉。","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ContactInput"},{"type":"object","properties":{"identitiesMode":{"type":"string","enum":["merge","replace"],"default":"merge","description":"replace 會把沒送的識別整批刪掉"}}}]}}}},"responses":{"200":{"description":"已更新既有聯絡人（action = updated）"},"201":{"description":"已建立新聯絡人（action = created）"},"401":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"403":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"422":{"description":"參數驗證失敗，或識別與其他聯絡人衝突","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}}}}},"/api/contacts/bulk":{"post":{"summary":"批次建立或更新聯絡人","description":"一次最多 500 筆。單筆失敗不影響其他筆；HTTP 一律 200，逐筆結果在 results 裡。","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["contacts"],"properties":{"identitiesMode":{"type":"string","enum":["merge","replace"]},"contacts":{"type":"array","minItems":1,"maxItems":500,"items":{"$ref":"#/components/schemas/ContactInput"}}}}}}},"responses":{"200":{"description":"逐筆結果","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"summary":{"type":"object","properties":{"total":{"type":"integer"},"created":{"type":"integer"},"updated":{"type":"integer"},"failed":{"type":"integer"}}},"results":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer"},"externalId":{"type":["string","null"]},"name":{"type":"string"},"status":{"type":"string","enum":["created","updated","failed"]},"contactId":{"type":"string"},"error":{"type":"string"}}}}}}}}}}},"401":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"422":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}}}}},"/api/contacts/export":{"get":{"summary":"匯出聯絡人 Excel","description":"整家公司的聯絡人，一列一個人、每種識別開多欄。回傳 .xlsx 二進位內容；這份檔案同時就是匯入用的範本。","parameters":[{"name":"companyId","in":"query","schema":{"type":"string"},"description":"用後台 session 呼叫時指定公司；用金鑰呼叫時由金鑰決定"},{"name":"companyCode","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Excel 活頁簿","content":{"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"403":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}}}}},"/api/contacts/import":{"post":{"summary":"匯入聯絡人 Excel","description":"上傳 .xlsx，逐列建立或更新（externalId 認人，識別合併）。單列失敗不影響其他列，HTTP 一律 200；有失敗時附上只含那幾列的 errorFileBase64。","parameters":[{"name":"companyId","in":"query","schema":{"type":"string"}},{"name":"companyCode","in":"query","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"逐列結果","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"summary":{"type":"object","properties":{"total":{"type":"integer"},"created":{"type":"integer"},"updated":{"type":"integer"},"failed":{"type":"integer"}}},"unknownColumns":{"type":"array","items":{"type":"string"},"description":"認不出來、已被忽略的欄位標題"},"results":{"type":"array","items":{"type":"object","properties":{"row":{"type":"integer","description":"Excel 上看到的列號（1 是標題列）"},"externalId":{"type":["string","null"]},"name":{"type":"string"},"status":{"type":"string","enum":["created","updated","failed"]},"contactId":{"type":"string"},"error":{"type":"string"}}}},"errorFileBase64":{"type":"string","description":"只含失敗列的 .xlsx（base64），欄位同原檔並多一欄「錯誤原因」"}}}}}}}},"401":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"403":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"413":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"422":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}}}}},"/api/schedules":{"get":{"summary":"列出預約發送","description":"含每個預約的下一次執行時間，以及最近幾班的結果。","parameters":[{"name":"companyId","in":"query","schema":{"type":"string"}},{"name":"companyCode","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"預約清單"},"401":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"403":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}}}},"post":{"summary":"建立預約發送","description":"單次（ONCE）、固定週期（RECURRING）或 cron（CRON）。到點由排程心跳觸發；設定 ratePerMinute 可以分批節流，避免一次打爆供應商。","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","kind","targetKind","body"],"properties":{"companyId":{"type":"string"},"companyCode":{"type":"string"},"name":{"type":"string","description":"清單上顯示的名字"},"kind":{"type":"string","enum":["ONCE","RECURRING","CRON"]},"runAt":{"type":"string","format":"date-time","description":"ONCE 用。必須是未來的時間"},"recurrence":{"type":"object","description":"RECURRING 用。freq＝daily／weekly／monthly／monthlyLast，weekly 帶 weekdays（0＝週日），monthly 帶 days"},"cron":{"type":"string","description":"CRON 用。五欄位，例如 0 9 * * 1"},"timeZone":{"type":"string","default":"Asia/Taipei"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"},"channel":{"$ref":"#/components/schemas/ChannelType"},"channelConfigId":{"type":"string"},"targetKind":{"type":"string","enum":["TAGS","TARGETS","RETRY_FAILED"]},"tags":{"type":"array","items":{"type":"string"},"description":"群組名稱；打錯會回 404，不會自動建立"},"tagIds":{"type":"array","items":{"type":"string"}},"tagMatch":{"type":"string","enum":["any","all"]},"targets":{"type":"array","maxItems":1000,"items":{"type":"object","properties":{"channel":{"$ref":"#/components/schemas/ChannelType"},"recipient":{"type":"string"}}}},"retryOfRunId":{"type":"string","description":"RETRY_FAILED 用：要重送哪一班的失敗名單"},"subject":{"type":"string"},"body":{"type":"string"},"ratePerMinute":{"type":"integer","minimum":1,"description":"每分鐘最多送幾筆；不給＝到點一次全部送出"}}}}}},"responses":{"201":{"description":"建立成功","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"},"nextRunAt":{"type":"string","format":"date-time"},"warnings":{"type":"array","items":{"type":"string"},"description":"不擋下來、但現在就該知道的事（例如「每月 31 號」有幾個月不會執行）"}}}}}}}},"401":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"403":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"404":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"422":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}}}}},"/api/schedules/{id}":{"get":{"summary":"單一預約（含最近幾班的執行結果）","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"預約內容與班次"},"401":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"403":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"404":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}}}},"patch":{"summary":"暫停或恢復預約","description":"恢復時會重算下一次執行時間 —— 沿用暫停前的時間會讓恢復的那一刻補送一批舊通知。","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["ACTIVE","PAUSED","CANCELLED"]}}}}}},"responses":{"200":{"description":"更新後的狀態"},"401":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"403":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"404":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"422":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}}}},"delete":{"summary":"取消預約","description":"取消而不是刪除：已經送出的記錄掛在這個預約上，刪掉會讓那些發送記錄失去來源。","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"已取消"},"401":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"403":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"404":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}}}}},"/api/notifications/send":{"post":{"summary":"發送通知","description":"可一次指定多個通道與收件者。個別通道送不出去時 HTTP 仍為 200，每一筆的 status 與 error 在 results 裡 —— 這樣反向代理不會把錯誤細節換成自己的錯誤頁。","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["body"],"properties":{"targets":{"type":"array","items":{"type":"object","required":["channel"],"properties":{"channel":{"type":"string","enum":["LINE","EMAIL","SMS","TELEGRAM","SLACK","TEAMS","SNS","FCM"]},"recipient":{"type":"string","description":"留空時使用該通道設定的預設對象（例如 Slack 的預設頻道、SMS 的預設收件門號）"},"channelConfigId":{"type":["string","null"],"description":"不指定就用該通道的預設設定"}}}},"subject":{"type":["string","null"],"maxLength":200},"body":{"type":"string","minLength":1},"requestId":{"type":["string","null"],"maxLength":100,"description":"冪等鍵。同一家公司重送同一個值不會重複發送，適合放呼叫端自己的事件 ID。"}}}}}},"responses":{"200":{"description":"逐通道發送結果"},"401":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}},"422":{"description":"失敗","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"details":{}}}}}}}}}}}