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.
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.0wheel on PyPI omits one production module (donations_embed/models/webhook_test.py) and raisesModuleNotFoundError: 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_webhookto 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file onefirstflock_donations_embed-1.2.7.tar.gz.
File metadata
- Download URL: onefirstflock_donations_embed-1.2.7.tar.gz
- Upload date:
- Size: 58.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
819140ec16e4ae7b2f6e6f9c462ae3a0570bb10c172026999c2e3cadfc4d3d04
|
|
| MD5 |
54abcc1956c65f9a4888a94d6bb68b1e
|
|
| BLAKE2b-256 |
48f6ad9a0a4a60c93b9221a963787a50f641f3229568e2e27679fe19859136fe
|
File details
Details for the file onefirstflock_donations_embed-1.2.7-py3-none-any.whl.
File metadata
- Download URL: onefirstflock_donations_embed-1.2.7-py3-none-any.whl
- Upload date:
- Size: 29.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c67506606b42eff4b184414960801abe8c93bc38b5652c0f88ce934cdec75c26
|
|
| MD5 |
cbe94b2a1b32153df23a34853f742150
|
|
| BLAKE2b-256 |
84239bce3c01df3908db1326ef3832242201d05976e826124caf843c844aa110
|