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#

  1. Look up the client by email: GET /clients?email=….
  2. If none is found, create one: POST /clients with the tag website.
  3. Create a task: POST /tasks linked to the client, due tomorrow.
  4. 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_id from errors so support can help.