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

Uploaded Python 3

File details

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

File metadata

File hashes

Hashes for onefirstflock_donations_embed-1.2.6.tar.gz
Algorithm Hash digest
SHA256 c3f63a32b4a800807ca9c5c6c5fb29ec84ecad12a39718137691dd3c15cecc16
MD5 8398f6f6ac2f15cc39ea887e6516cf2c
BLAKE2b-256 b6c2c07c8fb4413e644fb6541720951b4f11b71639379d3069f2291e7dd1ff26

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for onefirstflock_donations_embed-1.2.6-py3-none-any.whl
Algorithm Hash digest
SHA256 b734bfaa54d999df492663c013fe26e8949ad306ac2b71c7edcb75c9361fb462
MD5 5833709d913e95f86e470b0d0795ad71
BLAKE2b-256 31ef182ff081d488eabd552c3d68409fb328b5b2c6c19f4615863f300234da24

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