Klantly Developers

Website form to lead

Put every request from the form on your website into Klantly as a lead right away, with the message as a note and optionally a deal.

A visitor fills in the contact form on your website. With a few requests, that request is in Klantly as a lead, with the message attached and a card on your pipeline board.

Warning

Send the form to your own server and make the API requests from there. An API key never belongs in the code of your website: anyone can read it there.

What you need

An API key with these scopes:

Scope What for
customers.read Checking whether the email address is already known.
customers.write Creating the lead and adding the note.
deals.write Optional: putting the lead on the pipeline board.

Step 1: does the customer exist already?

An email address is unique within your company. So first check whether it is already known:

cURL
curl --globoff "https://app.klantly.com/api/v1/customers?filter[email]=jan@example.com" \
  -H "Authorization: Bearer $KLANTLY_API_KEY"

If data is empty, create the lead. Otherwise, use the id of the customer you get back.

Step 2: create the lead

cURL
curl -X POST "https://app.klantly.com/api/v1/customers" \
  -H "Authorization: Bearer $KLANTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: form-8f14e45f" \
  -d '{"email": "jan@example.com", "name": "Jan de Vries", "phone": "+31 6 12345678"}'

Give every submission its own Idempotency-Key, for example the id of the submission in your own database. If a visitor clicks send twice, or your server tries again after a timeout, you still get only one lead. More on that in Idempotency.

If two submissions with the same email address arrive at almost the same moment, the second can return a 422 with an error on email. Look the customer up again, as in step 1.

Step 3: the message as a note

cURL
curl -X POST "https://app.klantly.com/api/v1/customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/notes" \
  -H "Authorization: Bearer $KLANTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: form-8f14e45f-note" \
  -d '{"content": "Through the website form: I would like a quote for a 5 by 3 metre veranda."}'

Step 4: a deal on the board (optional)

cURL
curl -X POST "https://app.klantly.com/api/v1/deals" \
  -H "Authorization: Bearer $KLANTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: form-8f14e45f-deal" \
  -d '{"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70", "title": "Veranda 5x3 m"}'

A customer has only one open deal at a time. If there already is one, you get 409 with the code conflict and the id of that deal in deal_id. That is not a real error: the request then belongs to the deal that is already running.

All together

PHP
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;

function sendLeadToKlantly(array $form, string $submissionId): void
{
    $klantly = new Client([
        'base_uri' => 'https://app.klantly.com/api/v1/',
        'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
    ]);

    // 1. Does the customer exist already?
    $found = json_decode((string) $klantly->get('customers', [
        'query' => ['filter' => ['email' => $form['email']]],
    ])->getBody(), true)['data'];

    // 2. If not: create the lead.
    $customer = $found[0] ?? json_decode((string) $klantly->post('customers', [
        'headers' => ['Idempotency-Key' => "form-{$submissionId}"],
        'json' => array_filter(['email' => $form['email'], 'name' => $form['name'], 'phone' => $form['phone']]),
    ])->getBody(), true)['data'];

    // 3. The message as a note.
    $klantly->post("customers/{$customer['id']}/notes", [
        'headers' => ['Idempotency-Key' => "form-{$submissionId}-note"],
        'json' => ['content' => $form['message']],
    ]);

    // 4. A deal; 409 means there already is an open deal.
    try {
        $klantly->post('deals', [
            'headers' => ['Idempotency-Key' => "form-{$submissionId}-deal"],
            'json' => ['customer_id' => $customer['id'], 'title' => $form['subject']],
        ]);
    } catch (ClientException $e) {
        if ($e->getResponse()->getStatusCode() !== 409) {
            throw $e;
        }
    }
}
Node.js
const BASE_URL = 'https://app.klantly.com/api/v1';

async function klantly(method, path, { body, idempotencyKey } = {}) {
  const response = await fetch(`${BASE_URL}/${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
      ...(body ? { 'Content-Type': 'application/json' } : {}),
      ...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });

  return { status: response.status, json: await response.json() };
}

export async function sendLeadToKlantly(form, submissionId) {
  // 1. Does the customer exist already?
  const found = await klantly('GET', `customers?${new URLSearchParams({ 'filter[email]': form.email })}`);
  if (found.status !== 200) throw new Error(found.json.detail ?? found.json.title);
  let customer = found.json.data[0];

  // 2. If not: create the lead.
  if (!customer) {
    const created = await klantly('POST', 'customers', {
      body: { email: form.email, name: form.name, ...(form.phone ? { phone: form.phone } : {}) },
      idempotencyKey: `form-${submissionId}`,
    });
    if (created.status !== 201) throw new Error(created.json.detail ?? created.json.title);
    customer = created.json.data;
  }

  // 3. The message as a note.
  await klantly('POST', `customers/${customer.id}/notes`, {
    body: { content: form.message },
    idempotencyKey: `form-${submissionId}-note`,
  });

  // 4. A deal; 409 means there already is an open deal.
  const deal = await klantly('POST', 'deals', {
    body: { customer_id: customer.id, title: form.subject },
    idempotencyKey: `form-${submissionId}-deal`,
  });
  if (deal.status !== 201 && deal.status !== 409) throw new Error(deal.json.detail ?? deal.json.title);
}

Handling errors

  • 422 with validation_failed: for example an invalid email address. errors says per field what is wrong. Let the visitor correct it.
  • 429 with rate_limited: wait the number of seconds in Retry-After and try again. See Rate limits.
  • A 5xx error or a timeout: try again later with the same Idempotency-Key. Preferably put the submission in a queue, so no request is ever lost.

Tip

Want to know when a lead becomes a customer? Listen for the customer.converted event with a webhook.

Last updated on September 15, 2026