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
- You create a Form Capture channel in Settings → Channels and configure your fields.
- You add the generated code snippet to your website.
- 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/jsonResponse format
{
"success": true,
"conversation_id": "550e8400-e29b-41d4-a716-446655440000",
"contact_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
}{
"success": false,
"error": "Validation failed",
"field_errors": {
"email": "Invalid email format",
"phone": "Invalid phone number format"
}
}{
"success": false,
"error": "Origin not allowed"
}Code examples
<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>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>
);
}'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>
);
}<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 name | Maps to | Required | Validation |
|---|---|---|---|
name | Contact name | No | — |
email | Contact email | Yes | |
message | Conversation first message | No | — |
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
| Type | Behaviour |
|---|---|
email | Must contain @ and a domain |
phone | Accepts formats like +1 (555) 123-4567, minimum 10 digits |
url | Must be a valid URL |
number | Must be numeric |
Transform types
| Type | Behaviour |
|---|---|
lowercase | Converts to lowercase |
uppercase | Converts to UPPERCASE |
trim | Removes leading/trailing whitespace |
Contact matching
When a form is submitted, Zickt looks for an existing contact by checking identifiers in this order:
- Email — highest priority
- Phone
- External ID
- 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.commatchesapp.example.com,staging.example.com, andexample.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.