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.2.2.tar.gz (58.6 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.2.2-py3-none-any.whl (29.8 kB view details)

Uploaded Python 3

File details

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

File metadata

File hashes

Hashes for onefirstflock_donations_embed-1.2.2.tar.gz
Algorithm Hash digest
SHA256 0593a470eaa98fb971e9ea9305ad6822c24cb54eb7da4f42472feb27cfe37061
MD5 07af3f2205c58760b1a0e4046730bebf
BLAKE2b-256 eef7a067506c72bbf425f5d3037f2ef86c2babfdf8ebb3f8e0e61e051dacdff8

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for onefirstflock_donations_embed-1.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 f01a7327c838a1c1e149bea362652f0d8846cde889db0803a81a66ef9923cc63
MD5 9f7704ba4f3ef77167bf70a13e348137
BLAKE2b-256 b1eb6ffad83f84554de74d1b67396d864a85e9194f474df6d80e2f01cf779c07

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