Developers

Build on the affiliate platform, not around it.

One Partner API, a first-party tracking SDK, server-to-server postbacks, and an MCP server your AI can call, all over standard HTTPS and JSON. Real endpoints, copy-paste code, and docs you don't have to email sales to read.

Authenticate with an API key you scope and IP-restrict yourself. Every call, including denials, lands in your request log.

Partner API v1

A REST API any language can call

Every response is a { "success", "data", "meta" } envelope over plain HTTPS and JSON, so any language or tool that makes web requests can use it. Authenticate with an API key in the X-Api-Key header, page list endpoints with page and per_page, and branch on the stable error.code, never on message text.

The endpoints

The whole /v1 surface, with the scope each one needs. Try any of them live, with a real key, in the interactive reference, no request body required for the reads.

EndpointWhat it returnsScope
GET/v1/pingYour account name and the key's scopesnone
GET/v1/reports/clicksRow-level clicks, newest firstread:reports
GET/v1/reports/conversionsRow-level conversions, newest firstread:reports
GET/v1/offersYour offers (and /v1/offers/{id})read:entities
GET/v1/affiliatesYour affiliates (and /v1/affiliates/{id})read:entities
POST/v1/affiliatesCreate an affiliate (idempotent on email)write:affiliates
GET/v1/campaignsYour campaigns (and /v1/campaigns/{id})read:entities

Quickstart

List last month's conversionsbash
# The base URL is shown in Settings → Security → API access
curl -G https://api.limelijourney.com/v1/reports/conversions \
  -H "X-Api-Key: $LLJ_API_KEY" \
  --data-urlencode "from=2026-08-01" \
  --data-urlencode "to=2026-08-31" \
  --data-urlencode "per_page=200"
Same call, paginated, in Pythonpython
import os, requests

BASE = "https://api.limelijourney.com/v1"
HEADERS = {"X-Api-Key": os.environ["LLJ_API_KEY"]}

def conversions(frm, to):
    page = 1
    while True:
        r = requests.get(f"{BASE}/reports/conversions",
            headers=HEADERS,
            params={"from": frm, "to": to,
                    "page": page, "per_page": 200})
        body = r.json()
        assert body["success"], body.get("error")
        yield from body["data"]
        meta = body["meta"]
        if page >= meta["total_pages"]:
            return
        page += 1

for row in conversions("2026-08-01", "2026-08-31"):
    print(row["conversion_id"], row["revenue"])
Create an affiliatebash
# Idempotent on contact_email: a repeat returns 409
# with the existing affiliate_id in error.details
curl https://api.limelijourney.com/v1/affiliates \
  -H "X-Api-Key: $LLJ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Sunrise Media",
       "contact_email":"partners@sunrise.example"}'

The base URL above is illustrative; use the exact execute-api invoke URL shown in your dashboard's API access view. Only name and contact_email are required to create an affiliate.

Tracking SDK

One line, first-party, cookie-loss proof

Serve sdk.js from your own tracking domain and it sets first-party cookies server-side, so a Safari or iOS visitor still attributes weeks later, long after ITP has capped third-party JavaScript cookies at seven days. Drop it sitewide or on a single landing page; it only records a click when the URL actually carries tracking parameters.

What the tag gives you

The tracker keeps its own copy of the click ID, so conversions still attribute even if a pixel forgets to pass it. For ad platforms that dislike redirect hops, the SDK beacons the click directly, with an image-pixel fallback, and carries all five sub-ID slots through.

  • First-party cookies (ll_cid, ll_vid) set on your domain, outliving the ITP seven-day cap
  • Sitewide include or single placement, both one line, both auto-updating
  • Direct tracking with no redirect hop, plus a redirect link (/c) that needs nothing installed
  • Sub-IDs s1 through s5 captured on every click, reportable and controllable per campaign

Embed

Landing page · one linehtml
<!-- Load the first-party tracker from your own domain -->
<script src="https://track.yourbrand.com/sdk.js" async></script>

// on click, first-party cookies are set on your domain:
//   ll_cid = a4f1c9...   (the click id)
//   ll_vid = 7b02e1...   (the visitor id)
Direct or redirect, sub-IDs ride alongtext
# Redirect link on your tracking domain
https://track.yourbrand.com/c?o=12&aff=34&cmp=212
    &s1=newsletter&s2=aug&s3=variantB

Postbacks & S2S

Conversions in, exactly once

Fire a conversion from your server the moment it happens. Post the fields, read back the minted conversion ID for your CRM, and never worry about double-counting: ingestion is idempotent on the transaction ID, so a retry or an SQS redelivery returns the same ID instead of a second row.

Build and validate the URL

Not sure which parameters your network sends? The postback URL builder maps the four canonical inputs to your parameter names and validates the result before you paste it into an advertiser or a network. Every attempt, even the malformed ones, is captured in the pixel log, so debugging takes minutes.

  • Server-to-server postback returns JSON with the conversion ID to store in your CRM
  • Image pixel on /conv returns a 1×1 GIF and can't break a thank-you page
  • Idempotent on the transaction ID: a re-fire returns the same conversion, never a second row
  • Every attempt logged, including denials, so you can trace exactly what a network sent

Fire a conversion

Server-to-server postbackbash
# Fire from your server on a confirmed sale
curl -G https://track.yourbrand.com/conv \
  --data-urlencode "o=12" \
  --data-urlencode "txn=ORDER-8842" \
  --data-urlencode "amt=249.00"

# => {"conversion_id": 12}  (same txn, same id, always)
Thank-you page · image pixelhtml
<img src="https://track.yourbrand.com/conv
       ?o=12&txn=ORDER-8842&amt=249.00"
     width="1" height="1" alt="" />

Set track.yourbrand.com to your own tracking domain. Fires twice? The same txn returns the same conversion_id both times. One conversion, always.

Keys & security

You hold the keys, and the guardrails

Create keys in Settings → Security → API access. Each one carries only the scopes you grant, can be pinned to specific IPs, and shows up in a request log the moment it's used, so you can grant the least a job needs and see exactly what it did.

Scoped API keys

Grant only what a key needs: read:entities, read:reports, or write:affiliates. A call without the scope returns 403 insufficient_scope.

IP allowlists

Restrict a key to the IPs your servers call from. A request from anywhere else is refused with 403 ip_not_allowed, before it ever touches your data.

Request log

Every call, including denials, is recorded in Settings: which key, which endpoint, the status. Rotate or revoke a key the instant something looks off.

Tenant isolation

The key identifies your account; you only ever see your own data. tenant_id is never a request parameter, so it can't be spoofed from the client.

MCP server

The same API, as tools your AI can call

Point Claude Desktop, Claude Code, or any MCP client at the LimeliJourney MCP server and your assistant gets typed, tenant-scoped tools over the same authenticated API your dashboard uses. The tool surface is generated from the OpenAPI spec, so it tracks the API automatically; reads and safe writes are on, DELETE is off until you enable it. No competing tracker ships one.

Connected in one config entry

Install the server from mcp-server/, drop one entry into your client's MCP config with your API URL and login token, and restart. The limelijourney tools appear and your assistant can read your live program and propose safe changes, each call approved in your client before it runs.

Add it to your client

claude_desktop_config.jsonjson
{
  "mcpServers": {
    "limelijourney": {
      "command": "python",
      "args": ["/abs/path/to/mcp-server/server.py"],
      "env": {
        "LIMELI_API_BASE": "https://<your-api>/prod",
        "LIMELI_BEARER_TOKEN": "<your-cognito-id-token>"
      }
    }
  }
}

Claude Code reads the same shape. To expose DELETE and bulk backfill, add LIMELI_ALLOW_DESTRUCTIVE=1 to the env; it's off until you do.

Developer questions, answered

How do I authenticate?

Create an API key in the dashboard under Settings → Security → API access, then send it on every request in the X-Api-Key header (or as Authorization: Bearer llj_live_...). The key identifies your account, so you only ever see your own data. Start with GET /v1/ping to confirm the key works and see its scopes.

What does a response look like?

Every response is a JSON envelope: {"success": true, "data": ..., "meta": ...} on success, or {"success": false, "error": {"code", "message"}} on failure. Error codes are stable strings, so branch on error.code, never on the message text. List endpoints return paging in meta: page, per_page, total and total_pages.

Can I write data through the API?

Version 1 is read-first, with one write endpoint: POST /v1/affiliates creates an affiliate and needs the write:affiliates scope. It's idempotent on contact_email, so a repeat returns 409 conflict with the existing affiliate_id. Only name and contact_email are required; send "status": "pending" to queue a self-registered partner for review.

How is rate limiting and pagination handled?

Requests are throttled per account; on HTTP 429 retry with exponential backoff. List endpoints take page (1-based) and per_page (max 200, default 50); use meta.total_pages to drive your paging loop, as the Python example above does.

Do the postback and the API share a key?

No. The tracking endpoints (/c, /conv, sdk.js) run on your own tracking domain and identify traffic by the offer, campaign and click parameters, not an API key. The Partner API keys (llj_live_...) are for reading and writing your account data. See the postback URL builder for the tracking side and the API reference for the account side.

Is there an OpenAPI spec I can generate a client from?

Yes. The interactive API reference is driven by a published OpenAPI 3 document you can point a code generator at, and the same spec is what generates the MCP server's tools, so the API, the reference, and the AI tools never drift apart.

Start with a key and a curl.

Open the interactive reference, create a scoped key, and make your first call in a minute. Building something bigger? Book a demo and we'll walk the API, the SDK and the MCP server against your kind of program.

Not ready to build?

Get launch notes and API updates. No spam, unsubscribe anytime.