# Verification patterns

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](https://docs.fynex.ai/payments-api/v2/docs#tag/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](https://docs.fynex.ai/payments-api/v2/docs#tag/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](https://docs.fynex.ai/payments-api/v2/docs#tag/errors).

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

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:

```flow
caption: Stop as soon as the payout reaches a terminal status.
node fast 0 0 | 0–10 s | Poll every 1 s | Immediate confirm | end
node typical 1 0 | 10–70 s | Poll every 5 s | Fast-path settle
node slow 2 0 | 70 s–30 m | Poll every 30 s | Slow or queued | warn
node stop 3 0 | > 30 m | Circuit-break, alert, stop | | end error
edge fast typical | not terminal
edge typical slow | not terminal
edge slow stop | not terminal
```

#### JavaScript

```js
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

```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](https://docs.fynex.ai/payments-api/v2/docs#tag/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](https://docs.fynex.ai/payments-api/v2/docs#tag/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](https://docs.fynex.ai/payments-api/v2/docs#tag/polling-sse)** — Implementation guide: code snippets, cadence recommendations, and common pitfalls for both patterns.
- **[Webhooks](https://docs.fynex.ai/payments-api/v2/docs#tag/webhooks)** — The live outbound-webhook contract: configuration, HMAC-SHA256 signature verification, allowlist, and retries.
- **[Idempotency & retries](https://docs.fynex.ai/payments-api/v2/docs#tag/idempotency)** — Make your polling and retry logic safe so re-checks never replay a charge.
- **[Errors](https://docs.fynex.ai/payments-api/v2/docs#tag/errors)** — HTTP status codes and error shapes returned by the Fynex API.
