NimbusNexus

Webhooks SDKs

Status: published. Python, TypeScript, and Go are on their public registries at 0.5.2. Java and PHP are written and tested but not yet published — see where it stands.

Official clients for NimbusNexus Webhooks. Each one covers the same four jobs, so a team using two languages gets the same behaviour in both.

Looking for the cloud-platform SDKs? Those are separate clients for VMs, storage and networking — Python, TypeScript, Go. In Go the two are easy to confuse: both expose a Verify, with different signatures and different signing headers, so the samples here alias the import nnwh to keep the provenance visible.

The highest-value piece is verify. Signature checking is security-critical, easy to get subtly wrong, and wrong in a way that passes every functional test — the SDKs implement the full contract including the constant-time comparison and the replay window.

Install

pip install nn-webhooks-sdk                        # Python
npm install @nimbusnexus/webhooks-sdk              # TypeScript
go get github.com/NimbusNexus/webhooks-go          # Go

Verify an incoming webhook

For anyone receiving webhooks. Pass the raw body bytes — never a re-serialized object.

from nn_webhooks import verify

ok = verify(
    secret=ENDPOINT_SIGNING_SECRET,
    raw_body=request.body,
    signature=request.headers["X-Webhook-Signature"],
    timestamp=request.headers["X-Webhook-Timestamp"],
)
if not ok:
    return Response(status_code=400)   # forged, tampered, or outside the 300s window
import { verify } from "@nimbusnexus/webhooks-sdk";

const ok = verify(secret, rawBody, req.headers["x-webhook-signature"], {
  timestamp: req.headers["x-webhook-timestamp"],
});
if (!ok) return res.status(400).end();
import nnwh "github.com/NimbusNexus/webhooks-go"

ts, _ := strconv.ParseInt(r.Header.Get("X-Webhook-Timestamp"), 10, 64)
ok := nnwh.Verify(secret, body, r.Header.Get("X-Webhook-Signature"),
    &nnwh.VerifyOptions{Timestamp: &ts})

Always pass the timestamp. Omitting it computes the signature over the body alone, which never matches what we send — so verification fails for everything, not just replays. If verify returns false for deliveries you believe are genuine, a missing timestamp is the first thing to check.

Publish an event

For producers. Transient failures — connection errors, 429, 5xx — are retried with backoff, and a 429 honours Retry-After. Other 4xx raise.

from nn_webhooks import Client, WebhooksAPIError

with Client("https://webhooks.nimbusnexus.net", api_key="whsk_…") as wh:
    try:
        event = wh.publish(
            "order.created",
            {"order_id": "ord_123", "total": 4200},
            idempotency_key="order-123",     # makes the publish safe to retry
        )
        print(event.event_uid, event.deliveries_created)
    except WebhooksAPIError as e:
        print(e.status_code, e.code, e.message)
client := nnwh.New("https://webhooks.nimbusnexus.net", "whsk_…")
event, err := client.Publish(ctx, "order.created",
    map[string]any{"order_id": "ord_123", "total": 4200},
    &nnwh.PublishOptions{IdempotencyKey: "order-123"})

Durable buffering

publish() is synchronous — if the API is unreachable it raises, and the event is gone. The write-first outbox decouples the two: enqueue() persists to a store and returns immediately with no network call, and drain() ships the backlog later.

from nn_webhooks import Client, SQLiteStore

store = SQLiteStore("outbox.db")

# Construct for the process lifetime — NOT in a `with` block. Closing the client stops the
# drainer, so a `with` that exits immediately would shut down the thread you just started.
wh = Client("https://webhooks.nimbusnexus.net", api_key="whsk_…", store=store)
wh.start_drainer(interval_seconds=5)          # ships in the background

wh.enqueue("order.created", {"order_id": "ord_123"})   # returns at once, no network
...
wh.close()                                     # at shutdown; also stops the drainer

Every send carries Idempotency-Key = record.id, so re-draining after a crash never double-publishes. A record that keeps failing is retried with capped exponential backoff up to max_attempts (default 10), then flagged dead and handed to an optional on_dead callback.

StoreDurableExtra dependency
MemoryStoreNo — in-processnone
FileStore(dir)Yesnone
SQLiteStore(path)Yes, transactionalnone
RedisStore(url)Yespip install 'nn-webhooks-sdk[redis]'
PostgresStore(dsn)Yespip install 'nn-webhooks-sdk[postgres]'

No store needs a dependency beyond the SDK itself — Python's only runtime dependency is httpx — and the Redis and Postgres stores import their driver lazily, only when constructed.

Manage endpoints and keys

The same client wraps the control plane, with an admin-scoped key:

ep = wh.create_endpoint(
    "https://your-app.example/webhooks",
    subscriptions=[{"match_kind": "prefix", "pattern": "order."}],
)
endpoint_id, signing_secret = ep["id"], ep["secret"]   # secret returned once

wh.rotate_endpoint_secret(endpoint_id)
wh.enable_endpoint(endpoint_id)                        # recover an auto-disabled endpoint

for d in wh.list_deliveries(status="dead")["items"]:   # drain the dead-letter queue
    wh.redeliver(d["id"])

List methods return {"items": [...], "next_offset": int | None}; deletes and revokes return nothing on a 204.

Targeting a project

A project is addressed by id (prj_3f9a…) — there is no slug or short name. Leave project_id unset to target the workspace's default project, which is what a single-project workspace always wants:

wh.publish("order.created", {...})                          # default project
wh.publish("order.created", {...}, project_id="prj_3f9a…")  # a specific one

Only the server can resolve "the default project", so omitting the field is the way to ask for it. Passing an invented string — "default", or a project's display name — is a 404. The real id is on any response, or from GET /v1/projects.

Keeping the SDKs honest

All clients implement one signing contract: HMAC-SHA256(secret, "<timestamp>." + raw_body), sent as X-Webhook-Signature: sha256=<hex> with a 300-second window. They are pinned to it by a shared vector fixture generated by the server's own signer, so no client can drift from the server or from each other — including in CI, where the server package isn't installed.

Where it stands today

LanguagePackageStatus
Pythonnn-webhooks-sdkPublished — PyPI, 0.5.2
TypeScript@nimbusnexus/webhooks-sdkPublished — npm, 0.5.2
Gogithub.com/NimbusNexus/webhooks-goPublished — GitHub, v0.5.2
Javanet.nimbusnexus:webhooks-sdkWritten and tested; not yet on Maven Central
PHPnimbusnexus/webhooks-sdkWritten and tested; not yet on Packagist

Until Java and PHP publish, call the REST API directly — the signature page has everything needed to verify deliveries in any language, and it is about twenty lines.

What's next