Skip to main content

custom-domain-sdk

Python client and origin helpers for the Custom Domain API. Pure Python 3.10+ with httpx; no dependency on the service itself.

pip install custom-domain-sdk        # from PyPI; from a checkout: pip install ./sdk

Quick start: register a workspace domain

from custom_domain import Client

client = Client("https://edge.example.net", credential="cd_...")  # the edge name and credential from the operator

domain = client.create_domain(
    "forms.customer.example",  # the customer's exact subdomain
    reference="ws_8f3a1c",  # your workspace id, returned to you on every request
    idempotency_key="signup-ws_8f3a1c",  # makes the call safe to retry
)
print(domain.status)  # pending_dns
print(domain.render_dns_instructions())

prints something like

Create these DNS records for forms.customer.example:

TXT   _custom-domain-challenge.forms.customer.example  ->  custom-domain-verify=...
      purpose: ownership. Create a TXT record with exactly this name and value. ...
CNAME forms.customer.example  ->  acme.edge.example.net
      purpose: routing. Point the hostname at the target with a CNAME record. ...

Show both records to the customer as returned; each carries help text for the usual DNS console mistakes. Nothing here requires knowing anything about the edge or Caddy.

Later:

domain = client.get_domain(domain.id)
for check in domain.checks:
    print(check.type, check.status, check.error_code)
if not domain.is_ready:
    client.request_recheck(domain.id)  # after the customer fixed DNS; rate limited
for d in client.iter_domains(reference="ws_8f3a1c"):
    ...
client.delete_domain(domain.id)

Errors

Every API error raises a subclass of custom_domain.ApiError with status, the stable code from the API, message and details: AuthenticationError (401), NotFoundError (404), ConflictError (409, for example hostname_already_claimed), ValidationError (422, for example apex_not_supported), RateLimitedError (429, with retry_after), ServerError (5xx). TransportError means no HTTP response was received.

Timeouts and retries

Each request has a 10 second timeout (Client(timeout=...)). Requests that are safe to repeat, that is GET, DELETE, and create_domain when an idempotency key is given, are retried up to max_retries (default 2) on connection errors, timeouts, 429 (waiting Retry-After) and 5xx, with exponential backoff. A create without an idempotency key and a recheck are never retried by the client: retrying a create could register twice and a recheck is rate limited, so the caller decides.

Verifying requests at your origin

The edge adds X-Custom-Domain-Assertion to every proxied request. Select the workspace from it and from nothing else.

ASGI (FastAPI, Starlette, and others):

from custom_domain import CustomDomainMiddleware

app.add_middleware(
    CustomDomainMiddleware,
    keys={
        "1": "<EDGE_ASSERTION_KEYS secret from the operator>"
    },  # current and previous key during rotation
    application_id="<your application id>",
    workspace_lookup=load_workspace,  # returns the workspace for a reference, or None
    on_missing="reject",  # "passthrough" if this app also serves its own domain
)


@app.get("/")
def home(request: Request):
    assertion = request.state.custom_domain  # None only with on_missing="passthrough"
    workspace = load_workspace(assertion.reference)

The middleware also answers GET /.well-known/custom-domain-workspace: it verifies the assertion, calls your workspace_lookup with the reference, and returns the reference only when the lookup confirms the workspace exists (404 otherwise). The service's lifecycle worker uses that answer to prove that routing and tenant selection really work before it marks a domain ready, so workspace_lookup must consult the same source your request handlers use.

Any framework:

from custom_domain import WorkspaceResolver, AssertionInvalid

resolver = WorkspaceResolver(keys={"1": "..."}, application_id="...")
try:
    assertion = resolver.resolve(request.headers, request.host)
except AssertionInvalid as exc:
    return forbidden(exc.code)  # missing, expired, bad_signature, wrong_application, ...

Origins should only be reachable from the edge (network rules), or treat any request without a valid assertion as a direct call and refuse it.

Webhooks

from custom_domain import verify_webhook, parse_event, SignatureInvalid

hook = client.create_webhook(
    "https://app.acme.example/hooks/custom-domain", ["domain.ready", "domain.deleted"]
)
store(hook.secret)  # shown once

# in your handler
try:
    verify_webhook(
        request.headers.get("X-Custom-Domain-Signature"), body, secrets=[current, previous]
    )
except SignatureInvalid:
    return 400
event = parse_event(body)  # event.id, event.type, event.created_at, event.domain

Deliveries are at least once and may arrive out of order: deduplicate by event.id and compare event.created_at before moving state. A complete consumer is in examples/webhook_consumer.py.

Local development versus production

Against the local Compose environment (DISABLE_HTTPS=true, ORIGIN_ALLOW_PRIVATE=true), the edge serves plain HTTP, the certificate check is marked not applicable, and origins on private addresses are accepted. In production the edge obtains real certificates on demand at the first handshake after DNS points to it, verifies origin TLS, refuses private origin and webhook addresses, and a domain becomes ready only after the HTTPS probe through the edge returns the right workspace. Customer DNS changes take up to the record TTL to be observed; the API's checks show the last observation and the next attempt.

Compatibility

The SDK follows the API's v1 contract. Minor SDK releases add fields and methods; a field the API stops sending is never removed within a major version. Unknown fields in API responses are ignored, so newer servers work with older SDKs. Python 3.10 and newer are supported.

Metadata

Release files for custom-domain-sdk 0.8.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for custom-domain-sdk 0.8.0
File Size Uploaded
custom_domain_sdk-0.8.0.tar.gz 15.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for custom-domain-sdk 0.8.0
File Interpreter ABI Platform
custom_domain_sdk-0.8.0-py3-none-any.whl Python 3 none any Details

Total release size: 34.7 kB

Release files / custom_domain_sdk-0.8.0.tar.gz

Download URL custom_domain_sdk-0.8.0.tar.gz
Size 15.6 kB
Tags Source
SHA-256 checksum
How to use checksums
1423ec365437c603f7af24298f15b15b1c89ea7f83db3cea84fe9cc2af0c625a
BLAKE2b-256 checksum
How to use checksums
3850c73e390db3c57a8b246007fba534702d3dede96d48214f1e14fa7509cf3c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.

Transparency log

Release files / custom_domain_sdk-0.8.0-py3-none-any.whl

Download URL custom_domain_sdk-0.8.0-py3-none-any.whl
Size 19.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bc34d2e8dc20dc246ff73d6de2bda20ee8ad03dd112ee4a9cf5711173811f925
BLAKE2b-256 checksum
How to use checksums
e2287e22b7da9fa9ceaf77520ae4ed6564ba1f484b6e0090e9038e3fc1cc5307
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.

Transparency log

Release history Release notifications | RSS feed

0.13.0

2 release files

0.12.0

2 release files

0.11.1

2 release files

0.11.0

2 release files

0.9.0

2 release files

This release

0.8.0 This release

2 release files

0.7.0

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page