Skip to main content
developers/donations-sdk/api-reference
Developer13 min read

API Reference

Complete reference for every Donations Embed Customer SDK API endpoint. Authentication, request and response shapes, error envelopes, idempotency, and rate limits.

API Reference

This is the complete reference for the Donations Embed Customer SDK API — the server-to-server endpoints you call with your secret key. The browser embed and the Hub portal call additional cohorts of endpoints, but customer SDK code never touches them; this page documents only what you can call from your server.

For a more conversational introduction, see Server SDKs. For raw OpenAPI, see the published spec at https://api.1stflock.com/openapi.yaml.

Base URLs

EnvironmentBase URL
Productionhttps://api.1stflock.com
Sandboxhttps://api.sandbox.1stflock.com

The SDK routes to the correct base URL automatically based on the secret key prefix (sk_live_ → production, sk_test_ → sandbox). If you are calling the API directly without the SDK, pick the base URL that matches your key prefix.

Authentication

All Customer SDK endpoints authenticate with your secret key over HTTP Authorization: Bearer:

Authorization: Bearer sk_test_REPLACE_ME

Mixing key prefixes (live secret against the sandbox URL, or vice versa) returns 401.

Idempotency

Mutating endpoints (POST /v1/embed/donations/{id}/refund, POST /v1/embed/recurring/{id}/cancel) accept an optional Idempotency-Key HTTP header. Replays return the original response without performing the side effect a second time:

Idempotency-Key: refund:1f2dab36-2a55-4f11-bdc8-2a2a4a95b001

Each SDK provides an idempotencyKey helper that generates a fresh tagged UUID; see your SDK's reference page for usage.

Pagination

List endpoints (GET /v1/embed/donations, GET /v1/embed/recurring) are cursor-paginated. Default page size is 50; the cap is 200.

Pass ?cursor=… (the previous response's next_cursor) and ?limit=… (1 to 200). Iterate until next_cursor is null.

Rate limits

CohortLimit
Customer SDK (your secret key)1000 requests / minute / (key, source IP)

When exceeded, the API returns 429 with a Retry-After header (seconds). Each SDK surfaces this as a RateLimitError with the value parsed into retryAfter / retry_after.

Error envelope

Every non-2xx response uses the same JSON envelope:

{
  "code": "validation_error",
  "message": "allocations must contain at least one item",
  "details": {
    "loc": ["body", "allocations"]
  }
}
FieldTypeDescription
codestringStable, machine-readable identifier (invalid_cursor, card_declined, validation_error, etc.). Safe to switch on.
messagestringHuman-readable description. Subject to wording changes.
detailsobject (optional)Error-specific extra data. Schema varies by code.

The HTTP x-request-id response header carries a server-side correlation ID; include it when contacting support so we can trace the request quickly.

HTTP statusMeaning
400Bad request — malformed query parameters or body.
401Missing or invalid credentials.
403Credentials valid but action not allowed (frozen key, missing RBAC scope, origin not allowlisted).
404Resource does not exist or is not visible to the caller.
409Request conflicts with current resource state (e.g. refund of a fully-refunded donation).
410Session expired or terminally failed; restart the flow.
422Body fails schema validation.
429Rate limit exceeded.
502Upstream payment gateway error.
503Required platform configuration missing or unavailable.

Endpoints

List donations

GET /v1/embed/donations

Returns donations for the authenticated organisation, ordered (created_at DESC, id DESC). Cursor-paginated; page size capped at 200.

Query parameters:

NameTypeDefaultNotes
cursorstring(none)Opaque cursor returned in a previous response's next_cursor.
limitinteger (1–200)50Page size.
fund_idUUID(none)Filter to donations with at least one allocation to this fund.
donor_emailstring(none)Filter to donations from a specific donor email.

Response (200):

{
  "donations": [
    {
      "id": "8a1b9c4d-1234-4321-9abc-def012345678",
      "confirmation_id": "don_8a1b9c4d",
      "donor_name": "Sam Donor",
      "donor_email": "sam@example.com",
      "gross_amount_cents": 5175,
      "fee_amount_cents": 175,
      "net_amount_cents": 5000,
      "payment_method_type": "card",
      "payment_method_last4": "4242",
      "payment_method_brand": "visa",
      "funds": [
        {
          "fund_id": "6f1b9b54-0f0b-4d1d-9c8a-9a7e5d4d0001",
          "fund_name": "General",
          "amount_cents": 5000
        }
      ],
      "recurring_id": null,
      "refunded_amount_cents": 0,
      "status": "completed",
      "created_at": "2026-04-25T18:30:25Z"
    }
  ],
  "next_cursor": null
}

Possible errors: 400, 401, 403, 429.

Get a single donation

GET /v1/embed/donations/{donation_id}

Returns the full donation record. Cross-organisation access collapses to 404 so the response cannot confirm whether a UUID belongs to another tenant.

Path parameters:

NameTypeNotes
donation_idUUIDThe donation's unique ID.

Response (200): Same Donation shape as above.

Possible errors: 401, 403, 404, 429.

Refund a donation

POST /v1/embed/donations/{donation_id}/refund

Issues a full or partial refund. Idempotent on Idempotency-Key: a second call against an already-refunded donation returns the existing refund record without contacting the gateway again.

Path parameters:

NameTypeNotes
donation_idUUIDThe donation's unique ID.

Headers:

NameRequiredNotes
Idempotency-KeyrecommendedCaller-supplied key. Replays return the original response.

Request body:

{
  "amount_cents": 1000,
  "reason": "Donor requested adjustment"
}
FieldTypeDefaultNotes
amount_centsinteger (≥ 1)(full refund)Refund amount in cents. Omit or set null for full.
reasonstring (≤ 500)(none)Free-text reason recorded in the audit log.

Response (200):

{
  "donation_id": "8a1b9c4d-1234-4321-9abc-def012345678",
  "refund_amount_cents": 1000,
  "refunded_at": "2026-04-25T18:35:00Z",
  "processor_refund_id": "ref_8a1b9c4d"
}

Possible errors: 400, 401, 403, 404, 409, 422, 429, 502.

List recurring schedules

GET /v1/embed/recurring

Returns recurring donation schedules for the authenticated organisation, ordered (created_at DESC, id DESC).

Query parameters:

NameTypeDefaultNotes
cursorstring(none)Opaque cursor from a previous response.
limitinteger (1–200)50Page size.
statusenum: active / paused / cancelled(none)Filter by status.

Response (200):

{
  "recurring": [
    {
      "id": "5b2c8e10-1ed0-4f0c-bb8a-1234567890ab",
      "donor_name": "Sam Donor",
      "donor_email": "sam@example.com",
      "amount_cents": 25000,
      "interval": "monthly",
      "interval_count": 1,
      "status": "active",
      "next_charge_date": "2026-05-25",
      "created_at": "2026-04-25T18:30:25Z"
    }
  ],
  "next_cursor": null
}

Possible errors: 400, 401, 403, 429.

Cancel a recurring schedule

POST /v1/embed/recurring/{recurring_id}/cancel

Cancels the schedule on the gateway and marks it cancelled. Idempotent: a second call returns already_cancelled: true without re-emitting a recurring.cancelled webhook.

Path parameters:

NameTypeNotes
recurring_idUUIDThe recurring schedule's unique ID.

Headers:

NameRequiredNotes
Idempotency-KeyrecommendedReplays return the original response.

Response (200):

{
  "id": "5b2c8e10-1ed0-4f0c-bb8a-1234567890ab",
  "status": "cancelled"
}

On a replay:

{
  "id": "5b2c8e10-1ed0-4f0c-bb8a-1234567890ab",
  "status": "cancelled",
  "already_cancelled": true
}

Possible errors: 401, 403, 404, 409, 429, 502.

Fire a synthetic webhook

POST /v1/embed/webhooks/test

Queues a synthetic event of the requested type for delivery to the organisation's enabled webhook endpoint. The payload's data.object.synthetic flag is true so customer code can branch on it and skip downstream processing during integration testing. Useful for end-to-end testing your webhook handler without making a real donation.

Request body:

{
  "event_type": "donation.completed"
}
FieldTypeNotes
event_typestringOne of: donation.created, donation.completed, donation.failed, donation.refunded, recurring.activated, recurring.payment_succeeded, recurring.payment_failed, recurring.cancelled.

The key.frozen event cannot be triggered via this endpoint — it is fired only by the automatic protection system in response to real declined-attempt bursts.

Response (200):

{
  "event_id": "evt_test_a1b2c3d4e5f6",
  "queued": true
}

Possible errors: 401, 403, 409 (no enabled webhook endpoint configured), 422, 429.

Webhook endpoint configuration

The Customer SDK API does not expose webhook endpoint configuration — that is configured via the Hub portal, or programmatically via Hub-cohort endpoints (PUT /v1/embed/webhooks). The Hub-cohort endpoints authenticate with a user session, not a secret key, so they are not callable from a server-to-server SDK context. If you need programmatic webhook configuration, see your account manager for the Hub API surface.

To inspect delivery history, the Hub portal's Donations Embed → Webhooks → Deliveries tab and the underlying GET /v1/embed/webhooks/deliveries endpoint return the same data.

Object reference

Donation

FieldTypeNotes
idUUIDThe donation's unique ID.
confirmation_idstringHuman-readable identifier (e.g. don_8a1b9c4d).
donor_namestring
donor_emailstring
gross_amount_centsintegerAmount actually charged including any fee coverage.
fee_amount_centsintegerProcessing fee, if covered by the donor; 0 otherwise.
net_amount_centsintegerAmount that ends up in your fund(s).
payment_method_typeenumcard or ach.
payment_method_last4stringLast four digits of the card or bank account.
payment_method_brandstring | nullCard brand (e.g. visa); null for ACH.
fundsarray of FundAllocationPer-fund split of net_amount_cents.
recurring_idUUID | nullLinked recurring schedule, if this donation is a recurring cycle.
refunded_amount_centsintegerTotal refunded, in cents.
statusenumpending, completed, failed, refunded, partial_refund.
created_atISO 8601 timestamp

FundAllocation

FieldTypeNotes
fund_idUUID
fund_namestring
amount_centsinteger

Recurring

FieldTypeNotes
idUUID
donor_namestring
donor_emailstring
amount_centsintegerAmount of each cycle, in cents.
intervalenumweekly, monthly, annual.
interval_countintegerNumber of intervals between cycles (1 = every interval; 2 = every other; etc.).
statusenumactive, paused, cancelled.
next_charge_dateISO 8601 date | null
created_atISO 8601 timestamp

Refund response

FieldTypeNotes
donation_idUUID
refund_amount_centsintegerAmount refunded in cents.
refunded_atISO 8601 timestamp
processor_refund_idstringVendor-neutral refund identifier.

Webhook test response

FieldTypeNotes
event_idstringThe synthetic event's ID. Matches the Flock-Event-Id header on the resulting delivery.
queuedbooleanAlways true on success.

Sample flows

Sync the last hour of donations every minute

A simple poll-based sync. Since donations are also delivered via webhook, prefer the webhook flow if you can — but a polling fallback is useful for systems that cannot accept inbound webhooks.

import { Client } from "@1st-flock/donations-embed-node";

const client = new Client({ apiKey: process.env.FLOCK_SECRET_KEY! });

async function syncRecentDonations(sinceIso: string): Promise<void> {
  let cursor: string | undefined;
  do {
    const page = await client.listDonations({ cursor, limit: 200 });
    for (const donation of page.donations) {
      if (donation.created_at < sinceIso) {
        return; // Older than the window — done.
      }
      await upsertDonation(donation);
    }
    cursor = page.next_cursor ?? undefined;
  } while (cursor);
}

setInterval(() => {
  const since = new Date(Date.now() - 60 * 60 * 1000).toISOString();
  syncRecentDonations(since);
}, 60_000);
import asyncio
from datetime import datetime, timedelta, timezone
from donations_embed import Client

async def sync_recent_donations(since: datetime) -> 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:
                if donation.created_at < since:
                    return  # Older than the window — done.
                await upsert_donation(donation)
            if page.next_cursor is None:
                return
            cursor = page.next_cursor

async def main() -> None:
    while True:
        since = datetime.now(timezone.utc) - timedelta(hours=1)
        await sync_recent_donations(since)
        await asyncio.sleep(60)

asyncio.run(main())
package main

import (
    "context"
    "log"
    "os"
    "time"

    donationsembed "gitlab.com/1st-flock/donations/go/donations-embed"
)

func syncRecentDonations(ctx context.Context, c *donationsembed.Client, since time.Time) error {
    var cursor string
    for {
        page, err := c.ListDonations(ctx, &donationsembed.ListDonationsParams{
            Cursor: cursor,
            Limit:  200,
        })
        if err != nil {
            return err
        }
        for _, d := range page.Donations {
            if d.CreatedAt.Before(since) {
                return nil
            }
            if err := upsertDonation(ctx, d); err != nil {
                return err
            }
        }
        if page.NextCursor == nil {
            return nil
        }
        cursor = *page.NextCursor
    }
}

func main() {
    c, err := donationsembed.New(os.Getenv("FLOCK_SECRET_KEY"))
    if err != nil {
        log.Fatal(err)
    }
    ticker := time.NewTicker(time.Minute)
    defer ticker.Stop()
    for range ticker.C {
        since := time.Now().UTC().Add(-time.Hour)
        if err := syncRecentDonations(context.Background(), c, since); err != nil {
            log.Printf("sync error: %v", err)
        }
    }
}

Refund with retry-on-timeout

A refund call that survives a network blip without double-refunding the donor.

import { Client, idempotencyKey, RateLimitError } from "@1st-flock/donations-embed-node";

const client = new Client({ apiKey: process.env.FLOCK_SECRET_KEY! });

async function refundWithRetry(donationId: string, amountCents: number) {
  const key = idempotencyKey("refund");
  for (let attempt = 1; attempt <= 5; attempt++) {
    try {
      return await client.refundDonation(
        donationId,
        { amountCents },
        { idempotencyKey: key },
      );
    } catch (err) {
      if (err instanceof RateLimitError) {
        await sleep((err.retryAfterSeconds ?? 1) * 1000);
        continue;
      }
      throw err;
    }
  }
  throw new Error("retry budget exhausted");
}
import asyncio
from donations_embed import Client, RateLimitError, idempotency_key

async def refund_with_retry(donation_id: str, amount_cents: int):
    key = idempotency_key("refund")
    async with Client(api_key=os.environ["FLOCK_SECRET_KEY"]) as client:
        for attempt in range(5):
            try:
                return await client.refund_donation(
                    donation_id,
                    amount_cents=amount_cents,
                    idempotency_key=key,
                )
            except RateLimitError as exc:
                await asyncio.sleep(exc.retry_after or 1.0)
                continue
        raise RuntimeError("retry budget exhausted")
package main

import (
    "context"
    "errors"
    "fmt"
    "time"

    donationsembed "gitlab.com/1st-flock/donations/go/donations-embed"
)

func refundWithRetry(ctx context.Context, c *donationsembed.Client, donationID string, amountCents int64) (*donationsembed.RefundResponse, error) {
    key := donationsembed.IdempotencyKey("refund")
    for attempt := 0; attempt < 5; attempt++ {
        resp, err := c.RefundDonation(ctx, donationID,
            &donationsembed.RefundParams{AmountCents: &amountCents},
            donationsembed.WithIdempotencyKey(key),
        )
        if err == nil {
            return resp, nil
        }
        var rl *donationsembed.RateLimitError
        if errors.As(err, &rl) {
            time.Sleep(rl.RetryAfter)
            continue
        }
        return nil, err
    }
    return nil, fmt.Errorf("retry budget exhausted")
}

Next steps

  • Server SDKs — typed clients in TypeScript / Node, Python, and Go.
  • Webhooks — receive donation events server-side.
  • Operations — versioning, deprecation, rate limits.
Developer Portal