1.先選接入方式,再選資料模型
| 要完成的工作 | 使用哪套端點 | 保存方式 |
|---|---|---|
| 需要草稿、審核補件及兩種 UI 共用同一流程 | workflows/workflow-runs | OpenDashboard 保存申請和版本,外站 UI 與私人流程頁操作同一 run,見多步流程教程。 |
| 一份普通表單,提交後可修改自己的答案 | forms/responses | 每位 respondent 每份表單保存一份最新正式回覆。 |
| 隊伍報名,再分階段保存草稿及提交作品 | competitions/entries/submissions | 一個 entry 連接報名、隊員及各階段的草稿/提交版本。 |
普通 responses API 沒有草稿、競賽期限或前置階段規則。競賽生成的報名/階段表單必須使用競賽端點;把它送到普通 responses 會得到 409 competition_form_use_entry_api。本頁第 2–7 節示範普通表單,第 8 節說明競賽的接法。
先由管理員建立連接應用,保存伺服器端金鑰及表單 id。forms API 只讀取同一應用擁有的表單;不能把一般管理後台建立的任意表單 id 當作應用自己的表單。取得金鑰及建立定義。
2.由後端取得表單 JSON
表單已建立時,使用它的 id 讀取定義:
curl --fail-with-body "$OPENDASHBOARD_BASE_URL/api/apps/v1/forms/$FORM_ID" \
-H "Authorization: Bearer $OPENDASHBOARD_TEST_KEY"回傳是表單物件本身,沒有外層 form 包裝。下方是節錄的合法 JSON 結構;實際 questions 包含該表單所有題目。
{
"id": "evt_demo_form_ui",
"title": "Synthetic workshop signup",
"description": "Only synthetic data is used in this example.",
"test": true,
"created_at": "2026-01-01T00:00:00.000Z",
"questions": [
{ "key": "program", "title": "課程", "type": "short_text", "required": true, "personal": false },
{ "key": "session", "title": "場次", "type": "dropdown", "options": ["Demo A", "Demo B"], "required": true, "personal": false },
{ "key": "interest", "title": "興趣程度", "type": "number", "min": 0, "max": 5, "integer": true, "unit": "", "required": true, "personal": false },
{
"key": "participants", "title": "參加者", "type": "repeating_group",
"min": 1, "max": 4, "required": false, "personal": false,
"fields": [
{ "key": "participant_ref", "label": "匿名參加者編號", "type": "text", "required": true, "personal": false },
{ "key": "category", "label": "類別", "type": "single_choice", "options": ["Demo beginner", "Demo advanced"], "required": true, "personal": false }
]
}
]
}下載完整建立表單 JSON。它是 POST /forms 的正文,不是某個已存在表單的 id。先在測試環境執行,保存回傳 id,再 GET 取得補齊預設值的完整定義:
curl --fail-with-body "$OPENDASHBOARD_BASE_URL/api/apps/v1/forms" \
-H "Authorization: Bearer $OPENDASHBOARD_TEST_KEY" \
-H 'Content-Type: application/json' \
--data-binary @custom-ui-form.json成功回傳 201 { "id": "evt_…" }。首次建立不是冪等操作;如果結果未知,先查核建立結果,不要盲目再次建立。
3.把定義對應到自己的 UI
| JSON 設定 | 你的 UI 要怎樣使用 |
|---|---|
| key | 答案的固定欄位名。用 program 傳課程答案,不用中文標題「課程」。 |
| title/label | 題目顯示 title;群組子欄位顯示 label,沒有時用 title/key。 |
| required | 標示必填並提示缺漏。群組 min 大於 0 也要求項目,即使 required 是 false。 |
| type | 決定輸入控制及送出的 JSON 型別,見下表。 |
| options | 選項值必須原樣送回;顯示文字相同也不能自行改成數字索引。 |
| min/max | number 是數值範圍;repeating_group 是項目數量。兩者不能混用。 |
| integer/unit | number 的整數要求及顯示單位。群組數值欄位的規則可能在 number 物件,讀 field.number 或 field 本身。 |
| fields | 群組每個項目/結果物件的子欄位。子欄位仍按自己的 key 收集。 |
| personal | 資料能否進入報表的分類;不表示瀏覽器可以把欄位公開給所有訪客。 |
| type | 可自行選擇的 UI | answers 中的值 |
|---|---|---|
| short_text;群組內 text | 單行文字 | 字串,最多 500 字。 |
| paragraph | 多行文字 | 字串,最多 5000 字。 |
| number | 數值輸入 | JSON 數字;保留 0,未填留空或省略,不能把空值轉成 0。 |
| date | 日期輸入 | YYYY-MM-DD 字串。 |
| single_choice/dropdown | 單選按鈕/下拉 | 一個 options 字串。 |
| multiple_choice | 多選核取方塊 | options 字串陣列,沒有選擇用 [];必填時至少一個。 |
| repeating_group | 新增/移除項目的區塊或小表格 | 物件陣列,每個物件按 fields 的 key 保存。 |
| group,限群組子欄位 | 一組固定子欄位 | 一個物件,使用子欄位 key。 |
| labels,限群組/app_result 子欄位 | 多個標籤 | 字串陣列;完整限制見 API 參考。 |
| section/text_block | 標題/說明 | 不傳答案。 |
| hidden | 由網站後端設定 | 文字;不能讓訪客任意指定身份或來源值。 |
| app_result | 顯示應用計算的結果 | fields 定義的物件;成績/計算結果由可信後端產生。 |
| email/phone | 電郵/電話輸入 | 字串,需 identified mode;永遠屬個人資料。 |
| file_upload | 你的附件選擇及上傳流程 | HTTPS URL 字串,需 identified mode;不能直接傳 File、Base64 或 upload id。 |
這個 JSON 不是標準 JSON Schema,也沒有內建的版面、CSS 或 React 元件。可直接按 questions 生成控制,或設計固定頁面再對應 key。題目順序沿用 questions;unknown key 會被拒絕。表單標題、選項、說明及錯誤使用文字方式渲染,避免把內容直接插入 HTML。
hidden、app_result 及其答案只由可信後端設定或核對;只把訪客需要填寫及查看的定義/答案交給瀏覽器。對含 hidden 子欄位的群組也做相同處理,不要只隱藏最外層題目。
4.回傳答案的 JSON
下載完整提交答案 JSON。此檔供伺服器端測試,含由後端設定的 respondent 和 hidden 值;實際訪客瀏覽器不應自行選擇這兩項。
{
"respondent": "demo-contact-ui",
"answers": {
"program": "Synthetic workshop",
"session": "Demo A",
"interest": 0,
"preferred_date": "2026-10-15",
"topics": ["Demo X", "Demo Y"],
"participants": [
{ "participant_ref": "demo-person-a", "category": "Demo beginner" },
{ "participant_ref": "demo-person-b", "category": "Demo advanced" }
],
"notes": "Synthetic example only.",
"portal_source": "demo-portal"
},
"submitted_at": "2026-01-01T00:00:00Z",
"replace": true,
"expected_version": 0
}| 正文字段 | 誰提供/用途 |
|---|---|
| respondent | 網站後端從已驗證身份或存取憑證取得的穩定匿名編號,4–64 個英文字、數字、_ 或 -。每次更新沿用;不用姓名或電郵。 |
| answers | 按題目 key 收集的完整答案。array/object 保留結構,不能轉成一個 JSON 文字字串。 |
| submitted_at | 這次操作固定的 ISO 時間。範例時間是合成值;真實新操作記錄當下時間,重試保留原值。 |
| replace | true 表示可取代已有回覆。更新送完整答案,省略的舊答案會被移除。 |
| expected_version | 首次建立用 0;更新用讀回的目前版本。用來拒絕過時修改。 |
curl --fail-with-body "$OPENDASHBOARD_BASE_URL/api/apps/v1/forms/$FORM_ID/responses" \
-H "Authorization: Bearer $OPENDASHBOARD_TEST_KEY" \
-H 'Content-Type: application/json' \
--data-binary @custom-ui-response.json首次保存及新的取代操作成功回傳 201 { "id": "reg_…", "version": 1 };更新後 version 增加。相同答案、submitted_at 及原 expected_version 的精確重試回傳 200 和原 id/version。
以上合成表單只有欄位型別及範圍驗證。額外業務規則,例如不同欄位之間的條件關係,需由你的可信後端處理或使用對應流程 API,不能只寫在瀏覽器。
5.瀏覽器和網站後端如何分工
使用者填寫 → 你的網站後端確認存取權、加入可信資料 → OpenDashboard 保存 → 你的網站顯示成功及 version。
可以在你自己的網站提供以下兩個介面;這些是網站自行實作的路徑,不是 OpenDashboard 已提供的端點:
| 自己網站的路徑例子 | 返回/接收甚麼 |
|---|---|
| GET /api/my-form | 可填寫的定義、目前使用者獲准查看的答案、目前 version;沒有回覆時 version 為 0。 |
| POST /api/my-form | 接收 answers、expected_version、submitted_at;後端核對身份並加入 respondent、可信 hidden/app_result,轉送 OpenDashboard。 |
網站後端固定允許使用的 FORM_ID;不得接受訪客傳入任意 form id、respondent、OpenDashboard URL 或金鑰。每次讀寫核對登入/私人存取憑證與該回覆的關係,保留原有 CSRF 防護及請求大小限制。應用金鑰識別網站,不會替你識別訪客。
下方是瀏覽器的提交參考。答案由自己的 UI 收集;csrfToken 由網站提供,後端也必須驗證。沒有把 OpenDashboard 金鑰或 respondent 放入前端。提交期間停用按鈕,保留答案;同一次失敗重試沿用同一 submittedAt 和 version。
async function sendMyAnswers({ answers, version, submittedAt, csrfToken }) {
const response = await fetch('/api/my-form', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
answers,
expected_version: version,
submitted_at: submittedAt
})
});
const body = await response.json();
if (!response.ok) {
return { saved: false, status: response.status, error: body.error,
fields: body.fields || {}, message: body.message };
}
return { saved: true, id: body.id, version: body.version };
}自己的後端只有收到 OpenDashboard 保存成功才返回成功。網路錯誤/非 JSON 回應時,前端保留輸入及這次操作的時間/版本,提供查核或重試,不顯示已保存。
UI 可以在送出前檢查必填及格式,OpenDashboard 仍會在伺服器再驗證;讀取表單 JSON 不等於獲得其他人的答案。
6.修改及讀回自己的資料
後端用 GET /forms/$FORM_ID/responses?respondent=$RESPONDENT 讀取已授權的那一隊答案;不存在時 responses 是空陣列。不加 respondent 才回傳全體回覆,目前沒有 cursor 分頁。參數由已核對身份的後端提供,不能接受瀏覽器任意指定另一隊。
// Only run on the website server after access.respondent has been authorized.
const response = await fetch(
process.env.OPENDASHBOARD_BASE_URL + '/api/apps/v1/forms/' +
encodeURIComponent(process.env.OPENDASHBOARD_FORM_ID) +
'/responses?respondent=' + encodeURIComponent(access.respondent),
{ headers: { Authorization: 'Bearer ' + process.env.OPENDASHBOARD_APP_KEY } }
);
if (!response.ok) throw new Error('Unable to read the current response');
const payload = await response.json();
const own = payload.responses.find(item => item.respondent === access.respondent);
// Filter hidden/application-owned values before returning permitted answers.
const currentVersion = own ? own.version : 0;access 由你自己的後端登入/存取驗證取得,不從請求正文或網址直接讀 respondent。這段是伺服器參考片段,需接到網站既有的存取檢查及錯誤處理。
修改前載入目前答案和 version。送完整新答案並以該版本作 expected_version;成功後採用返回的新版本。收到 response_changed 時,保存使用者未送出的內容,再讀最新資料讓他核對,不能只提高版本後自動覆寫。
普通表單更新只保存最新回覆。若需保留每次事件原文,可另外使用 append-only records;競賽草稿及作品修訂使用第 8 節的專用端點。
7.驗證錯誤及附件
以下錯誤會拒絕整次寫入:
{
"ok": false,
"error": "invalid_answers",
"message": "The response was not saved. Fix these and send it again.",
"fields": {
"participants.0.category": "Unknown choice."
}
}這個路徑對應第 1 個參加者的 category;序號由 0 開始。保留伺服器回傳的完整路徑,對應到自己的欄位,顯示文字錯誤並把焦點移到需修正的位置。message/fields 的文字可能是英文,網站可依 error code 顯示本地語言提示;不要只依英文句子判斷重試。
| 狀態/錯誤 | UI 處理 |
|---|---|
| 400 invalid_answers | 顯示 fields,保留答案,修正後重新提交。 |
| 401 invalid_app_key | 後端處理金鑰/應用設定;不能向訪客要求金鑰。 |
| 404 form_not_found | 核對固定表單 id、所屬應用及測試/正式模式。 |
| 409 form_closed/already_submitted | 顯示目前不能提交的原因;已有回覆需先讀取版本。 |
| 409 response_changed | 核對最新答案與自己的修改,不強行覆寫。 |
| 413 request_body_too_large | 縮減請求;JSON 上限 256 KB,檔案不放 JSON。 |
| 429 rate_limited | 後端按 Retry-After 等待,有限次重試。 |
| 5xx/逾時/網路中斷 | 保留操作內容,查核未知結果,再依冪等規則重試。 |
file_upload 的 HTTPS 字串只保存一個檔案網址,不會自動備份檔案,也不是通用上傳元件。要由 OpenDashboard 保存私人 bytes,使用私人 uploads API,完成檔案驗證後保存固定 upload id 的關聯,讀取時由已授權後端取得短效下載 URL。upload id 目前不能直接當作普通 file_upload 答案;可在受控的 hidden 欄位或私人 records 中保存關聯。
不要把短效下載 URL 當作永久附件識別,也不要把一般管理表單的檔案 metadata 物件直接搬到 Connected Apps API。兩者格式不同。瀏覽器使用簽名上傳 URL 時,不帶應用金鑰。
8.自己的 UI 接競賽及階段提交
競賽仍可使用自己的頁面,但讀寫 entry/submission,不能用普通 responses。所有路徑仍以 /api/apps/v1 開始,由你的網站後端使用應用金鑰呼叫。
| 操作 | 端點及正文 |
|---|---|
| 取得報名欄位、隊員及階段 | GET /competitions/:id;讀 entry.fields、entry.members.fields 和 stages 的 questions/window/prerequisites。 |
| 建立隊伍草稿 | POST /competitions/:id/entries;送 respondent、division、answers、members。 |
| 讀回隊伍及各階段狀態 | GET /entries/:id。 |
| 修改隊伍資料 | PUT /entries/:id;送 division、完整 answers、完整 members、上次 revision。 |
| 正式提交報名 | POST /entries/:id/submit;送 revision。 |
| 讀回某階段草稿/作品 | GET /entries/:id/submissions/:stage_key。 |
| 儲存階段草稿 | PUT /entries/:id/submissions/:stage_key;送 answers、revision,首次用 0。 |
| 正式提交作品 | POST /entries/:id/submissions/:stage_key/submit;送剛保存後的 revision。 |
| 改用 OpenDashboard 私人填寫頁 | POST /entries/:id/links;取得 url 後私下交給該隊聯絡人,可設定 expires_at。 |
建立隊伍草稿的正文例子:
{
"respondent": "demo-contact-ui",
"division": "demo",
"answers": { "team_name": "Synthetic team" },
"members": [
{ "alias": "demo-person-a" },
{ "alias": "demo-person-b" }
]
}division、team_name、alias 必須先在該競賽定義宣告,不是可以套在任意競賽的固定欄位。報名 UI 顯示隊伍 fields 和隊員 members.fields,作品 UI 顯示該階段 questions。入口及成員模型與普通 form 的 answers 不同。
保存某階段草稿的正文例子:
{
"answers": { "work": "Synthetic proposal text." },
"revision": 0
}work 必須是該階段宣告的 question key。成功保存回傳 submission id 和 revision,再以返回 revision 正式提交。同一 entry 持續更新,網站後端每次核對使用者可管理這支隊伍。
競賽 API 在伺服器檢查報名狀態、開放時間、已提交的前置階段及版本。前置條件未完成時,連保存該階段草稿也會被拒絕。草稿放寬必填答案檢查,但隊伍草稿仍需符合設定隊員人數;提交時檢查完整必填資料。衝突用 entry_changed/submission_changed,而不是普通回覆的 expected_version;需重新讀取核對。私人競賽附件用該 entry 的 uploads API,file 答案保存 upl_ id,不能用普通 file_upload URL 格式。
現有版本同一 respondent 在同一競賽限一隊。可撤銷私人 link 是持有者的存取憑證;自助開始報名、驗證電郵、自動寄出/找回連結仍需另行實作。不要把產生私人連結當作已提供整套登入或自助報名入口。
9.交付前驗收
用測試金鑰驗證:GET 的每個 key 對應正確 UI;必填、數值 0、多選及兩位參加者的物件陣列能保存;錯誤指向正確項目;更新沿用 respondent 和版本;重試不增加重複回覆;陌生訪客不能選另一份回覆;金鑰沒有出現在瀏覽器或下載檔案中。
再檢查只有適當的非個人欄位進報表。資料分類、表單建立重試、回覆讀取限制及私人檔案完整規則,見API 參考及常見問題。每個網站仍需實作自己的身份核對、欄位對應及伺服器接入層;不用為它重新修改 OpenDashboard。
哪些情況需要把資料送回原站?
既有整合若仍由原站確認正式答案,維護方式見原站回寫。新流程即使外站需要評審,也應由 OpenDashboard 保存答案,再用流程資料同步提供副本;不必改用舊式回寫。以 OpenDashboard 為主時,兩個 UI 直接使用同一中央 API,通常不需要回寫;先看接入方式。公開入口只建立新隊伍,再以私人連結修改;既有資料不會按姓名或電郵被認領。需要配置原站回寫程序及通知,單純匯入或建立表單不會自動啟用。
公開入口及管理位置
兩份以上表單需要按步驟串聯時,使用多步流程。在流程管理選擇流程,再到「網站接入」取得公開新申請連結,或由可信後端建立同一 run 的私人連結。原站為正式資料來源時使用原站回寫入口。表單管理保留每張表的回覆查看,關聯表單可直接返回所屬流程。