Skip to content

Proofing & Mailing

Personalization Layer

Image personalization is the process of rendering unique front/back postcard images per recipient — inserting names, discount codes, and other dynamic data into the design template. The personalization layer has three rendering engines:

Engine Status Description Repo
Paper Run Figma Plugin Current Renders designs via Figma plugin, uploads to Cloudflare figma-plugin
Paper Run Backup Generator Live (fallback) Server-side renderer using Pixelixe under the hood, uploads to Cloudflare paperrun_image_exporter
Pixelixe Deprecated Direct Pixelixe API calls, does NOT upload to Cloudflare core/integrations/pixelixe.py

The rendering engine is selected per-campaign based on the template's provider_type. Both the Figma Plugin and Backup Generator produce Cloudflare URLs for front and back images. Pixelixe (deprecated) returns its own URLs directly.

Personalization Layer repo: paperrun_image_exporter — contains the rendering gateway that routes to the appropriate engine.

Proofing

Two Celery tasks work in tandem using an SQS queue as a buffer:

Staging: stage_proofs

Schedule: Every 15 seconds State transition: Pending → ProofingStaged

  1. Atomically grabs up to 50 Pending recipients via atomically_batch_update_and_retrieve_recipient_statuses()
  2. Enqueues them to SQS via bulk_enqueue_for_proofing()

Processing: dispatch_proofing_queue

Schedule: Every 7 seconds State transition: ProofingStaged → Proofing → Proofed

  1. Spawns multiple workers (count = config.QUEUE_DISPATCH_COUNT)
  2. Each worker dequeues up to 10 messages from SQS
  3. For each recipient:
  4. Sets status to Proofing
  5. Calls make_proof()runner.make_image_proofs() → rendering engine
  6. If using Figma Plugin or Backup Generator: images are uploaded to Cloudflare
  7. Sets status to Proofed
  8. Deletes the SQS message
graph TD
    A[Pending Recipients] --> B[stage_proofs]
    B -->|batch of 50| C[SQS Proofing Queue]
    C --> D[dispatch_proofing_queue]
    D --> E{Rendering Engine?}
    E -->|Figma Plugin| F[Figma Plugin renders]
    E -->|Backup Generator| G[Backup Generator renders]
    E -->|Pixelixe| H[Pixelixe renders]
    F --> I[Upload to Cloudflare]
    G --> I
    H --> J[Use Pixelixe URLs directly]
    I --> K[Proofed]
    J --> K

    classDef process fill:#e1f5ff;
    classDef success fill:#e1ffe1;
    classDef decision fill:#fff3e0;

    class A,B,C,D process;
    class F,G,H,I,J decision;
    class K success;

Mailing

Same two-task pattern as proofing:

Staging: stage_mailers

Schedule: Every 15 seconds State transition: Proofed → SendingStaged

Processing: dispatch_mailing_queue

Schedule: Every 7 seconds State transition: SendingStaged → Sending → Sent

  1. Dequeues recipients from SQS
  2. Selects printer via generate_printer_strategy():
  3. LOB — US printer
  4. IntelliPrint — UK/NL printer
  5. Stannp — UK printer
  6. Printer creates the mailpiece, returns tracking info
  7. Saves mailpiece, updates Stripe billing meter, submits Klaviyo event
  8. Cleans up Cloudflare images (if applicable)

Printer Error Handling

Printer errors are classified by provider and retryability:

  • PrinterError — Fatal error. Requires provider argument (e.g., "lob", "intelliprint"). Recipient moves to Error status.
  • RetryablePrinterError — Transient error (HTTP 500, 502, 520, 522, 524, 525). The SQS message is left in place for automatic retry instead of marking the recipient as Error.

Per-Printer Approval Rules

Before mailing, recipients are validated against printer-specific Pydantic models:

  • LobRecipientApproval — LOB-specific address/format rules
  • IntelliprintRecipientApproval — IntelliPrint-specific rules

Recipients that fail approval are set to Skipped with reason PrinterApprovalFailed. Validation errors are stored in recipient.json["approval_errors"] for debugging.

Image Handling by Printer

All printers apply size-appropriate Cloudflare variants via their gateway's size_variant_map. Sizes without an explicit variant fall back to public (a passthrough configured at max 30000x30000, fit:scale-down).

Campaign preview images are hosted with the public variant intentionally: previews show the rendered source creative, while printer-specific variants are applied only when a gateway submits a mailpiece.

Key Files

  • Proofing: celery/proofer.py, methods/proofs.py, methods/runner.py
  • Mailing: celery/mailer.py, methods/runner.py, methods/mailpieces.py
  • Rendering engines: core/integrations/internal/figma_plugin_generator.py, core/integrations/internal/pr_image_generator.py, core/integrations/internal/rendering_gateway.py, core/integrations/pixelixe.py
  • Printers: core/integrations/printers/lob.py, core/integrations/printers/intelliprint.py, core/integrations/printers/stannp.py
  • Printer errors: core/integrations/printers/errors.py
  • Approval rules: core/integrations/printers/approval_rules.py
  • Image hosting: core/integrations/cloudflare_images.py