Skip to content
← BlogDeveloper

The norbelys Python SDK quickstart: pip install to your first call

Install the official Python SDK from PyPI, authenticate with an API key, and make your first typed call against the Norbelys API — a complete quickstart.

By Gabriel Lara, Developer Relations, Norbelys

Founder-reviewed ·How we research and correct articles

The official Python SDK for the Norbelys API is norbelys on PyPI. It’s generated from the same OpenAPI spec as the Node SDK, the CLI, and the MCP server, so if you’ve read about how those stay in sync, this is that contract showing up as a pip install. This quickstart takes you from a clean virtual environment to a working call.

Why a generated Python client beats raw requests

You could call the API with requests and a hand-rolled Authorization header, and that works fine for a one-off script. Where it stops working is maintenance: every field rename, every new required parameter, every endpoint you didn’t know existed has to be tracked by hand. A generated SDK means the types in your editor are the types the API actually accepts today — because they’re built from the same spec the API itself publishes at https://api.norbelys.com/openapi.json.

That matters more in Python than it sounds like it should, because requests.json() gives you a plain dict with zero guarantees. A typo in a key name — given_name instead of givenName, say — fails silently at runtime with a KeyError three functions away from where you made the mistake. A typed client turns that into an error your IDE or mypy catches before you run anything.

From a clean environment to a first call

  1. Create a virtual environment

    Standard practice for any Python project — isolates the SDK and its dependencies from your system interpreter and other projects.

  2. Install the package

    pip install norbelys pulls the client and its typed models. No separate stub package needed — types ship in the wheel.

  3. Set your API key as an environment variable

    NORBELYS_API_KEY, read by the client at construction time — never hard-code a key in a script you might commit or share.

  4. Construct the client and make a read call

    List a resource you already have data for, such as people or senders, to confirm auth and connectivity before you write anything.

  5. Move to a write call

    Create a sender or send a message once the read path works. Typed request models catch a missing or misspelled field before the request goes out.

Install and run

Terminal
$ python -m venv .venv && source .venv/bin/activate$ pip install norbelysSuccessfully installed norbelys-...$ export NORBELYS_API_KEY="ak_..."$ python main.pyPerson(id='con_...', email='jane@acme.com', given_name='Jane')

A minimal script constructs a client and calls a resource method. Python convention maps the API’s lowerCamelCase fields (givenName, familyName) to idiomatic snake_case attributes (given_name, family_name) on the returned model, the way most typed Python API clients do:

import os
from norbelys import Norbelys

client = Norbelys(api_key=os.environ["NORBELYS_API_KEY"])

response = client.people.list(limit=5)

for person in response.data:
    print(person.email, person.given_name)

A first write: creating a sender

sender = client.senders.create(
    email="outreach@yourdomain.com",
    name="Outreach",
)

print(sender.id)  # snd_...

Pass a malformed payload — a required field missing, a value of the wrong type — and a typed client raises a validation error locally, before the request is sent, rather than letting the API reject it and leaving you to parse the error response by hand.

Handling errors idiomatically

Every Norbelys API error uses one shape, { error: { code, message, field? } }, regardless of which resource or endpoint raised it. A Python SDK worth using turns that into a proper exception hierarchy you can catch with normal try/except, rather than making you inspect a response object after the fact:

from norbelys import APIError

try:
    client.senders.create(email="not-an-email")
except APIError as err:
    print(err.code, err.message, err.field)

That’s the pattern to reach for in any script that isn’t purely throwaway — catch the specific exception, branch on err.code for the error conditions you expect (a duplicate sender, a suppressed address), and let anything unexpected propagate.

Pagination, the Pythonic way

The API pages by cursor — never offset — because IDs are prefixed cuid2 and aren’t time-sortable, so ordering is always by createdAt. A well-built Python client turns that into something you can loop over naturally instead of manually tracking a cursor variable:

for person in client.people.list_all():
    process(person)

Whether your installed version calls it list_all, exposes an iterator on the paginated response object, or something else, the underlying mechanism is the same opaque (createdAt, id) cursor described in the OpenAPI spec walkthrough — the Python idiom just hides the bookkeeping.

Type checking with mypy

Because the client’s models are generated from the OpenAPI spec, running mypy over code that uses it catches the same class of mistakes a TypeScript compiler would on the Node side — a field name that doesn’t exist, a value passed as str where the model expects int, a required argument left off a create() call. This is easy to skip in a quick script, but worth turning on for anything that runs unattended (a scheduled job, a production pipeline), because a caught type error is a five-second fix in your editor versus a failed job at 3 a.m.:

pip install mypy
mypy main.py

Async support for concurrent calls

If you’re pulling a large number of records — verifying a batch of addresses, or fetching detail on thousands of people — a synchronous client making one request at a time will be your bottleneck, not the API. Expect an async client variant alongside the synchronous one, following the same pattern most modern Python HTTP clients use (httpx, for example) so you can fan requests out with asyncio.gather instead of a serial loop:

import asyncio
from norbelys import AsyncNorbelys

async def main():
    client = AsyncNorbelys(api_key=os.environ["NORBELYS_API_KEY"])
    results = await asyncio.gather(
        *(client.people.find(person_id) for person_id in ids)
    )
    return results

asyncio.run(main())

Reach for this once a script’s runtime is dominated by waiting on network calls rather than by CPU work — the usual signal that concurrency, not a faster machine, is the fix.

Configuring timeouts and retries

As with the Node SDK, expect constructor options beyond api_key for a request timeout and a max_retries count, so a script that runs unattended doesn’t hang indefinitely on a stalled connection or fail outright on a single transient network error:

client = Norbelys(
    api_key=os.environ["NORBELYS_API_KEY"],
    timeout=10.0,
    max_retries=2,
)

Pair retries with the Idempotency-Key header on any state-changing call — a senders.create() retried automatically after a network blip should never be able to create the sender twice.

Where Python fits

Python is the natural choice for data-science and ops scripts: a nightly job that pulls people or message events into a notebook, a one-off backfill, a Jupyter-driven exploration of campaign performance. It’s a less natural fit for a latency-sensitive edge Worker — if that’s your use case, the Node SDK or a direct fetch call is the better tool, and the SDK comparison guide lays out the trade-offs by use case in more depth.

Next steps