Record a conversion via the API
Send a lead or a sale from your server together with the click that caused it, for conversions the LinkiFunnels pixel cannot see.
Use the API when the conversion happens somewhere the browser cannot see, such as a completed checkout, a signed contract, or a CRM stage change, or when you want to record a sale with a value.
Endpoint
Section titled “Endpoint”Authenticate with an API token from Settings, then Connect your site, then Manage tokens, passed as a bearer token. Tokens are scoped to one workspace. See Authenticate API requests.
curl -X POST \ https://app.linkifunnels.com/api/v1/conversions \ -H "Authorization: Bearer lf_live_..." \ -H "Content-Type: application/json" \ -d '{ "event": "sale", "click_id": "0f2a1c9e-...", "value_cents": 49000, "event_name": "Annual plan", "customer": { "external_id": "cus_8201", "email": "dana@northwind.io", "name": "Dana Whitaker" } }'const url = "https://app.linkifunnels.com/api/v1/conversions";const response = await fetch(url, { method: "POST", headers: { Authorization: `Bearer ${process.env.LINKIFUNNELS_API_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ event: "sale", click_id: clickId, value_cents: 49000, customer: { external_id: "cus_8201", email: "dana@northwind.io" }, }),});Where the click id comes from
Section titled “Where the click id comes from”The redirect appends ?lf_cid=<id> to your destination URL and the LinkiFunnels pixel stores it in a cookie named lf_cid for 90 days. Read it on the landing page, carry it through your sign up or checkout, and send it with the conversion. Without a click id the conversion cannot be attributed and the request is rejected. A visitor who arrived without a click can still be added to a campaign.
If the pixel is installed, it already records a lead when the signup form is submitted. Posting event: "lead" from your server for the same signup stores a second lead on the same click. Either leave leads to the pixel and post only sales, or put data-lf-ignore on the signup form and record leads server-side.
Without the pixel, store the cookie yourself on the first page the visitor lands on and remove lf_cid from the address bar so it does not travel in copied links. The pixel already does both.
Request body
Section titled “Request body”Send JSON. Only event and click_id are required.
| Parameter | Type | Required | Description |
|---|---|---|---|
event |
string | Yes | lead or sale |
click_id |
string | Yes | The click id from lf_cid. Ids stay valid as long as the click exists |
value_cents |
integer | For sale |
Order value in the smallest currency unit. 49000 is 490.00. A sale without a value is accepted and shown without revenue |
event_name |
string | No | A label shown in the activity feed, for example the plan or product name |
customer |
object | No | Who bought or signed up. Send external_id, email, or both. name and avatar_url are optional. See How customers are matched |
meta |
object | No | Any JSON you want stored with the event |
const params = new URLSearchParams(location.search);const clickId = params.get("lf_cid") ?? document.cookie.match(/(?:^|; )lf_cid=([^;]+)/)?.[1];Sales confirmed by a payment webhook
Section titled “Sales confirmed by a payment webhook”Most sales are confirmed by a webhook from Stripe, Paddle, or Lemon Squeezy rather than by a request from the visitor’s browser. The webhook carries no cookies, so lf_cid is not there to read. Carry the click id across yourself:
- When the visitor signs up or you create the checkout, read
lf_cidand store it on the user record or in the checkout’s metadata. Stripe calls itmetadata, Paddlecustom_data, Lemon Squeezycheckout_data.custom. - In the webhook handler, after verifying the signature, read the click id back from the user or from the event payload.
- Post
event: "sale"with thatclick_id, the order total invalue_cents, and the samecustomer.external_idyou use elsewhere.
If the click id is missing at webhook time there is nothing to attribute the sale to, and the endpoint rejects a request without one. You can still record that sale by hand.
// Creating the checkout session, on your serverconst session = await stripe.checkout.sessions.create({ mode: "payment", line_items, metadata: { lf_cid: readCookie(request, "lf_cid") ?? "" },});
// checkout.session.completed webhook handlerconst clickId = event.data.object.metadata?.lf_cid;if (clickId) { await fetch("https://app.linkifunnels.com/api/v1/conversions", { method: "POST", headers: { Authorization: `Bearer ${process.env.LINKIFUNNELS_API_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ event: "sale", click_id: clickId, value_cents: event.data.object.amount_total, customer: { email: event.data.object.customer_details.email }, }), });}How customers are matched
Section titled “How customers are matched”Every lead and sale belongs to one customer record per workspace, whether it arrives through the API, the LinkiFunnels pixel, or by hand.
The email address is the identity.
- If
emailis present and a customer already has that address, that customer is used, and anexternal_idin the same request is attached to them. A person added by hand is recognized the next time your backend reports a sale. - If only
external_idis present, the customer holding that id is used, or a new customer without an email address is created. It becomes a normal customer the moment an email arrives for that id. - Otherwise a new customer is created.
An external_id is a label rather than a second identity. If you send a known id with a new email address, the id moves to the customer who owns that address. Nothing is ever rejected for an identity clash, and the change is recorded in the customer’s activity. Names never take part in matching, and an existing name is never overwritten.
Customers reported this way appear in Contacts with everything else you know about the person.
{ "event": "sale", "click_id": "0f2a1c9e-...", "value_cents": 49000, "customer": { "email": "dana@northwind.io" }}Response
Section titled “Response”A successful call returns 201 Created with the stored event. link_id tells you which tracked link, and therefore which post, gets the credit.
{ "id": 48213, "event": "sale", "click_id": "0f2a1c9e-...", "link_id": 786, "customer": { "id": 1204, "external_id": "cus_8201" }, "occurred_at": "2026-09-10T09:41:12Z"}Errors
Section titled “Errors”| Status | Body | Meaning |
|---|---|---|
401 |
unauthorized |
The bearer token is missing, revoked, or belongs to another workspace |
422 |
invalid event |
event was something other than lead or sale |
422 |
click_id is required |
The request had no click_id |
422 |
unknown click_id |
No click with that id exists in this workspace |
429 |
rate_limited |
More than 1,000 requests per minute on this token, or 600 per minute from one IP |
{ "error": "unknown click_id" }