通过自有代码提交表单(UniSubmit)
使用一次 POST 从自建网站、原生应用或服务器向 Reach 提交表单、加入列表,或同时完成两者。
UniSubmit 是 Zalify 表单共用的接口。托管弹窗和内嵌表单会自动调用;自建 Next.js、React Native、Shopify Hydrogen 或服务器程序可以直接调用。一次 POST 可保存提交记录、加入列表,或同时完成两者。
POST https://reach.zalify.com/v1/public/unisubmit
Content-Type: application/json
该接口按公开表单用途设计,无需 API 密钥。工作区 ID(wid)和列表 ID 可以出现在浏览器端,但不要把其他私密凭证放入表单代码。
何时使用 UniSubmit
后续提交列表、列表成员和自动化使用相同处理流程。
获取表单与列表 ID
连接自己的表单
打开 表单 → 全部表单,点击 连接自有表单,填写名称;收集订阅时,选择或创建目标列表。
复制代码片段
Zalify 创建 API 表单,并提供预填 wid、form_key、list_id 的示例。复制这些值供程序使用。
form_key 实际传入表单 ID
字段名因兼容性保留为 form_key,新集成应传入应用生成的 API 表单 UUID。必须先在应用中创建表单;未知 ID 不会自动建表单。旧集成使用的 newsletter、contact_us 等字符串仍关联到已有记录,但新表单应使用应用提供的 ID。
请求字段
所有请求都需要 wid,其他对象按业务组合。
| 字段 | 要求 | 说明 |
|---|---|---|
wid | 必填 | 工作区 ID |
identity | 对象 | email 用于匹配联系人,first_name、last_name、phone 补充资料;存在 subscribe 时需要 identity.email |
submission | 对象 | form_key 和自定义字段对象 payload;仅订阅时省略 |
subscribe | 对象 | list_id 和布尔值 email_marketing_consent;仅保存答案时省略 |
context | 对象 | page_url、referrer、visitor_id、UTM 等来源信息 |
idempotency_key | 字符串 | 同一次提交的各次重试必须保持相同,见下方“重试与去重” |
strict_phone | 布尔值 | 手机号无效时拒绝整个请求,而不是去掉手机号后保存提交。托管表单会发送 true,以便标记手机号输入框。默认为 false。 |
同时提交与订阅
保存答案,同时加入列表:
await fetch("https://reach.zalify.com/v1/public/unisubmit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
wid: "YOUR_WORKSPACE_ID",
identity: {
email: emailInput.value,
first_name: firstNameInput.value,
},
submission: {
form_key: "YOUR_API_FORM_ID",
payload: {
company: companyInput.value,
message: messageInput.value,
},
},
subscribe: {
list_id: "YOUR_LIST_ID",
email_marketing_consent: marketingConsentCheckbox.checked,
},
context: {
page_url: location.href,
visitor_id: window.zalify?.vid,
},
}),
});
仅提交
省略 subscribe。适合联系表单、问卷和未申请营销订阅的场景。此时 identity 可省略,支持匿名提交。
仅订阅
省略 submission,用于订阅框或等候名单。必须提供 identity.email,该方式加入列表但不会产生 Submission 记录。
分别提交不同表单
同一个人使用多个表单时,各自发送请求并使用对应的 form_key。identity.email 用于关联到同一联系人。
响应与错误
成功时返回 2xx 及:
{ "status": "accepted" }
应同时检查 response.ok 与 status === "accepted"。其他情况按失败处理并允许重试,不要直接显示成功感谢页。
| HTTP | 响应 | 含义 |
|---|---|---|
2xx | { "status": "accepted" } | 已接受 |
404 | { "error": "workspace '…' not found" } | wid 错误 |
422 | 纯文本,例如缺少 wid | 字段缺失或类型不符合结构 |
422 | { "error": "phone is not a valid number", "code": "invalid_phone", "field": "phone" } | 仅在 strict_phone: true 时出现:手机号不符合所在国家或地区的号码规则,未保存任何数据 |
422 | { "error": "…" } | 结构已解析但校验失败,例如表单不存在、未启用、订阅缺邮箱或 payload 键名不合法 |
一个无效键名会导致整次请求失败
payload 键名只允许 ASCII 字母、数字及 _ - . :。空格、斜杠或中文等非拉丁字符会导致整次请求被拒绝,既不保存数据也不订阅。请将字段名映射为 snake_case,需要保留中文题目时把它放在字段值中。
通过自己的服务器转发
简单订阅框可以直接从浏览器调用。表单包含更多信息时,建议通过自己的服务端路由转发,主要用于:
- 校验数据: 重新验证枚举、长度和网址,不能只依赖浏览器控件。
- 过滤垃圾提交: 在服务端检查蜜罐字段和提交耗时。命中时可以返回与正常成功一致的响应并跳过 Reach 调用,减少暴露过滤规则。
- 控制页面响应: 返回参考编号、跳转地址或礼品码等自有业务结果。
- 衔接其他系统: 在同一位置处理内部通知或其他业务同步。
以下是 Next.js 的最小示例;正式使用时还应按业务补充字段校验与异常处理:
// app/api/lead/route.ts
import { NextResponse } from "next/server";
const UNISUBMIT = "https://reach.zalify.com/v1/public/unisubmit";
const WID = "YOUR_WORKSPACE_ID";
const FORM_ID = "YOUR_API_FORM_ID";
const LIST_ID = "YOUR_LIST_ID";
const ROLES = ["Founder", "Marketing", "Purchasing"] as const;
export async function POST(request: Request) {
const body = await request.json();
// Honeypot: real visitors never see this field.
if (body.website) return NextResponse.json({ ok: true });
const email = String(body.email ?? "").trim().toLowerCase();
const role = ROLES.includes(body.role) ? body.role : "";
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !role) {
return NextResponse.json({ ok: false, error: "invalid" }, { status: 422 });
}
const res = await fetch(UNISUBMIT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
wid: WID,
identity: { email },
submission: { form_key: FORM_ID, payload: { role, company: body.company } },
subscribe: { list_id: LIST_ID, email_marketing_consent: body.marketing === true },
context: {
page_url: body.pageUrl,
referrer: body.referrer,
visitor_id: body.visitorId,
},
idempotency_key: request.headers.get("x-idempotency-key") ?? undefined,
}),
});
const data = await res.json().catch(() => null);
if (!res.ok || data?.status !== "accepted") {
return NextResponse.json({ ok: false, error: "reach" }, { status: 502 });
}
return NextResponse.json({ ok: true });
}
注意三点:蜜罐命中返回成功形状;营销授权使用复选框的真实值;访客 ID 和幂等键从浏览器原样传入,而不是在服务器重新生成。
上线时需要注意
列表成员与营销授权分别处理
subscribe.email_marketing_consent 必须反映用户真实选择,不要因为要加入列表就总填 true。例如不接受营销的展会线索,也可能需要进入业务跟进列表;传入 { list_id, email_marketing_consent: false } 可分别记录这两件事。
是否加入列表应按表单用途和用户已知的处理目的决定。询价或预约演示,不应被当作营销授权。
保留像素与联系人的关联
页面已安装像素时,window.zalify?.vid 是访客 ID。将它作为 context.visitor_id 传入,才能关联可匹配的匿名浏览、商品访问和活动来源。
经过服务器代理时,务必在浏览器读取后转发。page_url 和 referrer 也应从浏览器传递,服务器本身无法代替访客页面提供这些信息。
成功后自行发送 lead 事件
托管表单自动触发 form_submitted 和 lead;UniSubmit 是服务端接口,不会替页面调用像素。确认服务端返回成功后执行:
window.zalify?.("track", "lead", { email });
否则 Reach 中虽然有记录,浏览器侧广告事件转发可能没有对应线索事件。
重试与去重
idempotency_key 用于提交去重。相同提交的重试应复用相同键,避免网络断开后重复提交。
可在浏览器为本次表单会话生成一次 crypto.randomUUID(),保存到 sessionStorage,每次尝试都使用它,服务器原样转发。不要每次请求都生成新键,否则无法去重。开始一份新的独立提交时,应使用新键。
payload 字段值
建议使用扁平对象,键名为 snake_case,值为字符串、数字或布尔值。多选答案可拼成字符串,例如 "Reading Robot, AI Cube",便于在提交记录中阅读。不要未经验证就依赖嵌套对象或数组的展示方式。
手机号
按访客输入的原样发送手机号。Reach 按 payload 中的国家或地区规则解析号码:优先使用 phoneCountryCode(国际区号,例如 +44),其次是 countryCode 或 country,都没有时按美国号码处理。号码无效时,Reach 会去掉手机号并保存其余内容;如果发送了 strict_phone: true,整个请求会返回 422,code 为 invalid_phone,你可以提示访客修改号码。
超时
浏览器请求可通过 AbortController 设置约 10–12 秒超时,并显示明确提示。超时后重试仍应使用原幂等键,避免重复提交。
不写服务端:用像素接管已有表单
网站已安装 Zalify Pixel 时,可以用 wire_form 直接接管页面上已有的表单,例如页脚订阅框,不需要自己调用 UniSubmit:
window.zalify('wire_form', {
selector: '#newsletter',
list_id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
form_id: 'footer_signup',
});
- 字段名: 输入项的
name对应email、phone、firstName、lastName、language、emailMarketingConsent、smsMarketingConsent。 - 事件: 与托管弹窗相同,提交前触发
form_submitted(含手机号时在号码校验通过后),收到2xx后触发lead。lead只带{ email },会转发给已连接的广告平台。 - 状态:
data-zalify-form-state按idle → submitting → success | error更新,可以只用 CSS 显示加载、成功和错误提示。 - 手机号无效: 输入框加上
error类和aria-invalid="true",表单回到idle,不触发事件;访客修改后标记清除。可用[data-zalify-form] [aria-invalid="true"] { border-color: #d92d20; }设置样式。 - 调用时机: 像素未完成启动时调用会进入队列。
- CSP: 网站使用内容安全策略时,需要允许
script-src https://cdn.zalify.com。
完整参考见表单指南;手写弹窗 HTML 的结构和动作(spin_to_win、copy_text、grant_consent 等)见弹窗结构说明。
不写入工作区的测试方法
使用本地接收服务。 临时将目标地址改为本地 HTTP 服务,记录请求体并返回 { "status": "accepted" }。检查字段和授权复选框的传值,确认结构正确后再接真实接口。
使用确认不存在的测试工作区。 不存在的 wid 会返回 404,可用于检查连接和错误处理。原示例使用 { "wid": "probe" },返回 workspace 'probe' not found;请确保所用标识确实不是有效工作区。
真实测试使用自己控制的邮箱,完成后清理测试联系人,并核对提交记录。
在哪里查看结果
- 表单 → 全部表单 中对应 API 表单下可见提交,与托管表单记录使用同一数据流。
- 提供
subscribe时,联系人按授权状态加入指定列表;后续自动化仍受自身入口和发送条件控制。 identity字段更新联系人资料。
常见问题
- 404 workspace not found: 从应用示例重新复制
wid,它不是店铺 slug。 - 422 提示缺字段: 检查
wid、字段类型及订阅所需的identity.email。 - 422 但内容看似正常: 检查键名,空格和非拉丁字符会使整次请求失败。
- accepted 但没加入列表: 确认提供了
subscribe,并核对list_id。 - accepted 但缺浏览历史: 检查
context.visitor_id和像素安装。 - 同一访客重复记录: 检查重试之间幂等键是否变化。
- 广告后台没有转化: 成功后自行触发
lead。