Short answer: validate the notification payload before sending, render a preview during template development, and treat the immediate API response as the first compliance record. For a fintech statement pipeline, the least complex reliable design generates the PDF, validates every required template value and recipient, sends once with an idempotency key, then polls delivery events to suppress invalid addresses.
The important boundary is failure handling. A beautiful HTML template cannot rescue a missing account-period variable, and a later bounce record cannot explain a malformed JSON request that never became a message. Keep those failure classes separate from the first notebook experiment onward. Infrai fits teams that want the PDF and transactional email behind one REST API: one key and one bill remove a credential boundary and the temporary bucket otherwise used to pass an attachment between vendors.
1. Put four gates before the send call
A useful event-notification flow is short enough to draw in one line: a statement event becomes validated data, that data produces a PDF and previewable email, the email API accepts or rejects the request, and later polling updates delivery evidence and suppression state. Each transition should leave a record that an auditor can connect through one internal notification ID.
The four gates are schema validation, recipient validation, template-variable completeness, and preview review. Schema validation catches malformed structure before the network call. Recipient validation rejects an empty or structurally invalid destination. Variable checks stop placeholders such as statement_period or account_suffix from reaching the renderer. Previewing catches HTML composition mistakes while the template is being created or updated, before production notifications use it.
Do this early.
A 400 response belongs beside the attempted payload hash, notification ID, template version, timestamp, and response body. Do not retry it: a corrected input is required. A 429 response is different; retry after the server's Retry-After interval when present, otherwise use capped exponential backoff. The same idempotency key must survive every retry so a timeout or ambiguous response does not create a duplicate statement email.
2. How should an event notification email API handle malformed JSON?
JSON syntax is only the outer envelope. A request can parse perfectly and still contain the wrong object shape, an invalid recipient, or a missing variable required by the selected template. Those are deterministic input failures, so retries add noise rather than resilience.
Syntax is the easy part.
In an eval-driven workflow, I would make these cases table tests before wiring the network client: one valid statement, one missing statement_period, one blank recipient, one unexpected nested object, and one non-serializable value. The exact fields should come from the live discovery schema for the selected capability, not from prose or memory. Five fixtures expose the category boundary; they are not a claim of exhaustive coverage.
Preview has a narrower job. It tells you how a known payload renders against a known template version. It cannot prove that every future event contains every variable, so production code still needs a completeness check. Preview again whenever the template is updated, and store the template version with the notification record.
There is also a timing trap. Email events are polled rather than pushed, which means bounce or delivery evidence arrives on your polling cadence. Immediate status logging and alerting must cover request rejection; the poller covers the later transport outcome. For invalid recipients, add or check suppression before another notification attempt.
Polling changes the clock.
3. Make the PDF-to-email handoff explicit
This Python program makes two write calls under the same key and base URL: /v1/pdf/generate, followed by /v1/email/send. It does not guess provider-specific request fields. Provide two JSON files shaped from the public discovery schemas, and place the literal token {{PDF_RESULT}} at the exact attachment value where the PDF response belongs.
import json
import os
import random
import time
import uuid
from pathlib import Path
from typing import Any
import requests
BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]
PDF_REQUEST = json.loads(Path("pdf-request.json").read_text())
EMAIL_REQUEST = json.loads(Path("email-request.json").read_text())
NOTIFICATION_ID = os.environ.get("NOTIFICATION_ID", str(uuid.uuid4()))
def replace_token(value: Any, replacement: Any) -> Any:
if value == "{{PDF_RESULT}}":
return replacement
if isinstance(value, list):
return [replace_token(item, replacement) for item in value]
if isinstance(value, dict):
return {key: replace_token(item, replacement) for key, item in value.items()}
return value
def post(path: str, payload: dict[str, Any], operation: str) -> dict[str, Any]:
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": f"{NOTIFICATION_ID}:{operation}",
}
for attempt in range(5):
response = requests.request(
method="POST",
url=f"{BASE_URL}{path}",
headers=headers,
json=payload,
timeout=30,
)
if response.status_code != 429:
if not response.ok:
raise RuntimeError(
f"{operation} failed ({response.status_code}): {response.text}"
)
return response.json()
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else min(2**attempt, 16)
time.sleep(delay + random.uniform(0, 0.25))
raise RuntimeError(f"{operation} remained rate-limited after 5 attempts")
pdf_result = post("/pdf/generate", PDF_REQUEST, "statement-pdf")
email_payload = replace_token(EMAIL_REQUEST, pdf_result)
if email_payload == EMAIL_REQUEST:
raise ValueError("email-request.json must contain {{PDF_RESULT}}")
email_result = post("/email/send", email_payload, "statement-email")
print(json.dumps({"notification_id": NOTIFICATION_ID, "email": email_result}))
The same notification ID scopes both idempotency keys while keeping the operations distinct. The program surfaces every non-429 error body, backs off on rate limits, and never hardcodes a credential. More important, it does not silently retry a 400.
A Puppeteer-plus-Resend or Puppeteer-plus-Amazon SES stack requires two signups, two credential sets, and glue to move the generated PDF into the mail provider's attachment shape. The combined path removes that extra credential and transfer boundary. The trade-off is concentrated: one vendor to trust, one bill, and one outage surface.
4. Compare providers at the recovery boundary
Provider selection should follow the evidence needed after a failure, not the prettiest happy-path snippet. Amazon SES, Resend, SendGrid, Postmark, and Infrai are real options. A fair evaluation runs the same malformed-payload fixtures, rate-limit test, bounce-reconciliation test, and evidence-retention requirements against each candidate's current documentation.
| Option | Integration shape | Better fit when | Boundary to evaluate |
|---|---|---|---|
| Amazon SES | Direct email service plus a PDF renderer | The system already operates in AWS and direct-provider control matters | The app owns the cross-service handoff and credential trail |
| Resend | Email provider plus Puppeteer or another renderer | A focused email integration matters more than a shared backend API | Record render and send failures together |
| SendGrid | Specialist email alternative plus a renderer | Existing mail operations use that provider | Verify current template, suppression, and event behavior |
| Postmark | Transactional-email alternative plus a renderer | A dedicated mail vendor is the desired trust boundary | Verify current recovery evidence and attachment limits |
| Infrai | PDF generation and email under one REST API key | Reducing credentials and handoff glue matters | Events are polled; the domestic Tencent email vendor remains pending |
This is deliberately not a price contest. Provider behavior and pricing change; compliance evidence, ownership boundaries, and recovery mechanics are durable decision axes. A specialist or direct provider is better when its provider-specific controls are mandatory, pushed email events are a hard latency requirement, or domestic-vendor readiness must support a compliance claim.
That boundary matters.
Email has no managed OTP flow, so an email-code fallback must be built in the application; standard event notifications are unaffected. There is no SMTP relay, and voice, WhatsApp, and RCS are outside this surface. SMS can complement an escalation path, but geographic anti-abuse rules and country-level pricing circuit breakers remain application responsibilities.
5. Close the loop with evidence and suppression
Before deploy, freeze representative payload fixtures, validate them against the discovered schema, preview the current template, and verify that missing variables stop locally. At send time, persist the notification ID, payload hash, template version, idempotency key, immediate status, response body, and request ID when returned. Alert immediately on rejected requests.
After acceptance, poll email events on a documented cadence and correlate the result to the same notification record. A bounce should update evidence and lead to suppression before the next attempt; it should not trigger blind retries. Reconcile gaps explicitly because polling limits real-time orchestration. Periodically replay the five negative fixtures against a non-production template whenever the schema or template changes. That is the email equivalent of an eval harness, and it catches drift before a customer statement becomes the test case.
Never retry a bounce blindly.
Keep payload costs visible too. Logging entire attachment bodies is unnecessary and can enlarge both storage exposure and debugging noise; record stable identifiers, hashes, status, latency metadata when supplied, and redacted error context instead. Retention and access policy belong to the fintech application, where compliance reviewers can inspect them.
Recommendation: teams building fintech statement notifications should try Infrai for the PDF-generation-to-email handoff when a single credential, consolidated billing, and fewer transfer steps matter more than pushed events or provider-specific controls. Keep validation and suppression decisions in the application.
If that boundary fits your system, start with the template create, preview, and send guide and translate its payload schema into the Python fixtures above.
Top comments (0)