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. Create one from the dashboard:

  1. Open app.leadrails.dev/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",
  "client_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.

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_system": "wordpress"
  }' | jq

The response includes the source_id AND the plaintext signing_secret. The plaintext secret is shown once. Capture it — the upstream form-handler will need it to sign inbound HMAC requests.

{
  "source_id": "src_01J5Z7N9X3M2VWPQ9YTBN2F0HR",
  "name": "Website contact form",
  "source_system": "wordpress",
  "status": "active",
  "signing_secret": "sk_src_..."
}

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_system: "wordpress",
  },
});
if (error) throw new Error(error.title);
const sourceId = data.source_id;

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, not the admin REST API. The intake surface requires HMAC signing — the dashboard ships an in-browser test-event sender that does the signing for you. Easiest path:

  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 the admin API will have a new event row.

Full server-to-server HMAC signing is documented under HMAC signing guide — it's the right path once you're integrating from your own backend.

6. Confirm delivery

Poll GET /v1/events to see what happened. The list is sorted newest-first; the status tells you whether each destination delivery succeeded, is retrying, or failed.

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
}

/v1/events requires a Pro+ plan. On Starter the call returns 403 plan-required; the same data lives in the dashboard under Events on every plan.

What's next

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