---
title: Integrate looot into your app
description: Call any looot job or tool from your own backend with one HTTP request. API key, first call, reading the answer, retries, long runs, files and costs.
---

Your app can use every looot job and tool directly, without an AI agent: a CRM that finds work
emails for new leads, an internal dashboard that enriches companies, a pipeline that pulls SEO
rankings every night. It is one REST API with one key and one prepaid balance.

## 1. Get an API key

In the dashboard, open **Settings, API keys** and create a key for your app. Give it a monthly
spending limit so a bug in your code can never spend your whole balance. Keep the key on your
server, never in a browser or mobile app.

```bash
export LOOOT_TOKEN="your key"
```

## 2. Pick a job or a tool

- A **job** is a task, like `people.email.find`. looot picks a provider for you, and with
  `fallback` on it tries the next one if the first finds nothing. Start here.
- A **tool** is one provider's endpoint, like `hunter-email-finder`. Use it when you need that
  exact provider.

Browse them all in [Jobs](/reference/jobs) and [Tools](/reference/tools). Each page lists the
inputs, the providers and their prices, and ready code.

## 3. Make the call

One request starts the run and, with `wait`, returns the answer in the same response.

```bash
curl -X POST "https://api.looot.ai/v1/runs" \
  -H "Authorization: Bearer $LOOOT_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: lead-4821-email" \
  -d '{"endpointId":"job:people.email.find","input":{"first_name":"Jane","last_name":"Doe","domain":"example.com"},"fallback":true,"wait":30}'
```

```js
const response = await fetch("https://api.looot.ai/v1/runs", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.LOOOT_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `lead-${lead.id}-email`,
  },
  body: JSON.stringify({
    endpointId: "job:people.email.find",
    input: { first_name: lead.firstName, last_name: lead.lastName, domain: lead.domain },
    fallback: true,
    wait: 30,
  }),
});
const run = await response.json();
```

```python
import os, requests

run = requests.post(
    "https://api.looot.ai/v1/runs",
    headers={
        "Authorization": f"Bearer {os.environ['LOOOT_TOKEN']}",
        "Idempotency-Key": f"lead-{lead_id}-email",
    },
    json={
        "endpointId": "job:people.email.find",
        "input": {"first_name": "Jane", "last_name": "Doe", "domain": "example.com"},
        "fallback": True,
        "wait": 30,
    },
    timeout=70,
).json()
```

## 4. Read the answer

| Field | What it holds |
| --- | --- |
| `status` | `completed`, `failed`, or `queued` and `running` while it works |
| `outcome` | `hit` (found it), `weak`, `miss` (nothing found), `error`, `rejected` (bad input) |
| `normalized` | The answer in looot's own field names, the same whichever provider answered (for example `normalized.email`) |
| `result` | The provider's raw answer, as it sent it |
| `pricing.finalChargeMicros` | What this run cost, in millionths of a dollar |
| `route` | With `fallback`: every provider tried, in order, and which one answered |

Read `normalized` first. It doesn't change when looot switches provider.

## 5. Retries never pay twice

Send the same `Idempotency-Key` again and you get the same run back, never a second charge. Build
the key from your own record (`lead-4821-email`), so a crashed worker can safely retry.

## 6. Long runs

`wait` holds the request for up to 60 seconds. If the run takes longer, you get it back with
`status: "queued"` or `"running"`. Poll it:

```bash
curl "https://api.looot.ai/v1/runs/RUN_ID" -H "Authorization: Bearer $LOOOT_TOKEN"
```

## 7. Files and text answers

Some tools answer with a file (speech, an image, a video, a PDF) or with CSV or plain text.

- Small text and CSV answers come back in `result.text`, with `result.format` (`csv`, `text`,
  `html`, `xml`, `markdown`).
- Files and large answers come back as `result.media`, with a `downloadPath`. Download it with
  your key: `GET https://api.looot.ai` + `downloadPath`.

## 8. What it costs

- Every run reserves its maximum price first, then charges what the provider really billed. The
  rest goes back to your balance.
- A run that fails at the provider costs $0. A job run that finds nothing usually costs $0.
- Check your balance with `GET /v1/balance` and top up in the dashboard.

## 9. Errors to handle

| HTTP | Code | What to do |
| --- | --- | --- |
| 402 | `insufficient_balance` | Top up, then retry with a new idempotency key |
| 402 | `key_spend_limit_reached` | This key hit its monthly limit. Raise it in Settings, API keys |
| 400 | `validation_error` | The input doesn't match the tool. `error.details` names the field |
| 429 | `too_many_inflight_runs` | Slow down, then retry |

Every error has `error.code`, `error.message` and `error.retryable`.

## Write actions

Some tools change data in an account you connected (send an email, create a CRM contact, delete a
file). Their pages say **Write action** at the top, and `GET /v1/operations/{id}` returns
`effectClassification: "effectful"`. Call those only on purpose.

