表单

/

15 分钟阅读

通过自有代码提交表单(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

场景方式
希望 Zalify 负责显示与样式弹窗或内嵌表单
已有自己的 HTML 和框架表单UniSubmit
应用、CRM 同步、展会设备或合作方系统提供线索从服务器调用 UniSubmit

后续提交列表、列表成员和自动化使用相同处理流程。

获取表单与列表 ID

1

连接自己的表单

打开 表单 → 全部表单,点击 连接自有表单,填写名称;收集订阅时,选择或创建目标列表。

2

复制代码片段

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。

相关文章