Guides
Guide: website contact form
Turn website enquiries into a Quilo client plus a follow-up task, from your server.
A visitor fills in your contact form. Your server creates (or finds) the client in Quilo and adds a follow-up task for your team. The key stays on your server.
What you need#
- A key with Read and write access, named
Website form, with an expiry you will renew. - Your brand ID from
GET /api/v1/me. - Environment variables on your server:
QUILO_URL,QUILO_API_KEY,QUILO_BRAND_ID.
Flow#
- Look up the client by email:
GET /clients?email=…. - If none is found, create one:
POST /clientswith the tagwebsite. - Create a task:
POST /taskslinked to the client, due tomorrow. - Show the visitor a thank-you page. Never show Quilo errors to visitors; log them.
If your company has an automation such as "New client → welcome follow-ups", it runs for clients created through the API too.
Node.js (Express or a serverless function)#
JavaScript
const API = `${process.env.QUILO_URL}/api/v1`;
const headers = { Authorization: `Bearer ${process.env.QUILO_API_KEY}`, 'Content-Type': 'application/json' };
async function quilo(path, init = {}) {
const res = await fetch(`${API}${path}`, { ...init, headers: { ...headers, ...(init.headers || {}) } });
const body = await res.json();
if (!res.ok) throw Object.assign(new Error(body.error?.message || res.statusText), { status: res.status, body });
return body;
}
export async function handleEnquiry({ name, email, phone, message, formId }) {
const [first_name, ...rest] = name.trim().split(/\s+/);
const found = await quilo(`/clients?email=${encodeURIComponent(email)}&limit=1`);
const client = found.data[0] ?? await quilo('/clients', {
method: 'POST',
headers: { 'Idempotency-Key': `web-client-${formId}` },
body: JSON.stringify({ brand_id: process.env.QUILO_BRAND_ID, first_name, last_name: rest.join(' ') || null, email, phone: phone || null, tags: ['website'] }),
});
const tomorrow = new Date(Date.now() + 86400e3).toISOString().slice(0, 10);
await quilo('/tasks', {
method: 'POST',
headers: { 'Idempotency-Key': `web-task-${formId}` },
body: JSON.stringify({ brand_id: process.env.QUILO_BRAND_ID, client_id: client.id, title: `Reply to website enquiry from ${name}`.slice(0, 200), description: message.slice(0, 5000), due_on: tomorrow, priority: 'high' }),
});
}formId is a unique ID you give each submission (for example a UUID stored with the form entry), so a double click or a retry never creates two clients.
PHP (WordPress, plain PHP)#
PHP
<?php
function quilo(string $method, string $path, ?array $body = null, ?string $idem = null): array {
$ch = curl_init(getenv('QUILO_URL') . '/api/v1' . $path);
$headers = ['Authorization: Bearer ' . getenv('QUILO_API_KEY'), 'Content-Type: application/json'];
if ($idem) $headers[] = 'Idempotency-Key: ' . $idem;
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$res = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException(($res['error']['message'] ?? 'Quilo error') . ' ' . ($res['error']['request_id'] ?? ''));
return $res;
}
$found = quilo('GET', '/clients?email=' . urlencode($email) . '&limit=1');
$client = $found['data'][0] ?? quilo('POST', '/clients', [
'brand_id' => getenv('QUILO_BRAND_ID'), 'first_name' => $first, 'last_name' => $last, 'email' => $email, 'tags' => ['website'],
], 'web-client-' . $formId);Checklist#
- Validate and limit the form on your side too (captcha, length limits).
- Keep the key out of the HTML and JavaScript of the page.
- Log
request_idfrom errors so support can help.