> ## Documentation Index
> Fetch the complete documentation index at: https://docs.datalane.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Create, enrich, monitor, and export a campaign in Python.

This walkthrough runs a complete enrichment in Python. You need an API key from your DataLane workspace settings and the `requests` package (`pip install requests`).

## 1. Set up a client

```python theme={null}
import os
import time

import requests

BASE_URL = "https://api.datalane.com"

session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['DATALANE_API_KEY']}"


def api(path: str, body: dict | None = None) -> dict:
    response = session.post(f"{BASE_URL}{path}", json=body or {})
    payload = response.json()
    if payload["error"] is not None:
        raise RuntimeError(f"{payload['error']['code']}: {payload['error']['message']}")
    return payload["data"]
```

Every endpoint is a `POST` returning the same envelope, so one helper covers the whole API: it returns `data` on success and raises the [error code](/guides/errors) otherwise. Keep the key server-side and out of source control.

## 2. Check your organization

```python theme={null}
org = api("/v1/org/describe")
print(org["credits"]["smb"])
```

```json Response theme={null}
{
  "data": {
    "organization": {
      "id": "org_8fKtVbW3nQzYxP2cMjHu4",
      "name": "Acme Robotics"
    },
    "credits": {
      "smb": 1000,
      "researchAgents": 500
    },
    "enrichment": {
      "enrichingAccounts": 0,
      "maxEnrichingAccounts": 10000
    }
  },
  "error": null
}
```

Enrichment consumes one SMB credit per eligible account, so `credits.smb` is the budget for everything that follows. `credits.researchAgents` is a separate balance for Research Agent runs. `enrichment.enrichingAccounts` counts accounts currently enriching across the organization; new launches must fit within `enrichment.maxEnrichingAccounts`.

## 3. Create a campaign

```python theme={null}
campaign = api("/v1/campaigns/create", {
    "name": "July target accounts",
    "accountIds": [
        "9feafba5-78e5-4e19-b577-1455b0d2018c",
        "ae46f8b8-4b9d-43ee-96e5-744943f6863a",
    ],
    "excludeEnrichedWithinDays": 30,
})
campaign_id = campaign["id"]
```

```json Response theme={null}
{
  "data": {
    "id": "camp_4Zw7kQhX2mRfT9pJcN3Ld",
    "status": "draft",
    "createdVia": "api",
    "name": "July target accounts",
    "accounts": {
      "requested": 2,
      "eligible": null,
      "excluded": null
    },
    "credits": { "charged": 0, "remaining": 1000 },
    "excludeEnrichedWithinDays": 30,
    "lastEnrichment": null,
    "createdAt": "2026-07-21T14:00:00.000Z"
  },
  "error": null
}
```

Account IDs must be UUIDs; duplicates are counted once in `accounts.requested`. The campaign starts as a `draft`; nothing is charged, and `accounts.eligible` and `accounts.excluded` stay `null` until enrichment evaluates the list. `excludeEnrichedWithinDays` tells the launch to skip accounts enriched within the last 30 days; it can be changed with `/v1/campaigns/update` while the campaign is still a `draft`.

## 4. Start enrichment

```python theme={null}
campaign = api("/v1/campaigns/enrich", {"campaignId": campaign_id})
```

```json Response theme={null}
{
  "data": {
    "id": "camp_4Zw7kQhX2mRfT9pJcN3Ld",
    "status": "enriching",
    "createdVia": "api",
    "name": "July target accounts",
    "accounts": {
      "requested": 2,
      "eligible": 2,
      "excluded": {
        "total": 0,
        "invalid": 0,
        "recentlyEnriched": 0,
        "crmExcluded": 0
      }
    },
    "credits": { "charged": 2, "remaining": 998 },
    "excludeEnrichedWithinDays": 30,
    "lastEnrichment": null,
    "createdAt": "2026-07-21T14:00:00.000Z"
  },
  "error": null
}
```

DataLane validates the list, applies the campaign's recency exclusion, charges one credit per remaining account, and starts finding contacts. The call returns once the campaign is `enriching`. If it raises `enrichment_pending`, the launch is still in progress. Repeat the same call after a short backoff.

## 5. Poll until enriched

```python theme={null}
delay = 2
while campaign["status"] == "enriching":
    time.sleep(delay)
    delay = min(delay * 2, 60)
    campaign = api("/v1/campaigns/get", {"campaignId": campaign_id})
    print("status:", campaign["status"])
```

The loop ends on `enriched`:

```json Response theme={null}
{
  "data": {
    "id": "camp_4Zw7kQhX2mRfT9pJcN3Ld",
    "status": "enriched",
    "createdVia": "api",
    "name": "July target accounts",
    "accounts": {
      "requested": 2,
      "eligible": 2,
      "excluded": {
        "total": 0,
        "invalid": 0,
        "recentlyEnriched": 0,
        "crmExcluded": 0
      }
    },
    "credits": { "charged": 2, "remaining": 998 },
    "excludeEnrichedWithinDays": 30,
    "lastEnrichment": null,
    "createdAt": "2026-07-21T14:00:00.000Z",
    "completedAt": "2026-07-21T14:06:12.000Z",
    "resultsExpireAt": "2027-01-21T23:59:59.999Z",
    "results": {
      "totalAccounts": 2,
      "totalContactsFound": 9,
      "pctAccountsWithContact": 1,
      "pctAccountsWithMobile": 0.5
    }
  },
  "error": null
}
```

Results stay exportable until `resultsExpireAt`. If the loop ends on `draft` instead, the attempt failed or was canceled. `lastEnrichment` reports what happened and whether its credits were refunded. See [Campaign lifecycle](/guides/campaign-lifecycle).

## 6. Export a CSV

```python theme={null}
export = api("/v1/campaigns/export", {
    "campaignId": campaign_id,
    "maxContactsPerAccount": 5,
})

download = requests.get(export["url"], timeout=300)
download.raise_for_status()
with open("campaign.csv", "wb") as file:
    file.write(download.content)
```

```json Response theme={null}
{
  "data": {
    "campaignId": "camp_4Zw7kQhX2mRfT9pJcN3Ld",
    "url": "https://storage.datalane.com/exports/...",
    "urlExpiresAt": "2026-07-21T15:06:30.000Z",
    "rows": { "accounts": 2, "contacts": 9 },
    "file": { "format": "csv", "encoding": "utf-8-bom" }
  },
  "error": null
}
```

`url` is presigned. Fetch it with a plain `requests.get`, without an `Authorization` header, and treat it as a secret. It stays valid for one hour; each export call builds a fresh file, so call it again to change filters or get a new URL. See [Export results](/guides/export-results) for contact caps and filters.

## Next steps

* [Campaign lifecycle](/guides/campaign-lifecycle): states, safe retries, and polling guidance
* [Errors and retries](/guides/errors): every error code and which ones to retry
* [API reference](/api-reference/overview): full schemas and an interactive playground
