Registration Ingestion & Payment Reconciliation: Pipeline Architecture & System Boundaries

High-volume event registration operates under strict latency, consistency, and financial-compliance constraints, and this section owns the segment of the workflow that sits between the attendee hitting “pay” and a validated, paid record becoming eligible for a badge. Payloads arrive from ticketing platforms and payment gateways, are deduplicated and gated against a hard schema contract, have their payment state reconciled against a canonical source of truth, and are then handed off to downstream rendering. Production reliability here is not achieved through optimistic network assumptions; it is engineered through explicit system boundaries, idempotent state transitions, and deterministic failure routing so that gateway jitter, out-of-order webhooks, or a provider API change never cascade into missing or duplicate badges on the print floor. This document targets event operations teams, registration managers, and the Python automation engineers responsible for keeping these time-sensitive, event-scale data flows correct under load.

Registration ingestion and payment reconciliation pipeline A left-to-right flow of seven stages — Ingestion Sources, Append-only Event Log, Idempotency Dedup Gate, Schema Validation Gate, Payment Reconciliation, Async Batch Queue, and Badge Handoff — with dashed failure arrows dropping from the two gates and the reconciliation stage into a dead-letter queue. IngestionSources Append-onlyEvent Log IdempotencyDedup Gate SchemaValidation Gate PaymentReconciliation Async BatchQueue BadgeHandoff webhooks + polling raw, immutable backpressure to badge print Dead-letter Queue quarantine · replay · audit on failure pipeline flow failure → dead-letter queue

Pipeline Architecture & Handoff Boundaries Link to this section

The pipeline is a sequence of single-responsibility stages separated by durable boundaries, and every boundary is a place where a payload can be safely parked rather than lost. Raw ingestion writes to an append-only event log before any interpretation happens; the deduplication gate rejects replays; the validation gate rejects malformed contracts; the reconciliation state machine resolves payment truth; and only then does a record cross into asynchronous processing. Each boundary is transactional and observable, so a failure at any stage quarantines the offending payload — typically into a dead-letter queue — instead of corrupting downstream state. The upstream edge of this section receives from external providers; its downstream edge hands finished, paid records to the Badge Generation & Template Sync workflow. Nothing past the async boundary is allowed to trigger an ingestion rollback, which keeps financial state and print state cleanly decoupled.

Stage 1 — Registration Ingestion & Polling Fallback Link to this section

Registration data enters through two mutually reinforcing vectors: real-time webhook delivery and scheduled API polling. Webhooks provide low-latency ingestion but introduce signature-verification overhead, out-of-order delivery, and potential replay attacks. Polling acts as the deterministic fallback, guaranteeing eventual consistency when gateway delivery fails or a network partition occurs. Robust form API polling strategies require cursor-based pagination, exponential backoff with jitter, and strict deduplication keyed on provider transaction IDs — the same discipline needed when polling Eventbrite web APIs without triggering rate limits. The ingestion layer enforces a hard rule: raw payloads must be persisted to an append-only event log before any transformation, validation, or routing occurs, isolating network instability from downstream state machines. Idempotency is enforced at the edge by hashing a composite key of provider_id, event_id, and submission_timestamp. When duplicates surface — from client retries, gateway redelivery, or polling overlap — the system rejects the payload and routes it to a dead-letter queue rather than silently overwriting existing records, preserving forensic integrity for manual review.

Stage 2 — Payment Webhook Handling & Signature Verification Link to this section

Real-time delivery requires cryptographic verification and strict timeout handling before a single byte of business logic runs. Correct payment webhook handling means validating the provider signature against the raw request body, tolerating a bounded timestamp skew to defeat replay, and returning an HTTP 2xx acknowledgment fast enough that the gateway does not itself retry and amplify load. In practice this is where most integrations regress: framework body-parser middleware mutates the payload and breaks the HMAC, or a slow handler blows the gateway’s delivery timeout. The concrete patterns for handling Stripe payment webhooks for ticket purchases — verifying with the raw body, catching stripe.SignatureVerificationError, and acknowledging before enqueueing work — apply to any modern gateway. The ingestion worker must never execute reconciliation or rendering inline; its sole responsibilities are signature verification, persistence to the event log, structural acknowledgment, and DLQ routing when verification fails. Everything expensive happens asynchronously, downstream of the acknowledgment.

Stage 3 — Schema Validation & Contract Gating Link to this section

Untrusted payloads cannot transition directly into payment reconciliation. The validation layer is a hard gate that enforces data contracts before any workflow executes. Building strict schema validation pipelines requires explicit type enforcement, deliberate field-coercion rules, and versioned schema registries so a provider changing its export format is caught immediately instead of poisoning downstream workers. Pydantic v2 models must be configured to reject rather than silently correct malformed data — the discipline described in validating attendee data with Pydantic before ingestion. Required fields, enum constraints, currency precision, and timezone normalization are codified at the pipeline entry point, and the canonical shape aligns with the event taxonomy schema design that governs the rest of the platform. Validation failures are routed through a structured taxonomy: structural violations (missing required fields, invalid JSON, type mismatches) trigger immediate rejection, while semantic violations (invalid currency codes, expired ticket types, mismatched attendee counts) trigger soft-fail routing for triage. Silent coercion is strictly prohibited; data integrity takes precedence over ingestion velocity.

Payment Reconciliation & State Management Link to this section

Payment reconciliation operates as a deterministic state machine spanning the webhook and batch stages. The pipeline must handle authorized, captured, refunded, and disputed transactions without introducing race conditions or double-counting. Idempotent upserts keyed on gateway_transaction_id prevent duplicate financial postings, and when gateway webhooks and polling responses diverge, the system resolves conflicts using a canonical source-of-truth policy and explicit reconciliation windows. The state machine enforces strict transitions: PENDINGAUTHORIZEDCAPTUREDRECONCILED. Any deviation trips a circuit breaker, halts downstream badge generation for the affected transaction, and routes it to the financial-ops queue. All state mutations are logged with audit trails — previous state, new state, actor (system or user), and reconciliation timestamp — so that a disputed charge can be reconstructed months later. Compensating workflows reconcile partial states, resolve timeout-induced UNKNOWN statuses, and drive automated refund or capture retries. Reconciliation is the last gate before a record is considered financially settled and therefore printable; nothing crosses into fulfillment on an unconfirmed payment.

Payment reconciliation state machine Happy-path states PENDING, AUTHORIZED, CAPTURED and RECONCILED run left to right; timeouts divert transactions to an UNKNOWN state that is either swept back into RECONCILED or escalated to a circuit breaker feeding the financial-ops queue. PENDING AUTHORIZED CAPTURED RECONCILED settled · printable authorize capture reconcile UNKNOWN timeout / partial Circuit breaker halt badge gen → financial-ops queue timeout > 120s sweep → reconcile unresolved valid transition timeout / escalation

Stage 4 — Async Batch Processing & Print Handoff Link to this section

Once a payload passes schema validation and payment reconciliation, it transitions to asynchronous processing. High-throughput badge generation requires decoupled workers that consume validated, paid records in controlled batches. A durable async batch processing layer provides backpressure handling, memory-constrained chunking, and checkpointing before payloads are pushed to print queues or badge APIs — the operational model detailed in using Celery for async registration batch processing. Workers enforce ordering guarantees per attendee group, apply exponential retry policies for transient print-service failures, and maintain a persistent offset tracker to prevent duplicate badge generation across pod restarts. The handoff boundary is explicit: once a payload enters the async queue it is considered successfully ingested, and control passes to the PDF routing workflows that render and dispatch the physical artifact. Downstream print failures are isolated to that workflow and never trigger ingestion or financial rollbacks, keeping the two subsystems independently recoverable.

Production-Ready Python Implementation Link to this section

The following worker demonstrates the section’s core contract end to end: it computes an idempotency key, rejects duplicates, validates against a strict Pydantic v2 model via model_validate, and routes every failure class to a dead-letter queue. It is runnable as-is and uses only explicit error handling suitable for time-sensitive event systems.

PYTHON
import hashlib
import json
import logging
from datetime import datetime
from typing import Any, Optional

import pydantic
from pydantic import BaseModel, ConfigDict, Field, ValidationError

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
)
logger = logging.getLogger("registration_ingestion")


class RegistrationPayload(BaseModel):
    model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)

    provider_id: str
    event_id: str
    submission_timestamp: str
    attendee_email: pydantic.EmailStr
    ticket_type: str
    currency: str = Field(pattern=r"^[A-Z]{3}$")
    amount_cents: int = Field(ge=0)
    gateway_transaction_id: Optional[str] = None

    @pydantic.field_validator("submission_timestamp", mode="before")
    @classmethod
    def validate_iso8601(cls, v: Any) -> str:
        if not isinstance(v, str):
            raise ValueError(f"submission_timestamp must be a string, got {type(v)}")
        try:
            datetime.fromisoformat(v.replace("Z", "+00:00"))
        except ValueError as exc:
            raise ValueError(f"Invalid ISO8601 timestamp: {v}") from exc
        return v


class IngestionError(Exception):
    """Base exception for pipeline ingestion failures."""

    def __init__(self, code: str, message: str, payload: dict[str, Any]):
        self.code = code
        self.message = message
        self.payload = payload
        super().__init__(self.message)


class IdempotencyStore:
    """Mock idempotency store. Replace with Redis/Postgres in production."""

    def __init__(self) -> None:
        self._seen: set[str] = set()

    def is_duplicate(self, composite_key: str) -> bool:
        if composite_key in self._seen:
            return True
        self._seen.add(composite_key)
        return False


class DLQRouter:
    """Mock dead-letter queue router."""

    @staticmethod
    def route(error: IngestionError) -> None:
        logger.warning(
            "DLQ_ROUTE | code=%s | msg=%s | payload=%s",
            error.code,
            error.message,
            json.dumps(error.payload),
        )


def compute_idempotency_key(payload: dict[str, Any]) -> str:
    required = ["provider_id", "event_id", "submission_timestamp"]
    missing = [k for k in required if k not in payload]
    if missing:
        raise IngestionError(
            code="MISSING_IDEMPOTENCY_FIELDS",
            message=f"Missing required idempotency fields: {missing}",
            payload=payload,
        )
    raw = (
        f"{payload['provider_id']}:{payload['event_id']}"
        f":{payload['submission_timestamp']}"
    )
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()


def process_registration(raw_payload: dict[str, Any], store: IdempotencyStore) -> None:
    """
    Ingestion boundary worker.
    1. Compute idempotency key
    2. Check for duplicates
    3. Validate against the schema contract
    4. Route to DLQ on any failure
    5. Acknowledge to the source system (mocked)
    """
    try:
        idem_key = compute_idempotency_key(raw_payload)
    except IngestionError as err:
        DLQRouter.route(err)
        return

    if store.is_duplicate(idem_key):
        logger.info("DUPLICATE_DETECTED | key=%s | skipping ingestion", idem_key)
        return

    try:
        validated = RegistrationPayload.model_validate(raw_payload)
    except ValidationError as ve:
        DLQRouter.route(
            IngestionError(
                code="SCHEMA_VALIDATION_FAILURE",
                message=str(ve),
                payload=raw_payload,
            )
        )
        return
    except Exception as exc:  # defensive: never crash the ingestion loop
        DLQRouter.route(
            IngestionError(
                code="UNEXPECTED_INGESTION_ERROR",
                message=str(exc),
                payload=raw_payload,
            )
        )
        return

    logger.info(
        "INGESTION_SUCCESS | key=%s | email=%s | amount=%d",
        idem_key,
        validated.attendee_email,
        validated.amount_cents,
    )


if __name__ == "__main__":
    store = IdempotencyStore()

    valid = {
        "provider_id": "stripe_001",
        "event_id": "evt_2024_q3",
        "submission_timestamp": "2024-09-15T14:30:00Z",
        "attendee_email": "[email protected]",
        "ticket_type": "VIP",
        "currency": "USD",
        "amount_cents": 15000,
        "gateway_transaction_id": "txn_998877",
    }

    process_registration(valid, store)           # ingested
    process_registration(dict(valid), store)      # duplicate → skipped

    process_registration(                          # invalid → routed to DLQ
        {
            "provider_id": "stripe_002",
            "event_id": "evt_2024_q3",
            "submission_timestamp": "not-a-date",
            "attendee_email": "invalid-email",
            "ticket_type": "GENERAL",
            "currency": "INVALID",
            "amount_cents": -50,
        },
        store,
    )

Failure Modes & Operational Runbook Link to this section

Failure Mode Trigger / Symptom Immediate Containment Resolution Path
Network partition / gateway timeout HTTP 5xx or ConnectionResetError on delivery Exponential backoff (max 3 retries), fall back to polling Verify gateway status page; switch to manual CSV reconciliation if the partition exceeds 15 minutes
Webhook replay / duplicate submission Idempotency key collision Reject, log, route to DLQ Audit the DLQ for systemic retry storms and tighten client-side retry policies
Schema drift / provider API change ValidationError rate exceeds 5% of throughput Circuit breaker halts ingestion for the affected provider Update Pydantic models, deploy the hotfix, then run historical payload revalidation
Payment gateway timeout / unknown state CAPTURED status missing after 120s Move the transaction to the reconciliation sweep queue Reconcile via provider dashboard; trigger a compensating refund or capture retry
DLQ saturation (> 10,000 pending) Queue-depth metric breaches threshold Pause ingestion, alert on-call, disable non-critical workers Forensic diff of failed payloads, run the bulk reprocessing script, complete root-cause analysis

Operational Guardrails Link to this section

These constraints are non-negotiable in production and should be enforced by CI checks, alert rules, and code review rather than convention:

  • Persist before you interpret. Raw payloads land in the append-only event log before validation, reconciliation, or routing — no exceptions.
  • Idempotency everywhere. Every ingest path and every financial upsert is keyed (provider_id+event_id+submission_timestamp for ingestion, gateway_transaction_id for payments); duplicate detection is mandatory, not best-effort.
  • Cap retries and always have a floor. Transient retries are bounded (max 3 with jittered backoff); anything that exhausts them lands in the DLQ, never in a silent drop or an infinite loop.
  • Verify signatures on the raw body. Webhook HMAC verification happens before any body parsing or business logic; acknowledge with 2xx before enqueueing downstream work.
  • No silent coercion. Malformed or semantically invalid records are rejected or soft-failed for triage — the pipeline never “fixes” data to keep moving.
  • Reconcile before you print. Only RECONCILED transactions cross the async handoff into badge generation; unconfirmed payments cannot produce a badge.
  • Instrument the boundaries. Emit correlation IDs on every record and expose Prometheus-compatible metrics for ingestion latency, reconciliation lag, and DLQ depth; alert thresholds tighten during early-bird and day-of registration windows.

This section is the ingestion-and-payment half of the platform. Continue with the workflows it depends on and hands off to: