# 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` field until it reaches a terminal state (`succeeded`, `failed`, `cancelled`, `completed`).

- **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 or refund changes state.

> [!NOTE]
> **Available today.** Outbound webhooks are live and signature-verified — see the [Webhooks](https://api.fynex.ai/payments-api/v2/docs#tag/webhooks) guide for the full contract (configuration, HMAC-SHA256 signature, allowlist, retries). Payout events do not yet emit webhooks, so keep polling for payouts.

---

## 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://api.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 inherently at-least-once with retries; without idempotency handling, you will process duplicate events. A dead-letter queue is essential for production. Reliability also depends on Fynex's retry policy (expected: backoff for at least 24 hours).

**Implementation complexity**: the highest of the three. A correct webhook handler must: verify the HMAC-SHA256 signature on every request, deduplicate by `X-Fynex-Event-Id`, 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 + polling on the backend**. Initialize and finalize payments from your own server; poll `genericPayment(id)` or the payout endpoint until terminal state. SSE is irrelevant here (no customer browser session). When webhooks ship, add them as a supplement.

### "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**: polls `genericPayment(id)` (or receives the finalize response and verifies it) 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:

```
0–10 s     →  poll every 1 s   (immediate confirmation window)
10–70 s    →  poll every 5 s   (typical fast-path settlement)
70 s–30 m  →  poll every 30 s  (slow provider / queued)
> 30 m     →  circuit-break, alert, stop polling
```

#### 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://api.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** do not yet emit webhooks — poll for payout status.
- **Delivery semantics**: at-least-once with retry-and-backoff; your receiver must return HTTP `200`.
- **Signature verification**: each request carries an `X-Fynex-Signature` header containing an HMAC-SHA256 digest of the raw request body keyed with your webhook signing secret, plus an `X-Fynex-Timestamp`.
- **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 (and is the only option for payouts).

## See also

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