API/Attribution and Events

Attribution and Events

Install first-party attribution, identify customers, and ingest trusted conversions or revenue.

LinkQuick connects redirects, external customer identifiers, events, conversions, and revenue through first-party last-touch attribution. Browser events use a publishable site key. Trusted server events use a secret Admin API key. Never put an Admin API key in browser code.

Create a tracking site

Open Organization → Attribution, then create one tracking site for each destination application or group of applications that share the same allowed origins.

  • Give the site a recognizable name.
  • Add every production hostname that may send browser events. Enter hostnames without paths; full URLs are accepted and normalized.
  • Add preview, staging, or local hostnames explicitly when they need tracking. Wildcards are not accepted.
  • Choose an attribution window from 7 to 90 days. The default is 30 days.

A site accepts up to 20 hostnames. When a visitor lands on one domain and converts on another, read Multi-Domain Attribution before deciding whether to create a second site.

LinkQuick creates a publishable key and a copy-ready configuration snippet immediately. The key is intentionally public and allowed origins protect ordinary browser use; they are not proof that an event is trusted. Treat browser events as product telemetry, and send conversions, entitlements, and revenue from your authenticated backend. Disable the tracking site to revoke browser ingestion without deleting historical attribution.

Install the browser SDK

Copy the generated snippet from Organization → Attribution. The hosted, framework-free client works in any browser application without a package-registry login or build dependency:

<script src="https://linkquick.melvynx.dev/linkquick.js"></script>
<script>
  const linkquick = LinkQuick.create({
    endpoint: "https://linkquick.melvynx.dev/tracking",
    publishableKey: "lq_pk_YOUR_PUBLISHABLE_KEY",
    attributionWindowDays: 30,
  });
</script>

The endpoint is https://linkquick.melvynx.dev/tracking, a first-party path on LinkQuick itself that forwards to the tracking backend. Keep it as generated; the backend host is an implementation detail and may change.

By default, initialization captures lf_id from the current URL, stores it in first-party localStorage, and removes only that parameter from the visible URL. Other query parameters and the URL fragment are preserved.

Load the script in the browser before initializing it. You can also pass a compatible storage implementation. Set captureOnInit: false only when the application calls linkquick.capture() itself.

The conversion funnel

LinkQuick models attribution as three steps, in the same order as the visit itself:

  1. Click — the redirect records a click and hands lf_id to the destination application, where the SDK stores it first-party.
  2. Lead — the first moment you learn who the visitor is. An email from a form is enough; a signed-in user ID is better. The lead binds the stored click to a customer.
  3. Sale — revenue for a customer that already has a lead. Every sale inherits the link attribution the lead established.

Only the lead step needs the click ID, so it must run in the browser (or receive a clickId you forwarded to your backend). Sales are pure server calls keyed by the customer.

Capture a lead

Use lead when the application does not have a user ID yet — a newsletter form, a waitlist, a demo request. The email becomes the customer key.

await linkquick.lead("melvyn@example.com", {
  name: "Melvyn",
  eventName: "Sign up",
  metadata: { form: "newsletter" },
});

trackLead is the full form when you have both an email and your own ID:

await linkquick.trackLead({
  externalCustomerId: currentUser.id,
  customerEmail: currentUser.email,
  customerName: currentUser.name,
  eventName: "Trial started",
  idempotencyKey: `lead:${currentUser.id}`,
});

eventName defaults to Sign up. When externalCustomerId is omitted the normalized email is used as the customer key, so the same address always resolves to the same customer.

A customer is identified by that key alone; LinkQuick does not merge two customers because they share an email. Pick one key per person — the email until an account exists, then your own user ID — and pass it consistently.

Browser leads require a real, unexpired click ID. A form submit from a visitor who never clicked a LinkQuick link is rejected rather than recorded as an unattributed customer. Send those from the server instead.

Identify a customer

Call identify after the destination application knows its stable internal customer identifier. Prefer an internal ID over an email here; use lead when the email is all you have.

const result = await linkquick.identify(currentUser.id, {
  idempotencyKey: `identify:${currentUser.id}`,
});

if (!result.ok) {
  console.error(result.code, result.retryAfter);
}

The SDK attaches the most recent eligible click and remembers the customer ID for subsequent browser events.

Track a browser event

await linkquick.track("signup.completed", {
  metadata: {
    plan: "pro",
    source: "onboarding",
  },
  idempotencyKey: `signup.completed:${currentUser.id}`,
});

Browser ingestion accepts named product events, but it deliberately rejects trusted conversion revenue. Send conversions and money from a server.

Verify the installation

  1. Open an active short link in a new browser session.
  2. Confirm the destination initially receives lf_id and the SDK then removes it from the visible address.
  3. Confirm linkquick:attribution exists in first-party local storage.
  4. Sign in or complete the identified action in the destination application.
  5. Open the LinkQuick parent link. Its detail page shows whether attribution is configured, and its Analytics page shows identified users and events.
  6. Open Customers to inspect the attributed journey.

An invalid_key response means the key does not exist or the site is disabled. A forbidden_origin response means the request's exact hostname is missing from the tracking site. A rate_limited response includes retryAfter in milliseconds.

List customers

curl "https://linkquick.melvynx.dev/api/v1/customers?limit=50" \
  -H "Authorization: Bearer $LINKQUICK_API_KEY"

Add linkId=LINK_ID to return customers currently attributed to one link.

Read a customer journey

The path uses your application's stable external customer ID, URL encoded when necessary:

curl "https://linkquick.melvynx.dev/api/v1/customers/customer_123" \
  -H "Authorization: Bearer $LINKQUICK_API_KEY"

The response includes the customer aggregate and their events in descending order with cursor pagination.

Track a lead from the server

When the signup happens on your backend, forward the click ID your frontend captured (linkquick.getClickId()) and record the lead with an Admin API key:

curl -X POST https://linkquick.melvynx.dev/api/v1/track/lead \
  -H "Authorization: Bearer $LINKQUICK_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "clickId": "CLICK_ID",
    "eventName": "Sign up",
    "externalCustomerId": "customer_123",
    "customerEmail": "melvyn@example.com",
    "customerName": "Melvyn",
    "idempotencyKey": "signup-customer_123"
  }'

Send customerEmail alone when you have no user ID. Unlike browser leads, server leads are trusted: a lead without a click ID is stored as an unattributed customer instead of being rejected.

Track a sale

curl -X POST https://linkquick.melvynx.dev/api/v1/track/sale \
  -H "Authorization: Bearer $LINKQUICK_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "externalCustomerId": "customer_123",
    "eventName": "Subscription created",
    "amount": 9900,
    "currency": "EUR",
    "invoiceId": "in_123",
    "metadata": {"plan":"pro"}
  }'

amount uses minor currency units, so 9900 EUR represents €99.00. currency defaults to USD and eventName defaults to Purchase. invoiceId doubles as the idempotency key, so replaying the same invoice returns the original event instead of double-counting revenue. Pass an explicit idempotencyKey when your billing system has no invoice.

The sale inherits the attribution of the customer's most recent eligible click, which the lead step established.

Coming from Dub

The funnel is the same; the field names differ.

DubLinkQuick
dub_id query parameterlf_id query parameter
POST /track/leadPOST /api/v1/track/lead
POST /track/salePOST /api/v1/track/sale
customerExternalIdexternalCustomerId
customerName / customerEmailsame
amount (minor units)amount (minor units)
invoiceId (idempotency)invoiceId (idempotency)
dubAnalytics.trackLead()linkquick.trackLead()
publishable key + allowed hostspublishable key + allowed hosts

LinkQuick has no browser-side sale call by design: revenue is only accepted from a server holding an Admin API key.

Ingest a trusted conversion

Trusted conversions and revenue must be sent from a server with an Admin API key created under Organization → API Keys, never from browser code.

curl -X POST https://linkquick.melvynx.dev/api/v1/events \
  -H "Authorization: Bearer $LINKQUICK_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "externalCustomerId": "customer_123",
    "clickId": "CLICK_ID",
    "eventName": "subscription.created",
    "kind": "conversion",
    "revenueMinor": 9900,
    "currency": "EUR",
    "metadata": {"plan":"pro"},
    "idempotencyKey": "subscription-sub_123-created"
  }'

If clickId is absent or outside the attribution window, LinkQuick keeps a valid customer event without fabricating a link attribution.

Reuse a stable idempotencyKey for retries of the same business event. Revenue uses minor currency units, so 9900 EUR represents €99.00.

Audit recent events

curl "https://linkquick.melvynx.dev/api/v1/events?limit=100" \
  -H "Authorization: Bearer $LINKQUICK_API_KEY"
Analytics APIDomains and Imports API