LeadRails

Quickstart

Go from no account to a delivered test lead in five minutes. Every step uses the live API. Copy, paste, run.

Before you start

You need:

  • A LeadRails workspace. Sign up free if you don't have one.
  • A terminal with curl, OR Node 22+ for the TypeScript examples.
  • One destination ready to receive a test event. The fastest is a Slack incoming webhook URL — grab one from Slack's webhook setup if you don't already have one.

Through this guide, replace $LR_KEY with your real API key (created in step 1). Replace placeholder IDs like cli_01J... and src_01J... with the ones the API returns to you.

1. Create an API key

API keys are workspace-scoped. They authorize every call to the v1 surface. See Authentication for the prefix scheme, rotation, and revocation; the short version is below. Create one from the dashboard:

  1. Open app.leadrails.dev/app/settings/api-keys.
  2. Click Create key. Name it something memorable (e.g. "local-dev").
  3. Copy the full key — it starts with lr_live_ and is shown once. You will not be able to retrieve the plaintext again.

Export it into your shell:

export LR_KEY="lr_live_yourkeyhere"

Verify the key works by calling GET /v1/me — the cheapest round-trip on the API. It returns your workspace identity:

curl -s https://api.leadrails.dev/v1/me \
  -H "Authorization: Bearer $LR_KEY" | jq

Expected response:

{
  "client_id": "cli_01J5Z7K3M8X4VWPQ9YTBN2F0HR",
  "name": "Acme Plumbing",
  "plan": "starter"
}

A 401 means the key is wrong or revoked. See Errors for the full list.

2. Create a source

A source is where leads come from — typically your website's contact form. Each source has a signing secret that the upstream form-handler uses to authenticate inbound posts.

Every POST and PATCH on the v1 surface requires an Idempotency-Key header. Generate a fresh value (a UUID is fine) per logical operation. If you retry the same request with the same key + body, the API returns the original result instead of creating a duplicate. See Idempotency for the retention window and conflict semantics.

Curl:

curl -s -X POST https://api.leadrails.dev/v1/sources \
  -H "Authorization: Bearer $LR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "name": "Website contact form",
    "source_type": "wordpress"
  }' | jq

The response is the created source. Note its id (a src_-prefixed ULID) — you'll need it to wire a route in step 4. Creating a source does not return a signing secret; mint one with the rotate-secret step below.

{
  "id": "src_01J5Z7N9X3M2VWPQ9YTBN2F0HR",
  "client_id": "cli_01J5Z7K3M8X4VWPQ9YTBN2F0HR",
  "name": "Website contact form",
  "source_type": "wordpress",
  "status": "active",
  "allowed_site_url": null,
  "auth_mode": "hmac_v1",
  "schema_version": "lead_event.v1",
  "created_at": "2026-01-01T00:00:00.000Z",
  "updated_at": "2026-01-01T00:00:00.000Z"
}

TypeScript (openapi-fetch):

import createClient from "openapi-fetch";
import type { paths } from "./lr-openapi-types";  // see the reference page

const lr = createClient<paths>({
  baseUrl: "https://api.leadrails.dev",
  headers: { Authorization: `Bearer ${process.env.LR_KEY}` },
});

const { data, error } = await lr.POST("/v1/sources", {
  headers: { "Idempotency-Key": crypto.randomUUID() },
  body: {
    name: "Website contact form",
    source_type: "wordpress",
  },
});
if (error) throw new Error(error.title);
const sourceId = data.id;

Mint a signing secret

Creating a source does not mint a signing secret. Generate one with POST /v1/sources/{id}/rotate-secret — the plaintext signing_secret is returned exactly once. Capture it; the upstream form-handler needs it to sign inbound HMAC requests. There is no endpoint to recover it later, and rotating again revokes the previous secret.

curl -s -X POST https://api.leadrails.dev/v1/sources/src_01J5Z7N9X3M2VWPQ9YTBN2F0HR/rotate-secret \
  -H "Authorization: Bearer $LR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" | jq

Response:

{
  "key_id": "key_01J5Z7P4B6M2VWPQ9YTBN2F0HR",
  "signing_secret": "k7Yx9p2sQv...g4Qe=",
  "created_at": "2026-01-01T00:00:00.000Z"
}

3. Create a destination

A destination is where a lead lands. The adapter type controls how the payload gets shaped. We'll use a Slack webhook here because it's the fastest to verify visually.

curl -s -X POST https://api.leadrails.dev/v1/destinations \
  -H "Authorization: Bearer $LR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "name": "Team Slack — new leads",
    "adapter_type": "slack_webhook",
    "config": {
      "webhook_url": "https://hooks.slack.com/services/T000/B000/XXX"
    }
  }' | jq

Response:

{
  "destination_id": "dst_01J5Z7Q1A2M2VWPQ9YTBN2F0HR",
  "name": "Team Slack — new leads",
  "adapter_type": "slack_webhook",
  "status": "active"
}

If the webhook URL fails the safe-outbound-URL check (private network, unsupported scheme, etc.) the API returns a 422 unsafe-url problem. Fix the URL and retry.

4. Wire a route

A route connects a source to a destination. One source can fan out to many destinations by creating multiple routes against the same source.

curl -s -X POST https://api.leadrails.dev/v1/routes \
  -H "Authorization: Bearer $LR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "source_id": "src_01J5Z7N9X3M2VWPQ9YTBN2F0HR",
    "destination_id": "dst_01J5Z7Q1A2M2VWPQ9YTBN2F0HR",
    "name": "Form → Slack"
  }' | jq

Cross-workspace IDs are rejected with 400 invalid-reference. The source_id and destination_id must both live in the workspace the API key belongs to.

5. Fire a test event

Test events go through the intake surface (intake.leadrails.dev), not the admin REST API. The intake surface requires HMAC signing. Two paths:

From the dashboard (fastest)

  1. Open your sources list in the dashboard.
  2. Click your new source.
  3. Click Send test event.

Within seconds, your Slack channel should show a message and step 6 below will return a new event row. The in-browser sender signs the request server-side for you — useful for sanity-checking the route. It is not the production path.

From your own backend (production path)

Sign the event yourself and POST it to the intake surface. The HMAC signing page documents the wire contract end to end — every required header, the canonical base string, the hash, and the encoding — so you can integrate from any runtime (Node, Bun, Deno, Cloudflare Workers, Vercel Edge, or Vercel Functions).

Set four env vars on your backend (the dashboard hands them to you when you create the source key — capture the signing secret on creation, it's shown once):

LEADRAILS_CLIENT_ID=cli_01J...
LEADRAILS_SOURCE_ID=src_01J...
LEADRAILS_KEY_ID=key_01J...
LEADRAILS_SIGNING_SECRET=...      # server-only — never NEXT_PUBLIC_*,
                                  # VITE_*, PUBLIC_*, EXPO_PUBLIC_*,
                                  # NUXT_PUBLIC_*, GATSBY_*, REACT_APP_*

See the HMAC signing page for the full signing algorithm, a worked end-to-end example, and the stable error codes the intake surface returns.

6. Confirm delivery

From the dashboard (works on every plan)

Open Events in the dashboard. The list is sorted newest-first; the status on each delivery tells you whether it succeeded, is retrying, or failed. Your test event should appear within seconds.

From the events API (Pro+)

If you're on a Pro+ plan you can also poll GET /v1/events to see the same data over the API. On Starter the call returns 403 plan-required — use the dashboard above instead.

curl -s "https://api.leadrails.dev/v1/events?limit=5" \
  -H "Authorization: Bearer $LR_KEY" | jq

Response:

{
  "data": [
    {
      "event_id": "evt_01J5Z7S5K3M2VWPQ9YTBN2F0HR",
      "source_id": "src_01J5Z7N9X3M2VWPQ9YTBN2F0HR",
      "received_at": "2026-05-23T14:02:11Z",
      "deliveries": [
        {
          "destination_id": "dst_01J5Z7Q1A2M2VWPQ9YTBN2F0HR",
          "status": "delivered",
          "delivered_at": "2026-05-23T14:02:12Z",
          "attempts": 1
        }
      ]
    }
  ],
  "next_cursor": null
}

What's next

  • Browse the full API reference — every endpoint, every field.
  • Authentication — API key prefix scheme, rotation, revocation, and workspace scoping.
  • Rate limits — per-key budgets, the RateLimit-* headers, and backoff.
  • Idempotency — the Idempotency-Key contract, retention, and conflicts.
  • Webhooks — verify the HMAC signature on outbound Generic Webhook deliveries.
  • Install the MCP server — talk to LeadRails from Claude Desktop or Cursor.
  • Read the errors page — every type URL the API emits, with the fix.
  • HMAC signing — the intake surface wire contract for sending signed events from your own backend: headers, base string, hash, and encoding.

Stuck? Email us. Include the request_id header from any failing response — we use it to find your request in the logs.