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

# Python SDK: Orbit quickstart for Python

> Official Orbit Python SDK quickstart — messaging, voice, contacts, campaigns, and OTP verify with copy-pasteable examples.

# Python SDK

The Orbit Python SDK wraps the platform's core API resources — messaging (SMS, WhatsApp, email), voice, contacts, campaigns, verify (OTP), numbers, and HLR lookup — with typed methods. It targets Python 3.9+ and is fully hand-written on the standard library, so it has zero runtime dependencies.

<Note>
  **Pre-publish — source-only.** This SDK is not yet on PyPI. The install
  commands below describe the future registry shape; until first publish,
  vendor the source from the monorepo (`packages/sdk-python/`) or call the
  [REST API](/api-reference) directly from your client. See the
  [Python: core-scope, not full parity](/sdks#python-core-scope-not-full-parity)
  section on the SDK index for exactly what is and isn't wrapped, and the
  low-level `client.request(method, path, ...)` escape hatch for uncovered routes
  (worked example [below](#covered-route-missing-use-the-escape-hatch)).
</Note>

## Installation

Not installable from PyPI yet. Vendor the SDK source from the monorepo
(`packages/sdk-python/` — a pure-standard-library package you can drop onto
your path), or call the REST API directly with any HTTP client until the
first release ships (install coordinates change).

Requires Python 3.9+.

## Client initialization

```python theme={null}
from orbit_sdk import OrbitClient

client = OrbitClient.from_api_key("dv_live_sk_...")
```

Or read the key from an environment variable (`ORBIT_API_KEY`):

```python theme={null}
client = OrbitClient.from_env()
```

Tune timeouts and retries with the direct constructor:

```python theme={null}
client = OrbitClient(
    api_key="dv_live_sk_...",
    base_url="https://api.orbit.devotel.io/api/v1",  # defaults to prod
    timeout_s=30.0,      # per-request timeout
    max_retries=3,       # 429/5xx retry budget
    initial_backoff_s=1.0,
)
```

## Quickstart: send your first SMS

A runnable end-to-end — the key comes from `ORBIT_API_KEY`, never from
source. Copy it into `first_send.py` and run it:

```python theme={null}
from orbit_sdk import OrbitClient

client = OrbitClient.from_env()  # reads ORBIT_API_KEY

msg = client.messages.send_sms(to="+14155552671", body="Hello from Orbit!")

print("message id:", msg["data"]["id"])          # msg_abc123
print("status:", msg["data"]["status"])          # 'queued' — watch it reach 'delivered'
print("status URL:", f"/api/v1/messages/{msg['data']['id']}")
```

Point `ORBIT_API_KEY` at a sandbox key prefixed `dv_test_sk_` first —
sandbox sends are simulated, free, and never reach a carrier. Swap in your
live key (`dv_live_sk_...`) when you're ready to send for real; the code
does not change.

The WhatsApp and email helpers below follow the same shape, and the full
request/response contract with per-language examples lives on the
[Messages API reference](/api-reference/endpoints/messaging).

## Messaging

```python theme={null}
# Send SMS
msg = client.messages.send_sms(to="+14155552671", body="Hello from Orbit!")
print(msg["data"]["id"])

# Send WhatsApp
client.messages.send_whatsapp(to="+14155552671", body="Hello on WhatsApp!")

# Send email
client.messages.send_email(
    to="user@example.com",
    subject="Welcome",
    body="Thanks for signing up.",
)
```

## Voice

```python theme={null}
# Initiate an outbound call, then hang up. Termination is handled
# server-side by the Devotel softswitch — the SDK never selects a carrier.
call = client.voice.create(to="+14155552671", from_="+14155550000", record=True)
client.voice.get(call["data"]["id"])
client.voice.hangup(call["data"]["id"])
```

## Second example: verify OTP

The send-and-check round trip is the most common first integration on the
platform. Three lines — the key is still just `ORBIT_API_KEY` read by
`OrbitClient.from_env()`:

```python theme={null}
# 1. Send an OTP (channel: sms | whatsapp | email).
sent = client.verify.send(to="+14155552671", channel="sms")

# 2. Check the code the user typed in, against the verification id send() returned.
result = client.verify.check(verification_id=sent["data"]["id"], code="482901")

print("valid:", result["data"]["valid"])     # True
print("status:", result["data"]["status"])   # 'approved'
```

A wrongly-typed or expired code reports `valid: False` — it never raises
for a bad guess (only for transport/auth failures), so check the flag.
The [Verify API reference](/api-reference/endpoints/verify) covers the full
contract, and [Starter examples](/guides/starter-examples) includes a
complete OTP sign-in starter repo (`orbit-otp-nextjs`).

## Verify (OTP)

```python theme={null}
sent = client.verify.send(to="+14155552671", channel="sms")  # sms | whatsapp | email
client.verify.check(verification_id=sent["data"]["id"], code="123456")
client.verify.get_detail(sent["data"]["id"])
```

## Contacts

```python theme={null}
contact = client.contacts.create(phone="+14155552671", first_name="Ada", tags=["vip"])
client.contacts.list(search="ada", limit=20)
client.contacts.get(contact["data"]["id"])
client.contacts.update(contact["data"]["id"], company="Acme Inc.")
client.contacts.add_tags(contact["data"]["id"], ["beta-tester"])
client.contacts.delete(contact["data"]["id"])
```

## Campaigns

```python theme={null}
campaign = client.campaigns.create(
    name="Fall promo",
    channel="sms",
    message_template="Hi {{first_name}}!",
)
client.campaigns.list()
client.campaigns.send(campaign["data"]["id"])
client.campaigns.update(campaign["data"]["id"], name="Fall promo v2")
client.campaigns.delete(campaign["data"]["id"])
```

## Numbers and HLR lookup

```python theme={null}
available = client.numbers.search(country="US", type="local", capabilities=["sms", "voice"])
client.numbers.purchase(number="+14155552671")

client.lookup.number("+14155552671")                              # validity, carrier, line type
client.lookup.bulk(["+14155552671", "+442071838750"])             # up to 100 per request
```

## Paginate a list

List endpoints are cursor-paginated — read `meta.pagination.cursor` and
`meta.pagination.has_more` off each response and pass the cursor back until
`has_more` is `False`. Page through SMS messages:

```python theme={null}
params = {"channel": "sms", "limit": 100}
total = 0

while True:
    page = client.request("GET", "/messages", params=params)

    total += len(page["data"])
    pagination = page["meta"]["pagination"]
    if not pagination["has_more"]:
        break
    params["cursor"] = pagination["cursor"]

print(f"Fetched {total} messages")
```

The full pagination model (cursor vs. offset endpoints, page-size caps, and
why cursors are not bookmarkable) is in the
[Pagination guide](/guides/pagination).

## Error handling

All Orbit-originated errors inherit from `OrbitError`:

| Exception                  | Raised when                                                |
| -------------------------- | ---------------------------------------------------------- |
| `OrbitAuthenticationError` | 401/403 — bad or missing-scope API key                     |
| `OrbitClientError`         | other 4xx (invalid request)                                |
| `OrbitRateLimitError`      | 429 after retries exhausted; check `exc.retry_after`       |
| `OrbitServerError`         | 5xx after retries exhausted, or persistent network failure |
| `OrbitError`               | base class — catch to handle any Orbit-originated error    |

```python theme={null}
from orbit_sdk import (
    OrbitError,
    OrbitAuthenticationError,
    OrbitRateLimitError,
)

try:
    client.messages.send_sms(to="+14155552671", body="...")
except OrbitRateLimitError as exc:
    print(f"throttled — retry in {exc.retry_after}s")
except OrbitAuthenticationError:
    print("bad API key — rotate it")
except OrbitError as exc:
    print(f"orbit error: {exc.code} ({exc.status}) — {exc.message}")
```

Every non-GET request automatically carries an `Idempotency-Key` header, so a retry never produces a duplicate charge on billable paths. Override per call with your own stable key (`idempotency_key="job-7a3b9d-attempt-1"`).

## Covered route missing? Use the escape hatch

The typed clients wrap 8 core resources; the rest of the API — contact segments, event sinks, frequency caps, and everything else listed as out of scope on the [SDK index](/sdks#python-core-scope-not-full-parity) — is reachable through `client.request(method, path, ...)`. It returns the raw JSON body as a `dict`. Fetch a segment by id:

```python theme={null}
segment = client.request("GET", "/contacts/segments/seg_01hxyz")
print(segment["data"]["name"])
```

The escape hatch carries the same auth, retry, and error model as the typed clients — treat it as a first-class client, not a fallback `urllib` call.

## Webhook signature verification

```python theme={null}
from orbit_sdk import verify_webhook, OrbitWebhookSignatureError

@app.post("/webhooks/orbit")
def handler(request):
    try:
        event = verify_webhook(
            payload=request.body,                            # bytes
            signature=request.headers["X-Devotel-Signature"],
            secret=os.environ["ORBIT_WEBHOOK_SECRET"],
        )
    except OrbitWebhookSignatureError:
        # Forgery attempt — drop, do not 200.
        return Response(status=400)

    # event is a dict — act on event["type"], event["data"], ...
    return Response(status=200)
```

Signatures use the `t=<unix_ts>,v1=<hex_hmac>` format (same as Stripe) with a 5-minute replay window enforced by default.
