Skip to main content

Server-side Python SDK for the 1st Flock Donations Embed API — typed client, webhook signature verification, and event models.

Project description

onefirstflock-donations-embed

Server-side Python SDK for the 1st Flock Donations Embed API. Async-first client built on httpx, typed Pydantic v2 models for every payload, and a constant-time HMAC-SHA256 webhook verifier. FastAPI / Starlette / Django friendly.

PyPI version Python versions License

Install

pip install onefirstflock-donations-embed
# or
uv add onefirstflock-donations-embed

The package name on PyPI is onefirstflock-donations-embed; the import name is donations_embed. Requires Python 3.10 or newer.

v1.0.0 install bug — install from source until v1.0.1 ships. The published onefirstflock-donations-embed==1.0.0 wheel on PyPI omits one production module (donations_embed/models/webhook_test.py) and raises ModuleNotFoundError: No module named 'donations_embed.models.webhook_test' at import time. Until v1.0.1 is published, install from the tagged source:

pip install "git+https://gitlab.com/1st-flock/donations@v1.0.0#subdirectory=python/donations-embed"
# or
uv add "git+https://gitlab.com/1st-flock/donations@v1.0.0#subdirectory=python/donations-embed"

The git-source install pulls every file under src/donations_embed/ and is byte-for-byte identical to what v1.0.1 will publish to PyPI.

Quickstart

import asyncio
import os
from donations_embed import Client


async def main() -> None:
    async with Client(api_key=os.environ["FLOCK_SECRET_KEY"]) as client:
        page = await client.list_donations(limit=25)
        for donation in page.donations:
            print(donation.id, donation.gross_amount_cents, donation.status)


asyncio.run(main())

Run this against a sandbox key (sk_test_…) and any donation made through the embed (e.g. with sandbox card 4111 1111 1111 1111, exp 10/29, CVV 123) shows up here within a few seconds.

Configuration

Generate keys in the 1st Flock account portal under Donations Setup → Embed Keys (full guide). The server SDK requires a secret key:

  • Secret keys (sk_live_…, sk_test_…) — server-only. Never ship a secret key to a browser. Store in environment variables, your secrets manager, or a settings library.
  • Webhook signing secret — issued separately when you create or rotate a webhook endpoint. Used by verify_webhook to authenticate inbound deliveries. Store alongside the secret key.

The constructor rejects publishable keys (pk_…) at construction time with a clear ValueError so misconfiguration surfaces synchronously rather than as a confusing 401.

from donations_embed import Client

client = Client(
    api_key=os.environ["FLOCK_SECRET_KEY"],
    # Optional overrides:
    base_url="https://api.sandbox.1stflock.com",  # also auto-resolved by sk_test_ prefix
    max_retries=3,    # retries on 5xx + transient network errors
    timeout=30.0,     # per-request timeout in seconds
)

For long-lived applications (FastAPI lifespan, Django async middleware), construct once at startup and call await client.aclose() on shutdown. For one-off scripts use the async-context-manager form (async with Client(...) as client:) so connection pools are cleaned up automatically.

Test mode

Pass a sandbox key (sk_test_…) — the SDK routes to the sandbox cluster automatically based on the _test_ prefix. No real money moves. Same code path, same wire format, same webhook events. Donations show up in the hub with a TEST badge and never appear in the production ledger.

Common tasks

List + paginate donations

async def stream_all_donations() -> None:
    async with Client(api_key=os.environ["FLOCK_SECRET_KEY"]) as client:
        cursor: str | None = None
        while True:
            page = await client.list_donations(cursor=cursor, limit=200)
            for donation in page.donations:
                await sync_to_ledger(donation)
            if page.next_cursor is None:
                break
            cursor = page.next_cursor

Filter by fund or donor email:

building_only = await client.list_donations(fund_id="general")
sams_history = await client.list_donations(donor_email="sam@example.com")

Fetch a single donation

donation = await client.get_donation("8a1b9c4d-1234-4321-9abc-def012345678")
print(donation.confirmation_id, donation.gross_amount_cents)

Cross-organization access deliberately collapses to 404 (NotFoundError) so you cannot use the API to confirm whether a UUID belongs to another tenant.

Refund a donation

from donations_embed import Client, idempotency_key

# Full refund:
await client.refund_donation(donation_id)

# Partial refund with an idempotency key (safe to retry on network blip):
refund = await client.refund_donation(
    donation_id,
    amount_cents=1000,
    reason="Donor adjustment",
    idempotency_key=idempotency_key("refund"),
)
print(refund.processor_refund_id, refund.refund_amount_cents)

idempotency_key("refund") returns a tagged UUID like "refund:1f2dab36-2a55-4f11-bdc8-2a2a4a95b001". Replays of the same key return the original response without re-contacting the gateway.

List + cancel recurring schedules

page = await client.list_recurring(status="active", limit=100)
for recurring in page.recurring:
    if donor_opted_out(recurring.donor_email):
        await client.cancel_recurring(
            recurring.id,
            idempotency_key=idempotency_key("cancel-recurring"),
        )

Fire a synthetic webhook (integration test)

result = await client.test_webhook("donation.completed")
print("test event id:", result.event_id)

The synthetic event's data.object.synthetic flag is set so downstream code can branch on it.

Webhook verification

Verify every inbound delivery with verify_webhook — constant-time HMAC-SHA256 comparison plus a 5-minute timestamp tolerance to defeat replays.

import os
from fastapi import FastAPI, Header, HTTPException, Request
from donations_embed import (
    DonationCompletedEvent,
    DonationRefundedEvent,
    KeyFrozenEvent,
    WebhookError,
    verify_webhook,
)

app = FastAPI()
WEBHOOK_SECRET = os.environ["FLOCK_WEBHOOK_SECRET"]


@app.post("/webhooks/donations")
async def receive_webhook(
    request: Request,
    flock_signature: str = Header(..., alias="Flock-Signature"),
) -> dict[str, str]:
    body = await request.body()
    try:
        event = verify_webhook(body, flock_signature, WEBHOOK_SECRET)
    except WebhookError:
        # Always 401 — never confirm which check failed.
        raise HTTPException(status_code=401, detail="invalid signature") from None

    match event:
        case DonationCompletedEvent():
            await enqueue_receipt(event.data.object)
        case DonationRefundedEvent():
            await reverse_receipt(event.data.object)
        case KeyFrozenEvent():
            await alert_on_call(event.data.object)
        case _:
            pass  # Ignore unknown event types.

    return {"status": "ok"}

Pass the exact body bytes the platform delivered. Re-serializing the JSON before verifying will invalidate the signature.

verify_webhook raises typed exceptions on failure (WebhookFormatError, WebhookTimestampError, WebhookSignatureError — all inherit from WebhookError) so you can log distinct failure modes even though the HTTP response is the same 401 in every case. An async wrapper, verify_webhook_async, is also provided for await-based middleware.

Dedup by Flock-Event-Id

Webhook delivery is at-least-once. Persist event.id (also exposed as the Flock-Event-Id HTTP header) and short-circuit re-deliveries.

Errors

Every API call raises a typed exception on a non-2xx response.

HTTP status Exception
400 / 422 ValidationError
401 AuthError
403 ForbiddenError
404 NotFoundError
409 ConflictError
410 GoneError
429 RateLimitError (carries retry_after)
502 BadGatewayError
503 ServiceUnavailableError
Other ApiError

All inherit from ApiError and expose status_code, code, message, details, and request_id.

from donations_embed import ApiError, ConflictError, NotFoundError, RateLimitError

try:
    refund = await client.refund_donation(donation_id, amount_cents=1000)
except RateLimitError as exc:
    await asyncio.sleep(exc.retry_after or 1)
    return await retry()
except ConflictError:
    log.warning("Donation %s already refunded", donation_id)
except NotFoundError:
    log.warning("Donation %s not found", donation_id)
except ApiError as exc:
    log.error("API error %s %s: %s", exc.status_code, exc.code, exc.message)
    raise

Full documentation

The full reference — every endpoint, every error code, the webhook event catalogue with payload schemas, and operations guidance — lives at 1stflock.com/developers/donations-embed/.

Vendor neutrality

This SDK never names the underlying payment processor, bank-link service, or any other third-party vendor in customer-facing strings. Donations carry vendor-neutral identifiers (processor_transaction_id, processor_refund_id, processor_subscription_id); errors route through 1st Flock's own taxonomy. The underlying integrations are an implementation detail and may change without notice — your code never has to.

License

MIT — see LICENSE.

Reporting issues

File issues at gitlab.com/1st-flock/donations/-/issues with the label donations-embed-python.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

onefirstflock_donations_embed-1.1.0.tar.gz (28.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

onefirstflock_donations_embed-1.1.0-py3-none-any.whl (29.8 kB view details)

Uploaded Python 3

File details

Details for the file onefirstflock_donations_embed-1.1.0.tar.gz.

File metadata

File hashes

Hashes for onefirstflock_donations_embed-1.1.0.tar.gz
Algorithm Hash digest
SHA256 87a9471d45ce7dfaf85881806543bf8aecd5a4fc28b000eb20afff30a7a12a9e
MD5 56eca77bbd0079189e356fff520ad33d
BLAKE2b-256 70fd86f064a17d092f5e724bbed033fd91df6d69e00afedfa8520993fbf80f80

See more details on using hashes here.

File details

Details for the file onefirstflock_donations_embed-1.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for onefirstflock_donations_embed-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 dc9f93be569d8fab9c43c77b12969acbf3707d6861b911014725d6f4d882304d
MD5 ebc9021061f95807e61ffd37830083c1
BLAKE2b-256 31ac49530641f00c7de980c9cddc31c46c317bfb9072bf780d6df06fc7f5eeeb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page