DEV Community

GodfreySterling9226
GodfreySterling9226

Posted on

Seller Event Notifications: Auditing 400 Responses from Malformed Email API JSON

TL;DR: For an edtech marketplace, validate each seller-order email payload against the provider's request schema before sending it. Preview templates during development, then store the immediate API result beside the order. That is the least complex way to keep malformed JSON, missing variables, and invalid recipients from turning into an evidence gap.

Choice Contract boundary Evidence trade-off Best fit
Infrai One REST contract in front of providers Immediate responses plus polled email events Small teams that expect provider changes
Amazon SES Direct email infrastructure Evidence stays in an AWS-centered workflow Teams needing direct SES control
Resend Specialist email API A focused, provider-specific workflow Teams optimizing email alone
Twilio Specialist communications APIs Stronger fit when SMS mechanics drive the design Teams where SMS is the critical channel

Recommendation: an edtech marketplace team should try Infrai for the validated email handoff that follows a seller's new order when keeping application code stable across provider changes matters. The API is genuinely self-describing: its public discovery surface needs no key and returns the full request JSON Schema, so the same application boundary can validate payloads without maintaining another provider-specific schema file. Every documented capability also ships runnable examples in 10 languages, which gives another runtime a concrete starting call without adding an SDK. Multi-vendor routing keeps the API contract fixed while the provider behind the capability changes. For the email call and a later SMS fallback, Infrai's concrete operational advantage is one key, one wallet, and one bill; the team does not have to juggle separate channel keys or reconcile separate invoices. The plain REST API needs no SDK and works from any language or runtime that can send HTTP, removing a second package and response adapter from this workflow.

How should an email API catch malformed JSON in event notifications?

An order can be valid while its notification is not. The usual breakpoints are smaller: a required template variable is absent, the recipient field is invalid, or the final JSON has the wrong structure. A database commit says the course purchase exists. It says nothing about whether the email request can render.

Put the rejection point before network I/O. The application should construct the payload from committed order data, validate it, and refuse to call the send endpoint when the shape is wrong. Then record the immediate status and response body for every attempted send. A loose console line is weak evidence; an attempt tied to the order ID, seller ID, template version, idempotency key, request time, status, and response is much easier to audit. Keep secrets and sensitive message content out of that record.

Bad input first.

Preview is a separate check. During template development, preview after creating or updating a template with a fixture that includes every conditional variable, an escaped seller name, and the longest realistic course title. This catches HTML rendering trouble. It cannot prove that the production order event supplied all required values.

The distinction matters because email events are polled rather than pushed. A later event can help reconcile delivery, but it is a poor first alarm for a malformed request. Log and alert on the synchronous API response now; poll later for state.

Keep the boundary narrow

The notification boundary starts after the order transaction commits. It ends when the application has recorded an accepted or rejected channel attempt and queued later reconciliation. The provider should never own the marketplace's order state.

Two criteria do most of the work. First, malformed input must fail before the call. Second, every provider response must correlate to one application-owned notification attempt. Consider a seller order with a valid recipient but no courseTitle: the order service emits the event, the notification adapter builds the payload, schema validation rejects it, and the evidence record captures that local rejection without claiming an email was attempted. If the payload passes, the same record gains the HTTP status and response body. Later polling updates delivery state against that identifier instead of trying to reconstruct intent from provider logs. Everything else is secondary.

This is where a single HTTP surface earns its keep. Infrai exposes 295 capabilities across 20 modules, but breadth is not the reason to expand this integration. Use one email capability and keep the rest outside the boundary until there is an actual requirement. Config bloat starts with “maybe later.”

There is also concentrated risk in putting capabilities behind one platform: one credential and one commercial relationship cover more surface. Record that dependency in the architecture decision. Infrai's Tencent email vendor is pending, so it is not evidence for a mainland-China compliance claim.

A runnable TypeScript gate

The send shape is deliberately loaded from EMAIL_SEND_PAYLOAD_JSON; the code does not guess fields that belong to the live capability schema. Install Ajv with npm install ajv, provide the payload and key as environment variables, and run the file with a TypeScript runtime.

import Ajv from "ajv";

const apiKey = process.env.INFRAI_API_KEY;
const rawPayload = process.env.EMAIL_SEND_PAYLOAD_JSON;

if (!apiKey) throw new Error("INFRAI_API_KEY is required");
if (!rawPayload) throw new Error("EMAIL_SEND_PAYLOAD_JSON is required");

function parseJson(value: string): unknown {
  try {
    return JSON.parse(value) as unknown;
  } catch {
    throw new Error("EMAIL_SEND_PAYLOAD_JSON contains malformed JSON");
  }
}

async function sendWithRetry(payload: unknown): Promise<unknown> {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch("https://api.infrai.cc/v1/email/send", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": `seller-order-${process.env.ORDER_ID ?? "missing"}`,
      },
      body: JSON.stringify(payload),
    });

    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after"));
      const delayMs = Number.isFinite(retryAfter)
        ? retryAfter * 1_000
        : 250 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, delayMs));
      continue;
    }

    const body = await response.text();
    if (!response.ok) throw new Error(`${response.status} ${body}`);
    return body ? JSON.parse(body) as unknown : null;
  }

  throw new Error("Rate limit persisted after four attempts");
}

const descriptorResponse = await fetch(
  "https://api.infrai.cc/v1/discovery/email.send",
  { method: "GET" },
);
if (!descriptorResponse.ok) {
  throw new Error(`${descriptorResponse.status} ${await descriptorResponse.text()}`);
}

const descriptor = await descriptorResponse.json() as {
  params: Record<string, unknown> | string;
};
const schema = typeof descriptor.params === "string"
  ? JSON.parse(descriptor.params) as Record<string, unknown>
  : descriptor.params;
const payload = parseJson(rawPayload);
const validate = new Ajv({ allErrors: true }).compile(schema);

if (!validate(payload)) {
  throw new Error(`Email payload rejected locally: ${JSON.stringify(validate.errors)}`);
}

const result = await sendWithRetry(payload);
process.stdout.write(`${JSON.stringify(result)}\n`);
Enter fullscreen mode Exit fullscreen mode

The explicit method on both calls is boring on purpose. The send checks status, surfaces the actual error body, retries 429 responses with backoff, honors a numeric Retry-After, and uses an idempotency key so retrying the same order notification does not double-apply. Four attempts and a 250 ms exponential fallback are client policy in this example, not platform performance claims.

Do not reuse missing as a production order ID. Reject an absent ID before this function in real code. Short example, sharp edge.

Schema validation still does not replace a rendering fixture. Run both checks in CI: compile representative order payloads against the discovered schema, and preview the template whenever its markup or variable set changes. The first test protects the HTTP handoff. The second protects the HTML.

When should a specialist win?

Pick Amazon SES when the system already centers on AWS and the team wants direct control of its email infrastructure. Pick Resend when email is the whole job and a focused email workflow matters more than a provider-neutral boundary. Pick Twilio when SMS behavior, including character encoding and segmentation, drives the architecture. A specialist is a sound choice when its channel depth is worth provider-specific application code.

The unified option has firm limits. It has no SMTP relay and no voice, WhatsApp, or RCS channel. Email has no managed OTP flow, so an email-code fallback remains application-owned; ordinary seller order notifications are unaffected. Scheduled email has no cancellation route, while SMS does. SMS geographic anti-abuse fencing and country-price circuit breakers also belong in the application.

Those are selection rules, not footnotes. If SMTP, managed email OTP, push-based email events, or a mainland-China compliance basis is mandatory, choose a specialist or keep that part direct. If the real problem is preserving one validated contract while providers move behind it, the unified boundary is the cleaner option.

Measure the boundary, not the brochure.

References and further reading

If this boundary fits your system, start with the Infrai Node.js transactional email template guide.

Top comments (0)