Skip to content
In this section

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} and GET /payments-api/v1/payouts/{id} (REST, bearer token — recommended for server-to-server backends). The genericPayment(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 its eventTypes names 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.


"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:

  1. Browser: opens EventSource on /checkout/{session_id}/events for instant UI feedback.
  2. Backend: receives PaymentCompleted, or polls GET /payments-api/v1/payments/{payment_id} (bearer token; the path value is your externalOrderRef), 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:

Stop as soon as the payout reaches a terminal status.
not terminalnot terminalnot terminal0–10 sPoll every 1 sImmediate confirm10–70 sPoll every 5 sFast-path settle70 s–30 mPoll every 30 sSlow or queued> 30 mCircuit-break, alert, stop

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's eventTypes, or poll for payout status.
  • Delivery semantics: at-least-once; only HTTP 200 counts 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 the X-Fynex-Timestamp itself. 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.