Python SDK Enrichment: Developer Quickstart Guide

Install, authenticate, run enrichments and waterfalls, handle tasks and errors. Every sample checked against databar 2.5.1.

var(--variable-yLy1gAThf)

David Abaev

Founder at Databar

Blog

— min read

Python SDK Enrichment: Developer Quickstart Guide

Python SDK Enrichment: Developer Quickstart Guide

Install, authenticate, run enrichments and waterfalls, handle tasks and errors. Every sample checked against databar 2.5.1.

var(--variable-yLy1gAThf)

David Abaev

Founder at Databar

Blog

— min read

Python SDK Enrichment: Developer Quickstart Guide

Build your dream workflow with Databar today.

You want to run enrichment from Python: inside a data pipeline, a CRM sync, or a lead qualification step. The Databar Python SDK wraps the same REST API the app runs on, so every enrichment and every waterfall in the catalog is one method call away. This quickstart takes you from install to a parsed result.

Everything below is written against databar 2.5.1 and checked method by method against the package. If a method here does not exist in your version, run pip install -U databar and check the signature with help(DatabarClient.run_enrichment).

Install

Python 3.9 or newer. The only dependency is httpx.

pip install databar
pip install databar
pip install databar
pip install databar

The package also ships a CLI, which is the fastest way to store your key and to see agent-ready setup instructions:

databar login        # saves your API key
databar agent-guide  # setup notes for Claude Code, Cursor and other agents
databar login        # saves your API key
databar agent-guide  # setup notes for Claude Code, Cursor and other agents
databar login        # saves your API key
databar agent-guide  # setup notes for Claude Code, Cursor and other agents
databar login        # saves your API key
databar agent-guide  # setup notes for Claude Code, Cursor and other agents

Authenticate

Create an API key in the app under Settings, then pass it to the client or leave it in the environment. The client sends it as an x-apikey header on every request.

import os
from databar import DatabarClient

client = DatabarClient(api_key=os.environ["DATABAR_API_KEY"])

user = client.get_user()
print(user.email, user.plan, user.balance)
import os
from databar import DatabarClient

client = DatabarClient(api_key=os.environ["DATABAR_API_KEY"])

user = client.get_user()
print(user.email, user.plan, user.balance)
import os
from databar import DatabarClient

client = DatabarClient(api_key=os.environ["DATABAR_API_KEY"])

user = client.get_user()
print(user.email, user.plan, user.balance)
import os
from databar import DatabarClient

client = DatabarClient(api_key=os.environ["DATABAR_API_KEY"])

user = client.get_user()
print(user.email, user.plan, user.balance)

get_user() is the cheapest call in the SDK and the one to use as a health check: it returns your email, plan, workspace and current credit balance. The client is also a context manager, so it closes its connection pool for you:

from databar import DatabarClient

# api_key is optional: the client reads DATABAR_API_KEY from the environment
with DatabarClient() as client:
    print(client.get_user().balance)
from databar import DatabarClient

# api_key is optional: the client reads DATABAR_API_KEY from the environment
with DatabarClient() as client:
    print(client.get_user().balance)
from databar import DatabarClient

# api_key is optional: the client reads DATABAR_API_KEY from the environment
with DatabarClient() as client:
    print(client.get_user().balance)
from databar import DatabarClient

# api_key is optional: the client reads DATABAR_API_KEY from the environment
with DatabarClient() as client:
    print(client.get_user().balance)

The constructor takes five arguments, and the defaults are sensible for scripts:

client = DatabarClient(
    api_key=os.environ["DATABAR_API_KEY"],
    base_url="https://api.databar.ai/v1",  # default
    timeout=30.0,            # seconds per HTTP request
    max_poll_attempts=150,   # polling gives up after 150 attempts
    poll_interval_s=2.0,     # 2s between polls, so ~5 minutes total
)
client = DatabarClient(
    api_key=os.environ["DATABAR_API_KEY"],
    base_url="https://api.databar.ai/v1",  # default
    timeout=30.0,            # seconds per HTTP request
    max_poll_attempts=150,   # polling gives up after 150 attempts
    poll_interval_s=2.0,     # 2s between polls, so ~5 minutes total
)
client = DatabarClient(
    api_key=os.environ["DATABAR_API_KEY"],
    base_url="https://api.databar.ai/v1",  # default
    timeout=30.0,            # seconds per HTTP request
    max_poll_attempts=150,   # polling gives up after 150 attempts
    poll_interval_s=2.0,     # 2s between polls, so ~5 minutes total
)
client = DatabarClient(
    api_key=os.environ["DATABAR_API_KEY"],
    base_url="https://api.databar.ai/v1",  # default
    timeout=30.0,            # seconds per HTTP request
    max_poll_attempts=150,   # polling gives up after 150 attempts
    poll_interval_s=2.0,     # 2s between polls, so ~5 minutes total
)

Two of those matter more than the rest. timeout is per HTTP request, not per enrichment. max_poll_attempts and poll_interval_s together set how long the blocking helpers wait for a provider before they raise: 150 attempts at 2 seconds is about five minutes.

Ready to automate your data enrichment? Access 100+ data providers through our SDK

Find the right enrichment

Every enrichment has a numeric id, a price in credits, a set of input parameters and a set of response fields. Look them up instead of guessing, because parameter names differ per provider.

# Search the catalog: 'q' matches name and description
for e in client.list_enrichments(q="email", limit=10):
    print(e.id, e.name, "|", e.data_source, "|", e.price, "credits")

# Then read the full spec: parameters in, fields out
enrichment = client.get_enrichment(1591)
print(enrichment.name, enrichment.price)

for param in enrichment.params or []:
    print("param:", param.name, "required" if param.is_required else "optional")

for field in enrichment.response_fields or []:
    print("returns:", field.name)
# Search the catalog: 'q' matches name and description
for e in client.list_enrichments(q="email", limit=10):
    print(e.id, e.name, "|", e.data_source, "|", e.price, "credits")

# Then read the full spec: parameters in, fields out
enrichment = client.get_enrichment(1591)
print(enrichment.name, enrichment.price)

for param in enrichment.params or []:
    print("param:", param.name, "required" if param.is_required else "optional")

for field in enrichment.response_fields or []:
    print("returns:", field.name)
# Search the catalog: 'q' matches name and description
for e in client.list_enrichments(q="email", limit=10):
    print(e.id, e.name, "|", e.data_source, "|", e.price, "credits")

# Then read the full spec: parameters in, fields out
enrichment = client.get_enrichment(1591)
print(enrichment.name, enrichment.price)

for param in enrichment.params or []:
    print("param:", param.name, "required" if param.is_required else "optional")

for field in enrichment.response_fields or []:
    print("returns:", field.name)
# Search the catalog: 'q' matches name and description
for e in client.list_enrichments(q="email", limit=10):
    print(e.id, e.name, "|", e.data_source, "|", e.price, "credits")

# Then read the full spec: parameters in, fields out
enrichment = client.get_enrichment(1591)
print(enrichment.name, enrichment.price)

for param in enrichment.params or []:
    print("param:", param.name, "required" if param.is_required else "optional")

for field in enrichment.response_fields or []:
    print("returns:", field.name)

list_enrichments() returns what your workspace is authorized to run by default. Pass authorized_only=False to see the whole catalog, and category="..." to narrow it.

Run your first enrichment

The blocking helper submits the run, polls it for you and returns the data:

# Use the exact param names from get_enrichment(...).params
data = client.run_enrichment_sync(
    enrichment_id=1591,
    params={"linkedin_url": "https://www.linkedin.com/in/janesmith/"},
)

print(data)  # dict of the enrichment's response fields
# Use the exact param names from get_enrichment(...).params
data = client.run_enrichment_sync(
    enrichment_id=1591,
    params={"linkedin_url": "https://www.linkedin.com/in/janesmith/"},
)

print(data)  # dict of the enrichment's response fields
# Use the exact param names from get_enrichment(...).params
data = client.run_enrichment_sync(
    enrichment_id=1591,
    params={"linkedin_url": "https://www.linkedin.com/in/janesmith/"},
)

print(data)  # dict of the enrichment's response fields
# Use the exact param names from get_enrichment(...).params
data = client.run_enrichment_sync(
    enrichment_id=1591,
    params={"linkedin_url": "https://www.linkedin.com/in/janesmith/"},
)

print(data)  # dict of the enrichment's response fields

That is a single call and a single result. The async pair gives you the task id, which is what you want in a queue or a web request:

run = client.run_enrichment(
    enrichment_id=1591,
    params={"linkedin_url": "https://www.linkedin.com/in/janesmith/"},
)
print(run.task_id, run.status)   # "processing"

# Check without blocking
task = client.get_task(run.task_id)
print(task.status)               # processing | completed | no_data | failed | ...

# Or hand it to the poller, which returns the data payload
data = client.poll_task(run.task_id)
run = client.run_enrichment(
    enrichment_id=1591,
    params={"linkedin_url": "https://www.linkedin.com/in/janesmith/"},
)
print(run.task_id, run.status)   # "processing"

# Check without blocking
task = client.get_task(run.task_id)
print(task.status)               # processing | completed | no_data | failed | ...

# Or hand it to the poller, which returns the data payload
data = client.poll_task(run.task_id)
run = client.run_enrichment(
    enrichment_id=1591,
    params={"linkedin_url": "https://www.linkedin.com/in/janesmith/"},
)
print(run.task_id, run.status)   # "processing"

# Check without blocking
task = client.get_task(run.task_id)
print(task.status)               # processing | completed | no_data | failed | ...

# Or hand it to the poller, which returns the data payload
data = client.poll_task(run.task_id)
run = client.run_enrichment(
    enrichment_id=1591,
    params={"linkedin_url": "https://www.linkedin.com/in/janesmith/"},
)
print(run.task_id, run.status)   # "processing"

# Check without blocking
task = client.get_task(run.task_id)
print(task.status)               # processing | completed | no_data | failed | ...

# Or hand it to the poller, which returns the data payload
data = client.poll_task(run.task_id)

Task statuses are processing, no_data, completed, partially_completed, failed, cancelled and gone. Results live for 24 hours, after which the status becomes gone and you have to re-run. Store what you need as soon as the task completes.

Bulk enrichment

For lists, send all the inputs in one call instead of looping. The platform runs them in parallel and bills per result.

inputs = [
    {"linkedin_url": "https://www.linkedin.com/in/janesmith/"},
    {"linkedin_url": "https://www.linkedin.com/in/bobjohnson/"},
    {"linkedin_url": "https://www.linkedin.com/in/sarahlee/"},
]

results = client.run_enrichment_bulk_sync(enrichment_id=1591, params=inputs)

# One element per input, in input order. None means that input returned nothing.
for src, result in zip(inputs, results):
    if result is None:
        print(src["linkedin_url"], "-> no data")
    else:
        print(src["linkedin_url"], "->", result)
inputs = [
    {"linkedin_url": "https://www.linkedin.com/in/janesmith/"},
    {"linkedin_url": "https://www.linkedin.com/in/bobjohnson/"},
    {"linkedin_url": "https://www.linkedin.com/in/sarahlee/"},
]

results = client.run_enrichment_bulk_sync(enrichment_id=1591, params=inputs)

# One element per input, in input order. None means that input returned nothing.
for src, result in zip(inputs, results):
    if result is None:
        print(src["linkedin_url"], "-> no data")
    else:
        print(src["linkedin_url"], "->", result)
inputs = [
    {"linkedin_url": "https://www.linkedin.com/in/janesmith/"},
    {"linkedin_url": "https://www.linkedin.com/in/bobjohnson/"},
    {"linkedin_url": "https://www.linkedin.com/in/sarahlee/"},
]

results = client.run_enrichment_bulk_sync(enrichment_id=1591, params=inputs)

# One element per input, in input order. None means that input returned nothing.
for src, result in zip(inputs, results):
    if result is None:
        print(src["linkedin_url"], "-> no data")
    else:
        print(src["linkedin_url"], "->", result)
inputs = [
    {"linkedin_url": "https://www.linkedin.com/in/janesmith/"},
    {"linkedin_url": "https://www.linkedin.com/in/bobjohnson/"},
    {"linkedin_url": "https://www.linkedin.com/in/sarahlee/"},
]

results = client.run_enrichment_bulk_sync(enrichment_id=1591, params=inputs)

# One element per input, in input order. None means that input returned nothing.
for src, result in zip(inputs, results):
    if result is None:
        print(src["linkedin_url"], "-> no data")
    else:
        print(src["linkedin_url"], "->", result)

The payload is aligned to your inputs: same length, same order, None where a row returned nothing. That alignment is the reason to prefer bulk over a loop, because you can zip results back onto your source rows without matching keys. For when to batch and when to go real time, see batch vs real-time enrichment.

One SDK, 100+ data providers, zero manual enrichment

Waterfalls

A waterfall enrichment sends each row through several providers in order and stops at the first usable answer. In the SDK a waterfall has a string identifier rather than a numeric id, and you choose which providers run and in what order.

for wf in client.list_waterfalls():
    print(wf.identifier, "|", wf.name)
    print("   inputs:", [p.get("name") for p in wf.input_params])
    print("   providers:", [(e.id, e.name, e.price) for e in wf.available_enrichments])
    print("   verifiers:", wf.email_verifiers if wf.is_email_verifying else "none")
for wf in client.list_waterfalls():
    print(wf.identifier, "|", wf.name)
    print("   inputs:", [p.get("name") for p in wf.input_params])
    print("   providers:", [(e.id, e.name, e.price) for e in wf.available_enrichments])
    print("   verifiers:", wf.email_verifiers if wf.is_email_verifying else "none")
for wf in client.list_waterfalls():
    print(wf.identifier, "|", wf.name)
    print("   inputs:", [p.get("name") for p in wf.input_params])
    print("   providers:", [(e.id, e.name, e.price) for e in wf.available_enrichments])
    print("   verifiers:", wf.email_verifiers if wf.is_email_verifying else "none")
for wf in client.list_waterfalls():
    print(wf.identifier, "|", wf.name)
    print("   inputs:", [p.get("name") for p in wf.input_params])
    print("   providers:", [(e.id, e.name, e.price) for e in wf.available_enrichments])
    print("   verifiers:", wf.email_verifiers if wf.is_email_verifying else "none")
wf = client.get_waterfall("email_getter")   # use an identifier from list_waterfalls()

data = client.run_waterfall_sync(
    identifier=wf.identifier,
    params={"first_name": "Jane", "last_name": "Smith", "domain": "acme.com"},
    enrichments=[e.id for e in wf.available_enrichments],  # provider order = list order
    # email_verifier=<id from wf.email_verifiers>

wf = client.get_waterfall("email_getter")   # use an identifier from list_waterfalls()

data = client.run_waterfall_sync(
    identifier=wf.identifier,
    params={"first_name": "Jane", "last_name": "Smith", "domain": "acme.com"},
    enrichments=[e.id for e in wf.available_enrichments],  # provider order = list order
    # email_verifier=<id from wf.email_verifiers>

wf = client.get_waterfall("email_getter")   # use an identifier from list_waterfalls()

data = client.run_waterfall_sync(
    identifier=wf.identifier,
    params={"first_name": "Jane", "last_name": "Smith", "domain": "acme.com"},
    enrichments=[e.id for e in wf.available_enrichments],  # provider order = list order
    # email_verifier=<id from wf.email_verifiers>

wf = client.get_waterfall("email_getter")   # use an identifier from list_waterfalls()

data = client.run_waterfall_sync(
    identifier=wf.identifier,
    params={"first_name": "Jane", "last_name": "Smith", "domain": "acme.com"},
    enrichments=[e.id for e in wf.available_enrichments],  # provider order = list order
    # email_verifier=<id from wf.email_verifiers>

The enrichments list is the provider order: first in the list runs first, and the cascade stops at the first usable result. email_verifier attaches a verification step so a candidate that fails verification falls through to the next provider instead of landing in your CRM. Bulk works the same way:

people = [
    {"first_name": "Jane", "last_name": "Smith", "domain": "acme.com"},
    {"first_name": "Bob", "last_name": "Johnson", "domain": "widgets.io"},
]

results = client.run_waterfall_bulk_sync(identifier="email_getter", params=people)

for person, result in zip(people, results):
    status = "no match" if result is None else result
    print(person["first_name"], person["last_name"], "->", status)
people = [
    {"first_name": "Jane", "last_name": "Smith", "domain": "acme.com"},
    {"first_name": "Bob", "last_name": "Johnson", "domain": "widgets.io"},
]

results = client.run_waterfall_bulk_sync(identifier="email_getter", params=people)

for person, result in zip(people, results):
    status = "no match" if result is None else result
    print(person["first_name"], person["last_name"], "->", status)
people = [
    {"first_name": "Jane", "last_name": "Smith", "domain": "acme.com"},
    {"first_name": "Bob", "last_name": "Johnson", "domain": "widgets.io"},
]

results = client.run_waterfall_bulk_sync(identifier="email_getter", params=people)

for person, result in zip(people, results):
    status = "no match" if result is None else result
    print(person["first_name"], person["last_name"], "->", status)
people = [
    {"first_name": "Jane", "last_name": "Smith", "domain": "acme.com"},
    {"first_name": "Bob", "last_name": "Johnson", "domain": "widgets.io"},
]

results = client.run_waterfall_bulk_sync(identifier="email_getter", params=people)

for person, result in zip(people, results):
    status = "no match" if result is None else result
    print(person["first_name"], person["last_name"], "->", status)

For the reasoning behind provider order and cost per verified contact, see how waterfall enrichment works, and the waterfalls Databar ships for the ready-made cascades. Phone cascades behave the same way with lower hit rates: phone enrichment for cold outbound.

Tables from code

Everything the app does with tables is in the SDK too, which is how you mix a UI workflow with a scripted one: build the table in Python, let your team work in it, or the reverse.

from databar import InsertRow, InsertOptions

table = client.create_table(name="Q3 inbound leads", columns=["email", "company"])

client.create_rows(
    table_uuid=table.identifier,
    rows=[
        InsertRow(fields={"email": "jane@acme.com", "company": "Acme"}),
        InsertRow(fields={"email": "bob@widgets.io", "company": "Widgets"}),
    ],
    options=InsertOptions(allow_new_columns=True),
)

# Attach an enrichment and map its params to columns
added = client.add_enrichment(
    table_uuid=table.identifier,
    enrichment_id=1591,
    mapping={"linkedin_url": "LinkedIn URL"},
    launch_strategy="run_on_click",      # or "run_on_update"
)

client.run_table_enrichment(
    table_uuid=table.identifier,
    enrichment_id=str(added.id),
    run_strategy="run_empty",            # run_all | run_empty | run_errors
)

rows = client.get_rows(table.identifier, per_page=100)
for row in rows.data:
    print(row)
if rows.has_next_page:
    more = client.get_rows(table.identifier, page=2)
from databar import InsertRow, InsertOptions

table = client.create_table(name="Q3 inbound leads", columns=["email", "company"])

client.create_rows(
    table_uuid=table.identifier,
    rows=[
        InsertRow(fields={"email": "jane@acme.com", "company": "Acme"}),
        InsertRow(fields={"email": "bob@widgets.io", "company": "Widgets"}),
    ],
    options=InsertOptions(allow_new_columns=True),
)

# Attach an enrichment and map its params to columns
added = client.add_enrichment(
    table_uuid=table.identifier,
    enrichment_id=1591,
    mapping={"linkedin_url": "LinkedIn URL"},
    launch_strategy="run_on_click",      # or "run_on_update"
)

client.run_table_enrichment(
    table_uuid=table.identifier,
    enrichment_id=str(added.id),
    run_strategy="run_empty",            # run_all | run_empty | run_errors
)

rows = client.get_rows(table.identifier, per_page=100)
for row in rows.data:
    print(row)
if rows.has_next_page:
    more = client.get_rows(table.identifier, page=2)
from databar import InsertRow, InsertOptions

table = client.create_table(name="Q3 inbound leads", columns=["email", "company"])

client.create_rows(
    table_uuid=table.identifier,
    rows=[
        InsertRow(fields={"email": "jane@acme.com", "company": "Acme"}),
        InsertRow(fields={"email": "bob@widgets.io", "company": "Widgets"}),
    ],
    options=InsertOptions(allow_new_columns=True),
)

# Attach an enrichment and map its params to columns
added = client.add_enrichment(
    table_uuid=table.identifier,
    enrichment_id=1591,
    mapping={"linkedin_url": "LinkedIn URL"},
    launch_strategy="run_on_click",      # or "run_on_update"
)

client.run_table_enrichment(
    table_uuid=table.identifier,
    enrichment_id=str(added.id),
    run_strategy="run_empty",            # run_all | run_empty | run_errors
)

rows = client.get_rows(table.identifier, per_page=100)
for row in rows.data:
    print(row)
if rows.has_next_page:
    more = client.get_rows(table.identifier, page=2)
from databar import InsertRow, InsertOptions

table = client.create_table(name="Q3 inbound leads", columns=["email", "company"])

client.create_rows(
    table_uuid=table.identifier,
    rows=[
        InsertRow(fields={"email": "jane@acme.com", "company": "Acme"}),
        InsertRow(fields={"email": "bob@widgets.io", "company": "Widgets"}),
    ],
    options=InsertOptions(allow_new_columns=True),
)

# Attach an enrichment and map its params to columns
added = client.add_enrichment(
    table_uuid=table.identifier,
    enrichment_id=1591,
    mapping={"linkedin_url": "LinkedIn URL"},
    launch_strategy="run_on_click",      # or "run_on_update"
)

client.run_table_enrichment(
    table_uuid=table.identifier,
    enrichment_id=str(added.id),
    run_strategy="run_empty",            # run_all | run_empty | run_errors
)

rows = client.get_rows(table.identifier, per_page=100)
for row in rows.data:
    print(row)
if rows.has_next_page:
    more = client.get_rows(table.identifier, page=2)

create_rows() auto-batches into chunks of 50, so you can hand it thousands of rows. run_strategy="run_empty" is the cheap default for re-runs: it only spends credits on cells that are still empty.

The data providers available through Databar

Errors

The SDK raises typed exceptions, all subclasses of DatabarError, and each carries status_code and response_body:

from databar import (
    DatabarAuthError,
    DatabarValidationError,
    DatabarInsufficientCreditsError,
    DatabarRateLimitError,
    DatabarTaskFailedError,
    DatabarTimeoutError,
    DatabarGoneError,
    DatabarError,
)

try:
    data = client.run_enrichment_sync(1591, {"linkedin_url": url})
except DatabarAuthError:
    raise SystemExit("Check DATABAR_API_KEY")
except DatabarValidationError as e:
    print("Bad input:", e, e.response_body)
except DatabarInsufficientCreditsError:
    print("Out of credits, or the plan does not allow this enrichment")
except DatabarRateLimitError as e:
    print("Rate limited:", e.response_body)   # the client already retried with backoff
except DatabarTaskFailedError as e:
    print("The provider run failed:", e, "task", e.task_id)
except DatabarTimeoutError:
    print("Still running after the poll budget. Keep the task_id and poll later")
except DatabarGoneError:
    print("Results expired (24h). Re-run the enrichment")
except DatabarError as e:
    print("Other API error:", e.status_code, e)
from databar import (
    DatabarAuthError,
    DatabarValidationError,
    DatabarInsufficientCreditsError,
    DatabarRateLimitError,
    DatabarTaskFailedError,
    DatabarTimeoutError,
    DatabarGoneError,
    DatabarError,
)

try:
    data = client.run_enrichment_sync(1591, {"linkedin_url": url})
except DatabarAuthError:
    raise SystemExit("Check DATABAR_API_KEY")
except DatabarValidationError as e:
    print("Bad input:", e, e.response_body)
except DatabarInsufficientCreditsError:
    print("Out of credits, or the plan does not allow this enrichment")
except DatabarRateLimitError as e:
    print("Rate limited:", e.response_body)   # the client already retried with backoff
except DatabarTaskFailedError as e:
    print("The provider run failed:", e, "task", e.task_id)
except DatabarTimeoutError:
    print("Still running after the poll budget. Keep the task_id and poll later")
except DatabarGoneError:
    print("Results expired (24h). Re-run the enrichment")
except DatabarError as e:
    print("Other API error:", e.status_code, e)
from databar import (
    DatabarAuthError,
    DatabarValidationError,
    DatabarInsufficientCreditsError,
    DatabarRateLimitError,
    DatabarTaskFailedError,
    DatabarTimeoutError,
    DatabarGoneError,
    DatabarError,
)

try:
    data = client.run_enrichment_sync(1591, {"linkedin_url": url})
except DatabarAuthError:
    raise SystemExit("Check DATABAR_API_KEY")
except DatabarValidationError as e:
    print("Bad input:", e, e.response_body)
except DatabarInsufficientCreditsError:
    print("Out of credits, or the plan does not allow this enrichment")
except DatabarRateLimitError as e:
    print("Rate limited:", e.response_body)   # the client already retried with backoff
except DatabarTaskFailedError as e:
    print("The provider run failed:", e, "task", e.task_id)
except DatabarTimeoutError:
    print("Still running after the poll budget. Keep the task_id and poll later")
except DatabarGoneError:
    print("Results expired (24h). Re-run the enrichment")
except DatabarError as e:
    print("Other API error:", e.status_code, e)
from databar import (
    DatabarAuthError,
    DatabarValidationError,
    DatabarInsufficientCreditsError,
    DatabarRateLimitError,
    DatabarTaskFailedError,
    DatabarTimeoutError,
    DatabarGoneError,
    DatabarError,
)

try:
    data = client.run_enrichment_sync(1591, {"linkedin_url": url})
except DatabarAuthError:
    raise SystemExit("Check DATABAR_API_KEY")
except DatabarValidationError as e:
    print("Bad input:", e, e.response_body)
except DatabarInsufficientCreditsError:
    print("Out of credits, or the plan does not allow this enrichment")
except DatabarRateLimitError as e:
    print("Rate limited:", e.response_body)   # the client already retried with backoff
except DatabarTaskFailedError as e:
    print("The provider run failed:", e, "task", e.task_id)
except DatabarTimeoutError:
    print("Still running after the poll budget. Keep the task_id and poll later")
except DatabarGoneError:
    print("Results expired (24h). Re-run the enrichment")
except DatabarError as e:
    print("Other API error:", e.status_code, e)

Retries are built in. Transport errors, 429s and 5xx responses are retried with exponential backoff; 4xx errors other than 429 raise immediately, and non-idempotent calls are not retried on a 5xx so a flow never runs twice. You do not need your own retry wrapper around the client.

Patterns worth copying

CSV in, enriched CSV out

import csv, os
from databar import DatabarClient, DatabarError

client = DatabarClient(api_key=os.environ["DATABAR_API_KEY"])

with open("leads.csv") as f:
    leads = list(csv.DictReader(f))

inputs = [{"first_name": l["first_name"], "last_name": l["last_name"],
           "domain": l["domain"]} for l in leads]

try:
    results = client.run_waterfall_bulk_sync("email_getter", inputs)
except DatabarError as e:
    raise SystemExit(f"Enrichment failed: {e}")

with open("leads_enriched.csv", "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["first_name", "last_name", "domain", "result"])
    writer.writeheader()
    for lead, result in zip(leads, results):
        writer.writerow({**{k: lead[k] for k in ("first_name", "last_name", "domain")},
                         "result": result})
import csv, os
from databar import DatabarClient, DatabarError

client = DatabarClient(api_key=os.environ["DATABAR_API_KEY"])

with open("leads.csv") as f:
    leads = list(csv.DictReader(f))

inputs = [{"first_name": l["first_name"], "last_name": l["last_name"],
           "domain": l["domain"]} for l in leads]

try:
    results = client.run_waterfall_bulk_sync("email_getter", inputs)
except DatabarError as e:
    raise SystemExit(f"Enrichment failed: {e}")

with open("leads_enriched.csv", "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["first_name", "last_name", "domain", "result"])
    writer.writeheader()
    for lead, result in zip(leads, results):
        writer.writerow({**{k: lead[k] for k in ("first_name", "last_name", "domain")},
                         "result": result})
import csv, os
from databar import DatabarClient, DatabarError

client = DatabarClient(api_key=os.environ["DATABAR_API_KEY"])

with open("leads.csv") as f:
    leads = list(csv.DictReader(f))

inputs = [{"first_name": l["first_name"], "last_name": l["last_name"],
           "domain": l["domain"]} for l in leads]

try:
    results = client.run_waterfall_bulk_sync("email_getter", inputs)
except DatabarError as e:
    raise SystemExit(f"Enrichment failed: {e}")

with open("leads_enriched.csv", "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["first_name", "last_name", "domain", "result"])
    writer.writeheader()
    for lead, result in zip(leads, results):
        writer.writerow({**{k: lead[k] for k in ("first_name", "last_name", "domain")},
                         "result": result})
import csv, os
from databar import DatabarClient, DatabarError

client = DatabarClient(api_key=os.environ["DATABAR_API_KEY"])

with open("leads.csv") as f:
    leads = list(csv.DictReader(f))

inputs = [{"first_name": l["first_name"], "last_name": l["last_name"],
           "domain": l["domain"]} for l in leads]

try:
    results = client.run_waterfall_bulk_sync("email_getter", inputs)
except DatabarError as e:
    raise SystemExit(f"Enrichment failed: {e}")

with open("leads_enriched.csv", "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["first_name", "last_name", "domain", "result"])
    writer.writeheader()
    for lead, result in zip(leads, results):
        writer.writerow({**{k: lead[k] for k in ("first_name", "last_name", "domain")},
                         "result": result})

Real-time form enrichment

Give the request a short poll budget and fall back rather than blocking a signup form:

from flask import Flask, request, jsonify
from databar import DatabarClient, DatabarError

app = Flask(__name__)
client = DatabarClient(timeout=5.0, max_poll_attempts=5, poll_interval_s=1.0)

@app.route("/api/enrich-lead", methods=["POST"])
def enrich_lead():
    email = request.json["email"]
    try:
        data = client.run_enrichment_sync(1305, {"email": email})
        return jsonify({"email": email, "enriched": True, "data": data})
    except DatabarError:
        # Never block the form on an enrichment: fall back and enrich later
        return jsonify({"email": email, "enriched": False})
from flask import Flask, request, jsonify
from databar import DatabarClient, DatabarError

app = Flask(__name__)
client = DatabarClient(timeout=5.0, max_poll_attempts=5, poll_interval_s=1.0)

@app.route("/api/enrich-lead", methods=["POST"])
def enrich_lead():
    email = request.json["email"]
    try:
        data = client.run_enrichment_sync(1305, {"email": email})
        return jsonify({"email": email, "enriched": True, "data": data})
    except DatabarError:
        # Never block the form on an enrichment: fall back and enrich later
        return jsonify({"email": email, "enriched": False})
from flask import Flask, request, jsonify
from databar import DatabarClient, DatabarError

app = Flask(__name__)
client = DatabarClient(timeout=5.0, max_poll_attempts=5, poll_interval_s=1.0)

@app.route("/api/enrich-lead", methods=["POST"])
def enrich_lead():
    email = request.json["email"]
    try:
        data = client.run_enrichment_sync(1305, {"email": email})
        return jsonify({"email": email, "enriched": True, "data": data})
    except DatabarError:
        # Never block the form on an enrichment: fall back and enrich later
        return jsonify({"email": email, "enriched": False})
from flask import Flask, request, jsonify
from databar import DatabarClient, DatabarError

app = Flask(__name__)
client = DatabarClient(timeout=5.0, max_poll_attempts=5, poll_interval_s=1.0)

@app.route("/api/enrich-lead", methods=["POST"])
def enrich_lead():
    email = request.json["email"]
    try:
        data = client.run_enrichment_sync(1305, {"email": email})
        return jsonify({"email": email, "enriched": True, "data": data})
    except DatabarError:
        # Never block the form on an enrichment: fall back and enrich later
        return jsonify({"email": email, "enriched": False})

Watch the credit balance

user = client.get_user()
if user.balance < 500:
    notify_ops(f"Databar balance low: {user.balance} credits")
user = client.get_user()
if user.balance < 500:
    notify_ops(f"Databar balance low: {user.balance} credits")
user = client.get_user()
if user.balance < 500:
    notify_ops(f"Databar balance low: {user.balance} credits")
user = client.get_user()
if user.balance < 500:
    notify_ops(f"Databar balance low: {user.balance} credits")
Build enrichment workflows in minutes, not days. Explore the Databar SDK

SDK, REST or MCP

The SDK is one of three ways into the same API. Use the SDK for Python services and pipelines. Use REST directly (POST /v1/enrichments/{id}/run, then GET /v1/tasks/{task_id}) from other languages, with the same x-apikey header. Use the MCP server when an agent should pick the enrichment itself. The trade-offs are in MCP vs SDK vs API.

What to build next

  • A nightly re-enrichment job over stale CRM records, using run_strategy="run_empty" so it only pays for gaps. Background in CRM enrichment.

  • An inbound form handler that enriches on submit and falls back quietly, as above.

  • A waterfall with verification for outbound lists, ordered by cost per hit rather than by brand.

The data providers available through Databar

FAQ

Which Python versions are supported?

Python 3.9 and newer. The SDK depends only on httpx.

Do I have to poll tasks myself?

No. The _sync helpers poll for you and return the data. Use the async pair when you want the task id, for example in a worker queue.

How long do results stay available?

24 hours. After that the task status becomes gone and the run has to be repeated.

What happens if a row returns nothing?

In a bulk run that element is None, aligned to your input order. Providers that return nothing do not cost credits.

Does the SDK handle rate limits?

Yes. 429s and 5xx responses are retried with exponential backoff, and 4xx errors raise straight away.

Build your dream workflow today

Start for free today · no credit card required

Build your dream workflow today

Start for free today · no credit card required

Build your dream workflow today

Start for free today · no credit card required