Skip to main content
FASTCREDITS · FOR HEALTHCARE PROVIDERS

Hold, capture, refund. Every consult.

Patients pay into a hold the moment a consult starts. You receive when the consult ends — or you release the hold and they get their credits back. Idempotent capture, deterministic refund grammar, ledger-quality audit trail.

FastCredits gives you a hold-and-capture lifecycle that mirrors the operational reality of a healthcare service — patients reserve credits at booking, you finalise only the actual cost at consult end, you refund deterministically when something goes wrong. Every capture is idempotent, every refund is bound to the original debit by reference grammar, and every transaction is one immutable row on a SERIALIZABLE ledger.
01 / 06

1 · Booking → hold

When a patient books a consult, your service places a hold against their FastCredits account through POST /v1/accounts/:id/hold. The credits move from their balance into held_total atomically. The hold lives for the consult duration plus a buffer; the handler caps every hold at twenty-four hours regardless. You see the incoming hold, the patient identifier, and the deadline; the patient sees the held subline on their balance card.

Pending consultsDoorcta · Dr. Adesina
PatientConsultHeldReferenceStatus
usr_42a8…GP follow-up · 25 min250 CREDITSconsult_42a8…Awaiting capture
usr_eef0…Initial assessment · 40 min1,800 CREDITSconsult_eef0…Awaiting capture
usr_7c11…Repeat prescription · 10 min120 CREDITSconsult_7c11…Awaiting capture
Held · not yet earnedCapture on consult-endNo PII via FastCredits
3 PENDING
fastcredits.fastclinic.xyz/dashboard
02 / 06

2 · Consult ends → capture

When the consult ends, you call POST /v1/holds/:id/capture with the actual amount. CaptureHold zeros the held_total — the credits are already gone from balance — and writes a debit transaction with reference capture:{originalRef}. The capture is idempotent: a retry after a network blip returns the same row, not a second debit. The single-writer 409-is-success invariant means a 409 response from a retry is a confirmation, not an error, when your service is the sole writer of the hold's reference.

Capture confirmedDoorcta · Dr. Adesina
  1. 09:42:18Consult ended · 8m 16s
  2. 09:42:18POST /v1/holds/hld_42a8…/capture
  3. 09:42:18201 · ledger row · ref capture:consult_42a8…
  4. 09:42:18held_total ↓ 250 · balance unchanged

Idempotent — duplicate calls return same row.

Append-only · 1 ledger rowSERIALIZABLE · Tx auto-retried409-on-retry = success
CAPTURED
fastcredits.fastclinic.xyz/dashboard
03 / 06

3 · Patient cancels → refund

Cancellation has two paths. Before capture, you call POST /v1/holds/:id/release; the held credits return to the patient's balance and a release transaction is written with reference release:{originalRef}. After capture, you call POST /v1/transactions/:id/refund; the refund returns the credits and writes a refund transaction with reference refund:{originalTxID} and related_tx_id pointing at the captured debit. Only debits can be refunded; the refund is idempotent on its own reference.

Refund this consulttx_42a8 · 250 CREDITS · debit

Only debit transactions can be refunded.

Cancel
Issue refund
  1. 10:14:22POST /v1/transactions/tx_42a8…/refund
  2. 10:14:22201 · ref refund:tx_42a8… · +250 CREDITS to patient
  3. 10:14:22related_tx_id chains refund row → original debit
Idempotent on refund:<txID>Provider sees patient amount restored
REFUND
fastcredits.fastclinic.xyz/dashboard
04 / 06

4 · Earnings summary — coming 2026

The provider earnings summary is on the 2026 roadmap, shipping alongside the dedicated Provider Payouts service. Today, every capture and refund is a ledger row your service token can read via GET /v1/accounts/:id/transactions; the dashboard surface that aggregates them into a daily, weekly, monthly view is what 2026 unlocks. The mock is here so you can see the shape we are designing toward — earnings by period, capture rate, refund rate, average consult value — and shape it before it ships.

Earnings SummaryThis week · Mon–Sun
Today
42 consults captured
This week
32,650 CREDITS
Capture rate
94.2%
Captured · CREDITS / daypeak Fri 7,100
Released holds · CREDITS / daycancelled within hold window
Aggregated from ledger readsService-token auditable today
Coming 2026
fastcredits.fastclinic.xyz/provider/earnings
05 / 06

5 · Daily payout statement — coming 2026

Provider payouts are forward-looking. The Provider Payouts service is the 2026 surface that will roll up the captured-debit transactions into a daily statement, settle to your bank account on a configurable cadence, and reconcile any refunds against the same period. Today the underlying ledger primitives are already there — every captured debit is a row, every row carries a source_service, every row is idempotent — so when payouts ship, the migration is a query rather than a re-architecture.

Payouts StatementLast 5 days
DateGrossFeesNet to bankStatus
Apr 25₦7,100−₦107₦6,993Paid
Apr 24₦6,300−₦95₦6,205Paid
Apr 23₦4,900−₦74₦4,826Paid
Apr 22₦5,100−₦77₦5,023In transit
Apr 21₦4,250−₦64₦4,186Pending

Settled to GTBank •••• 8821 · payouts run T+1 (next business day).

Paystack settlementCSV exportPer-batch reference
Coming 2026
fastcredits.fastclinic.xyz/provider/payouts
06 / 06

6 · Capture-rate analytics — coming 2026

Capture rate — captures over holds — is the metric that tells you whether your operational reality matches the booking-to-consult cycle the ledger assumes. A capture rate dragging below ninety percent suggests holds are expiring before consults end, or releases are firing for reasons unrelated to cancellation. The analytics surface is on the 2026 roadmap; today the underlying data is in the ledger as transaction rows your service token can read.

Capture-rate analyticsLast 30 days
Capture rate
93.6%
Release rate
3.1%
Refund rate
2.4%
Expired hold
0.9%
Capture rate · daily %range 87–97%
Aggregated from ledgerDaily granularity90-day retention
Coming 2026
fastcredits.fastclinic.xyz/provider/analytics
What you get

Predictable hold/capture lifecycle

Hold at booking, capture at consult end, release on cancel. The same primitive a card network uses for a pre-authorisation, applied to healthcare. The default hold is ten minutes; the maximum is twenty-four hours; the partial index on active holds keeps the lookup fast even when most holds are terminal.

Idempotent capture and refund

Capture and refund are bound to deterministic reference grammar — capture:{originalRef}, refund:{originalTxID} — so any retry after a network failure resolves to the original row. The single-writer 409-is-success invariant lets you treat a 409 as confirmation when your service is the sole writer of the reference.

Refund grammar built into the ledger

Only debits can be refunded. The refund row carries related_tx_id pointing at the captured debit, the negative-of-the-debit amount, and the deterministic reference. There is no separate chargeback flow, no separate refund table, no separate audit trail.

Ledger-quality audit trail

Every transaction is one immutable row. BEFORE-UPDATE and BEFORE-DELETE triggers prevent any historical row from being edited. balance_after and held_after are snapshotted on insert. The seven-year audit-chain retention applies when FastCredits sits behind the consolidated audit posture.

No chargeback exposure

Card disputes are Paystack's concern. FastCredits never sees a card number; the ledger is one layer above the rail. When a patient disputes a charge with their bank, Paystack handles the chargeback against Paystack's records; the FastCredits refund grammar handles the patient-initiated cancel inside the ledger.

Capabilities

Ledger
  • Immutable transactions · BEFORE UPDATE/DELETE triggers
  • 5 transaction types · purchase, debit, refund, hold, release
  • balance_after / held_after snapshot per row
  • Append-only history · never re-derived
  • related_tx_id chains refund→debit, capture→hold
  • Currency code CREDITS · single allowed value
Concurrency
  • SERIALIZABLE isolation on every write
  • 40001 auto-retry · max 5 attempts
  • Backoff 5–200ms exponential with jitter
  • Optimistic version on accounts · bumped per write
  • SELECT FOR UPDATE on account row
  • No advisory locks · row lock is enough
Idempotency
  • UNIQUE(account_id, reference) index on transactions
  • capture:{ref} · release:{ref} · refund:{txID} grammar
  • Single-writer 409-is-success invariant
  • Webhook idempotency on Paystack reference
  • errIdempotentHit re-reads existing row
  • Refunds idempotent on refund:{txID}
Holds
  • DefaultHoldTTL 10 minutes · max 24 hours
  • Partial index WHERE status = 'active'
  • ExpireHolds background worker
  • Two-step authorize-capture pattern
  • release:{ref} returns held to balance
  • capture:{ref} finalises the debit
Cache & performance
  • Redis balance cache · prefix credits:balance:
  • TTL 5 minutes · best-effort fill on miss
  • Balance-only · held_total never cached
  • Invalidate on every write
  • Falls back to Postgres on cache outage
  • JWKS cache 5 minutes · singleflight on unknown kid
Compliance & residency
  • NDPA 2023 compliant · controller Fastclinic Limited
  • African data residency · single Nigerian region
  • Paystack as PCI-DSS-Level-1 processor
  • No PCI scope on FastCredits
  • Documented data-processing record
  • Append-only ledger · 7-year retention

Integrations

Fastclinic
FastLogin

Every FastCredits API call carries a Hydra-issued OAuth2 access token from fastlogin.fastclinic.xyz. The dashboard UI uses the fastcredits-public authorization-code-with-PKCE client, scoped to openid credits:read credits:write. Service callers (Doorcta, OneHealth, the FastLogin webhook itself) use the fastcredits-server client_credentials grant, scoped to services:credits. The middleware classifies tokens by client_id rather than sub-non-empty because Hydra 2.x stamps sub equal to client_id on client_credentials tokens. Account auto-creation runs on FastLogin's post-registration webhook with both 201 and 409 treated as success.

Fastclinic
Doorcta

Doorcta calls FastCredits at consultation start to PlaceHold against the patient's account, scoped to services:credits, with a duration that bounds the consult. On consultation end, Doorcta calls CaptureHold with the actual amount; on patient cancel, Doorcta calls ReleaseHold and the held credits return to balance. The Doorcta integration shape is locked at the API contract — POST /v1/accounts/:id/hold, POST /v1/holds/:id/{capture,release} — even though the active Doorcta caller is on the 2026 roadmap. The reference grammar (capture:{ref}, release:{ref}) makes every retry idempotent.

Fastclinic
OneHealth

OneHealth places a hold at session start and captures on session end. The hold TTL is computed from the session lifetime plus a one-hundred-twenty-second buffer rather than the FastCredits ten-minute default — pre-OH-09 the default silently overrode the requested duration, which is now honoured. Suspend-all and revoke cascade to ReleaseHold; the OneHealth session reconciler ticks every five minutes and recovers any hold that landed in pending or failed state. Break-the-glass sessions ship with capture_state=captured so cost cannot become a deterrent against legitimate emergency access.

External
Paystack

Paystack is the cash on-ramp. The dashboard initialises a Paystack transaction with the user's email, the amount in kobo (one hundred times credits), the reference, the callback URL, and metadata that pins the account_id, the user_id, and the credits_amount. The browser is redirected to Paystack's authorisation_url. The webhook is double-verified — X-Paystack-Signature HMAC-SHA512 over the raw body, then a server-side verify call before crediting the account. The unique index on (account_id, reference) handles duplicate webhook deliveries.

Compliance & safety

NDPA 2023 — controller Fastclinic Limited (RC 1919428)

FastCredits processes the personal-data fields it needs to operate the ledger — the FastLogin user identifier, the Paystack receipt email, the transaction metadata — under NDPA 2023 §25 lawful bases. The data controller is Fastclinic Limited, RC 1919428, registered in Lagos. The data-processing record is updated alongside every release that touches a new dataset or a new processor. We do not claim NDPC processor registration; that filing is in progress.

NDPA 2023
PCI scope — Paystack as PCI-DSS-Level-1 processor

Card data never lands in FastCredits. Paystack holds PCI DSS Level 1 attestation and operates the card-present and card-not-present rails; the dashboard never sees a primary account number, a CVV, or a bank credential. FastCredits stores the Paystack reference, the credits amount, the user identifier, and the receipt email. The boundary between processor and ledger is the answer when a regulator asks about cardholder data scope.

Paystack PCI compliance
Append-only ledger — Postgres BEFORE-UPDATE/DELETE triggers

Every transaction row is immutable. A BEFORE-UPDATE trigger and a BEFORE-DELETE trigger on the transactions table raise the exception "transactions table is append-only" against any DML that tries to edit or remove a historical row. Balances never re-derive from history; balance_after and held_after are snapshotted on insert. The seven-year retention applies to the audit chain that wraps the ledger when FastCredits sits behind the consolidated audit posture.

Source — initial_schema migration
SERIALIZABLE isolation — Postgres-level integrity

Every write path runs at SERIALIZABLE isolation. On a 40001 serialization conflict, the repository retries the entire transaction up to five attempts with exponential backoff (five to two hundred milliseconds, jittered). The account row is pinned with SELECT FOR UPDATE inside every write transaction. The combination is the strongest concurrency guarantee Postgres ships and the right primitive for a financial ledger.

Postgres SERIALIZABLE docs
Idempotency by design — UNIQUE(account_id, reference)

A unique index on (account_id, reference) deduplicates every transaction. A duplicate insert returns errIdempotentHit and the repository re-reads the existing row. The reference grammar — capture:{originalRef}, release:{originalRef}, refund:{originalTxID} — is server-derived, never user-supplied, so the same logical operation produces the same reference and converges on the same row. The pattern follows Stripe's industry-standard idempotency-key design adapted to a server-derived reference.

Stripe — Idempotency keys

Plain answers

Hold, capture, refund. Every consult, deterministically.

Patients reserve at booking. You finalise at consult end. Cancellations release automatically. Refunds bind to the original debit by reference. Talk to us about onboarding the integration; the API contract is locked, the reference grammar is deterministic, the ledger is ready.