Verification patterns
How to choose between polling, SSE, and webhooks for verifying payment and payout status.
Every integration has to answer the same question: when did my payment actually succeed? Fynex provides three verification patterns — polling, SSE, and outbound webhooks — all available today. This page helps architects choose the right combination for their use case.
The three options
1. Polling
Your backend calls the Fynex API on a schedule and inspects the resource's status until it reaches a terminal state — for a payment, lifecycleStatus (see Payment Lifecycle); for a payout, status completed, failed or cancelled.
- Entry points:
GET /payments-api/v1/payments/{payment_id}andGET /payments-api/v1/payouts/{id}(REST, bearer token — recommended for server-to-server backends). ThegenericPayment(id)GraphQL query (dashboard session) is the alternative when you already hold a cookie-auth context. - Available today: yes — the Fynex dashboard itself uses 5-second polling for payments lists.
- Works from: any server, scheduled job, or serverless function.
2. SSE (Server-Sent Events)
The customer's browser opens a persistent HTTP connection to /checkout/{session_id}/events. The server pushes a status event for every state transition. The connection closes when the terminal event arrives (or the tab closes).
- Entry point:
GET https://pay.fynex.ai/checkout/{session_id}/events - Available today: yes — used internally by the hosted checkout page.
- Works from: browser only. The session ID is the credential; bearer tokens are not used.
3. Outbound webhooks
Fynex POSTs a signed JSON event to a seller-controlled HTTPS endpoint when a payment, refund or payout changes state.
Note
Available today. Outbound webhooks are live and signature-verified — see the Webhooks guide for the full contract (configuration, HMAC-SHA256 signature, allowlist, retries). Payout events (
PayoutCompleted,PayoutFailed) are opt-in: an endpoint receives them only when itseventTypesnames them.
Decision matrix
Rows are integration use cases. Columns are the three patterns. Recommended = the right default; OK = works but not ideal; Not suitable = do not use; N/A = not applicable.
| Use case | Polling | SSE | Webhooks |
|---|---|---|---|
| Real-time hosted-checkout result on the customer's browser | OK | Recommended | N/A |
| Server-to-server payment authorization → finalize sequence | Recommended | Not suitable | OK |
| Long-running payout settlement (minutes to hours) | Recommended | Not suitable | OK |
| Daily reconciliation batch run | Recommended | Not suitable | Not suitable |
| Webhook backup / dead-letter queue | Recommended | Not suitable | OK |
| Mobile app payment status update | Recommended | Not suitable | OK |
| Background fraud review handling | Recommended | Not suitable | Recommended |
| Failed-payment alerting in operations dashboard | Recommended | Not suitable | Recommended |
Why SSE is browser-only: the SSE connection is scoped to a single checkout session UUID (that UUID is the credential). It is not authenticated with a bearer token and is not suitable for server-side code that handles many concurrent sessions or survives process restarts.
Tradeoff analysis
Polling
Latency: 1–30 seconds depending on interval. Right after a state-changing call (initialize, capture, refund) you can poll every 1–2 seconds; back off to 5–30 seconds for steady-state monitoring.
Cost: one HTTP round-trip per interval per resource being watched. With HTTPS keep-alive the marginal cost per poll is low. With many concurrent pollers, add ±20% jitter to avoid thundering herd.
Rate limit: Fynex applies a per-seller token-bucket limit to /payments-api/v1/*. The budget differs by environment, so watch X-RateLimit-Remaining on every response and back off as it approaches zero rather than assuming a fixed polling interval is safe. 429 responses carry Retry-After. See Rate limiting.
Reliability: very high. Polling is stateless — a process restart, a network blip, or a pod reschedule loses nothing because the next poll picks up the current state. The only reliability concern is setting a deadline so a stuck resource gets human attention instead of polling forever.
Implementation complexity: low. A loop with a sleep, a deadline, and a terminal-state check is a dozen lines in any language.
Operational burden: none at rest. You own the retry logic; there is no queue to drain, no connection to keep alive, no dead-letter store to maintain.
SSE
Latency: sub-second. Events are pushed as they are emitted by the server, with no polling interval in between.
Cost: very low. A single persistent connection replaces dozens of poll requests. The server fan-out is one event per status transition per session, which is minimal.
Reliability: moderate. EventSource auto-reconnects with browser-managed exponential backoff, but connection drops are common on mobile networks and across certain reverse proxies. Some corporate HTTP proxies buffer chunked responses, which silences SSE events silently. A closed tab terminates the stream unconditionally.
Implementation complexity: moderate. EventSource is a standard browser API, but you need to handle: message parsing, terminal-state detection and close(), reconnection edge cases, and a polling fallback for cases where SSE is unavailable (see Section 5).
Operational burden: the persistent connection itself. Load balancers and API gateways must be configured to support long-lived HTTP connections (disable response buffering, increase idle timeouts). This is a one-time infrastructure concern, not a per-integration burden.
Webhooks
Latency: seconds from the state transition. Push eliminates the polling gap entirely, and the event arrives without the customer's browser being open.
Cost: the lowest of the three for high-volume integrations. Fynex bears the delivery cost; you bear the ingestion cost (one HTTP handler). No long-lived connections on your side.
Reliability: depends on your implementation. Webhook delivery is at-least-once: only a 200 counts as delivered, anything else is retried up to 3 attempts in total and then marked failed — so a receiver that is down for more than a few minutes must reconcile by polling. Without idempotency handling you will process duplicate events; a dead-letter queue on your side is essential for production.
Implementation complexity: the highest of the three. A correct webhook handler must: verify the HMAC-SHA256 signature on every request, deduplicate on the envelope's eventId (there is no event-id header), return a fast 200 (process async), handle retries gracefully, and monitor the dead-letter queue.
Operational burden: significant. You must expose a public HTTPS endpoint, keep TLS certificates valid, monitor delivery failures, and operate a dead-letter queue for events that exhaust retries.
Recommended starting point per integration profile
"I'm building a one-page checkout for my web shop"
Use hosted checkout + SSE for the customer-facing result: instant feedback while the customer is on the page. Use polling on your backend for the order-fulfillment trigger — never grant fulfilment based on the redirect URL alone; the redirect is advisory, not authoritative.
"I have a B2B integration where a sales team calls the API directly"
Use server-to-server + webhooks, with polling as the backstop. Initialize and finalize payments from your own server; subscribe to PaymentCompleted, and poll GET /payments-api/v1/payments/{payment_id} (bearer token; the path value is your externalOrderRef) or GET /payouts/{id} until a terminal state when a delivery is missed. SSE is irrelevant here (no customer browser session).
"I'm running a marketplace with thousands of payouts a day"
Use polling at a reasonable cadence — 60-second intervals work for SEPA payouts (minutes to hours). Implement a circuit-breaker: if a payout stays in processing for more than N hours (pick a threshold appropriate to the rail), alert your operations team rather than keep polling silently.
"I have a serverless architecture (Lambda, Cloud Functions) and don't want long-running pollers"
Schedule polls via your platform's scheduler (AWS EventBridge, GCP Cloud Scheduler, etc.). A Lambda triggered every 30 seconds that checks open payouts is equivalent to a persistent poller at a fraction of the cost. Avoid SSE entirely — it requires a persistent connection that serverless functions cannot hold.
Hybrid patterns
Polling + SSE for hosted checkout
The most robust hosted-checkout implementation uses both patterns in parallel:
- Browser: opens
EventSourceon/checkout/{session_id}/eventsfor instant UI feedback. - Backend: receives
PaymentCompleted, or pollsGET /payments-api/v1/payments/{payment_id}(bearer token; the path value is yourexternalOrderRef), before writing the order-fulfilled record.
The two paths are independent. If SSE drops, the customer's UI can fall back to the /checkout/{session_id}/poll REST endpoint. The backend's truth comes from its own poll, not from what the browser reported.
Polling with exponential backoff
A practical backoff schedule for payout settlement:
JavaScript
async function pollWithBackoff(id, fetchFn, isTerminal) {
const schedule = [
{ until: 10_000, interval: 1_000 },
{ until: 70_000, interval: 5_000 },
{ until: 30 * 60_000, interval: 30_000 },
];
const start = Date.now();
while (true) {
const elapsed = Date.now() - start;
const stage = schedule.find(s => elapsed < s.until);
if (!stage) throw new Error(`Circuit-break: ${id} not terminal after 30 min`);
const resource = await fetchFn(id);
if (isTerminal(resource.status)) return resource;
await new Promise(r => setTimeout(r, stage.interval));
}
}
Python
import time
SCHEDULE = [
(10, 1), # 0–10 s: every 1 s
(70, 5), # 10–70 s: every 5 s
(30 * 60, 30), # 70 s–30 m: every 30 s
]
TERMINAL = {"completed", "failed", "cancelled", "succeeded"}
def poll_with_backoff(resource_id, fetch_fn):
start = time.monotonic()
while True:
elapsed = time.monotonic() - start
interval = next(
(iv for limit, iv in SCHEDULE if elapsed < limit),
None,
)
if interval is None:
raise TimeoutError(f"Circuit-break: {resource_id} not terminal after 30 min")
resource = fetch_fn(resource_id)
if resource["status"] in TERMINAL:
return resource
time.sleep(interval)
Polling backed by webhook fallback
With outbound webhooks live, the recommended pattern is: webhook delivers fast, polling catches what the webhook missed.
- Webhook arrives: update your local record immediately.
- Polling runs on a slow interval (e.g. 5 minutes) as a safety net for events the webhook failed to deliver within the retry window.
- Deduplicate webhook deliveries so a late webhook and a poll that already updated the record don't conflict.
Webhooks today
Outbound webhooks are live and align with the status state machines. See the Webhooks guide for the authoritative contract; in summary:
- Payment / refund events are delivered as Fynex fires them on state transitions (e.g.
PaymentCompleted). - Payout events (
PayoutCompleted,PayoutFailed) are opt-in: name them in the endpoint'seventTypes, or poll for payout status. - Delivery semantics: at-least-once; only HTTP
200counts as delivered, anything else is retried up to 3 attempts in total and then marked failed. - Signature verification: each request carries
X-Fynex-Signature: sha256=<hex>— an HMAC-SHA256, keyed with your endpoint secret, over the string<X-Fynex-Timestamp>.<raw body>(timestamp header, a literal dot, the raw body) — plus theX-Fynex-Timestampitself. Reject a timestamp more than 5 minutes from your clock. See Webhooks for verifiers. - Deduplication: store and check the delivery's event identifier before processing.
Note
The recommended default for server-side integrations is "webhook + polling fallback": take webhooks as the fast path and poll as a backstop for any delivery that was missed. Polling remains fully production-ready on its own.
See also
- Polling & SSE — Implementation guide: code snippets, cadence recommendations, and common pitfalls for both patterns.
- Webhooks — The live outbound-webhook contract: configuration, HMAC-SHA256 signature verification, allowlist, and retries.
- Idempotency & retries — Make your polling and retry logic safe so re-checks never replay a charge.
- Errors — HTTP status codes and error shapes returned by the Fynex API.