Zickt

Form Capture

Turn website form submissions into Zickt conversations with automatic contact matching and team routing. Includes HTML, React, and Vue code examples.

Form Capture turns HTML form submissions on your website into Zickt conversations. Each submission creates a contact (or matches an existing one), opens a conversation in your inbox, and routes it to the right team.

How it works

  1. You create a Form Capture channel in Settings → Channels and configure your fields.
  2. You add the generated code snippet to your website.
  3. When a visitor submits the form, Zickt creates or matches a contact, opens a conversation, and applies your default assignment rules.

API endpoint

All form submissions are sent to a single endpoint. Replace YOUR_CHANNEL_KEY with the key from your channel's Installation tab.

POST https://api.zickt.com/v1/forms/YOUR_CHANNEL_KEY/submit
Content-Type: application/json

Response format

200 OK
{
  "success": true,
  "conversation_id": "550e8400-e29b-41d4-a716-446655440000",
  "contact_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
}
400 Bad Request
{
  "success": false,
  "error": "Validation failed",
  "field_errors": {
    "email": "Invalid email format",
    "phone": "Invalid phone number format"
  }
}
403 Forbidden
{
  "success": false,
  "error": "Origin not allowed"
}

Code examples

contact-form.html
<form id="contact-form">
  <input type="text" name="name" placeholder="Your name" required />
  <input type="email" name="email" placeholder="Your email" required />
  <textarea name="message" placeholder="Your message" required></textarea>
  <button type="submit">Send</button>
</form>

<script>
  document.getElementById('contact-form').addEventListener('submit', async (e) => {
    e.preventDefault();
    const data = Object.fromEntries(new FormData(e.target).entries());

    const res = await fetch('https://api.zickt.com/v1/forms/YOUR_CHANNEL_KEY/submit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });

    if (res.ok) {
      e.target.reset();
      alert('Thanks! We\'ll be in touch soon.');
    }
  });
</script>
ContactForm.tsx
import { useState } from 'react';

export function ContactForm() {
  const [status, setStatus] = useState<'idle' | 'sending' | 'sent'>('idle');

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setStatus('sending');
    const data = Object.fromEntries(new FormData(e.currentTarget));

    const res = await fetch('https://api.zickt.com/v1/forms/YOUR_CHANNEL_KEY/submit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });

    setStatus(res.ok ? 'sent' : 'idle');
  };

  if (status === 'sent') return <p>Thanks! We'll be in touch soon.</p>;

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required />
      <button type="submit" disabled={status === 'sending'}>
        {status === 'sending' ? 'Sending…' : 'Send'}
      </button>
    </form>
  );
}
app/contact/page.tsx
'use client';

import { useState } from 'react';

export default function ContactPage() {
  const [status, setStatus] = useState<'idle' | 'sending' | 'sent'>('idle');

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setStatus('sending');
    const data = Object.fromEntries(new FormData(e.currentTarget));

    const res = await fetch('https://api.zickt.com/v1/forms/YOUR_CHANNEL_KEY/submit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });

    setStatus(res.ok ? 'sent' : 'idle');
  };

  if (status === 'sent') return <p>Thanks! We'll be in touch soon.</p>;

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required />
      <button type="submit" disabled={status === 'sending'}>
        {status === 'sending' ? 'Sending…' : 'Send'}
      </button>
    </form>
  );
}
ContactForm.vue
<script setup lang="ts">
import { ref } from 'vue';

const status = ref<'idle' | 'sending' | 'sent'>('idle');

const handleSubmit = async (e: Event) => {
  status.value = 'sending';
  const data = Object.fromEntries(new FormData(e.target as HTMLFormElement));

  const res = await fetch('https://api.zickt.com/v1/forms/YOUR_CHANNEL_KEY/submit', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });

  status.value = res.ok ? 'sent' : 'idle';
};
</script>

<template>
  <p v-if="status === 'sent'">Thanks! We'll be in touch soon.</p>
  <form v-else @submit.prevent="handleSubmit">
    <input name="name" placeholder="Name" required />
    <input name="email" type="email" placeholder="Email" required />
    <textarea name="message" placeholder="Message" required />
    <button type="submit" :disabled="status === 'sending'">
      {{ status === 'sending' ? 'Sending…' : 'Send' }}
    </button>
  </form>
</template>

Replace YOUR_CHANNEL_KEY with the channel key from Settings → Channels → Form Capture → Installation.

Field mapping

Each form field maps to a contact property, company property, or the conversation's first message. Configure mappings in the channel's Field Mapping tab, or use the visual form builder during channel creation.

Default fields

New Form Capture channels start with these fields:

Field nameMaps toRequiredValidation
nameContact nameNo—
emailContact emailYesEmail
messageConversation first messageNo—

Available field targets

Every Form Capture channel must include at least one identifier field (contact.email, contact.phone, contact.external_id, or any social/CRM identifier). This ensures Zickt can create or match a contact.

Validation types

TypeBehaviour
emailMust contain @ and a domain
phoneAccepts formats like +1 (555) 123-4567, minimum 10 digits
urlMust be a valid URL
numberMust be numeric

Transform types

TypeBehaviour
lowercaseConverts to lowercase
uppercaseConverts to UPPERCASE
trimRemoves leading/trailing whitespace

Contact matching

When a form is submitted, Zickt looks for an existing contact by checking identifiers in this order:

  1. Email — highest priority
  2. Phone
  3. External ID
  4. Other identifiers — social profiles, CRM IDs

If a match is found, the existing contact is updated with any new data from the submission. If no match is found, a new contact is created.

Company data is handled the same way: if a company.domain or company.name matches an existing company, the contact is linked to it automatically.

Allowed origins

Form Capture validates the Origin header on every submission. Only requests from domains you've approved are accepted.

  • Configure origins in Settings → Channels → Form Capture → Security
  • Wildcard subdomains are supported: *.example.com matches app.example.com, staging.example.com, and example.com
  • If no origins are configured, all domains are allowed (useful during initial development)

Submissions from unlisted origins return a 403 error. Add localhost to your allowed origins for local development.

Default assignment

Optionally assign a default team or team member to conversations created from a form. Configure this in the channel settings:

  • Default team — all form conversations are assigned to this team
  • Default assignee — all form conversations are assigned to this person

These can be overridden manually after the conversation is created.

Channel switching

Route conversations to a different channel after the initial form submission. For example, capture leads via a contact form but route the ongoing conversation to your Web Messenger channel for live chat follow-up.

Configure this under Switch to channel in the channel's security settings.

Troubleshooting

Next steps

On this page