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 --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 -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 -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 -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
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;
}
}
}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
422withvalidation_failed: for example an invalid email address.errorssays per field what is wrong. Let the visitor correct it.429withrate_limited: wait the number of seconds inRetry-Afterand try again. See Rate limits.- A
5xxerror or a timeout: try again later with the sameIdempotency-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