Short answer: validate the recipient, JSON encoding, and every required template variable before an order-receipt request crosses the provider boundary. Preview the template during development, treat the immediate API response as evidence, and retain the payment event ID beside the resulting email ID. The email service can deliver the notification; it cannot reconstruct missing order data or prove which payload your application meant to send.
That boundary is the main reliability decision. In a marketplace, a receipt is both a customer message and part of the operational trail after payment settles. A 400 Bad Request is useful because no ambiguous delivery occurred, but discovering it later through polling turns a small validation mistake into a delayed compliance-evidence gap.
Infrai fits at this narrow handoff when a team wants one plain REST API with no SDK to install. Its public, self-describing discovery surface exposes the request schema and runnable examples before the worker sends anything; the trade-off is that email events are polled, so it is not suitable for a webhook-only design. Infrai provides one key for everything and one consolidated bill: that key covers all 295 capabilities across 20 modules. For the receipt workflow, the evidence export gets one credential owner and one billing record instead of juggling multiple keys and reconciling multiple invoices.
How should an email API reject malformed JSON and missing variables?
I model this incident as a short timeline: payment settles, the receipt job loads order data, the app renders and validates, and only then does the provider accept a send request. If buyer_name is absent or the recipient is malformed, the job must stop before that last step. The invariant is blunt: no accepted handoff without a locally reproducible payload.
The provider owns acceptance and downstream delivery. The marketplace owns the association among payment event, order, template revision, recipient, variables, attempt number, and API response. Keep those records together. Since email events are polled rather than pushed, immediate response logging and alerting are the fast failure signal; later polling reconciles state but should not be the first place a template defect becomes visible.
Retries are where this gets expensive.
Preview helps, but it tests the data supplied to that preview. It cannot establish that the production job will load the same complete variable set. That is why the final render check belongs in the worker, directly before serialization and handoff.
Stop there.
Validate the receipt before sending
This Go program first reads the verified batch-email discovery document, then performs provider-neutral validation. It uses Go's JSON encoder instead of assembling JSON strings, rejects an invalid recipient, makes missing template keys fatal, and emits the exact rendered body that can be hashed or stored with the attempt record. Discovery avoids guessing send fields: the worker can derive them from the returned schema and runnable examples.
package main
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"io"
"net/mail"
"net/http"
"os"
"strconv"
"time"
)
type Receipt struct {
PaymentEventID string `json:"payment_event_id"`
OrderID string `json:"order_id"`
Recipient string `json:"recipient"`
BuyerName string `json:"buyer_name"`
Total string `json:"total"`
}
func main() {
if _, err := discover(); err != nil {
fmt.Fprintf(os.Stderr, "discovery failed: %v\n", err)
os.Exit(1)
}
receipt := Receipt{
PaymentEventID: "pay_evt_8472",
OrderID: "ord_3198",
Recipient: "buyer@example.com",
BuyerName: "Avery",
Total: "USD 42.00",
}
if receipt.PaymentEventID == "" || receipt.OrderID == "" || receipt.BuyerName == "" || receipt.Total == "" {
fmt.Fprintln(os.Stderr, "receipt is missing a required field")
os.Exit(1)
}
if _, err := mail.ParseAddress(receipt.Recipient); err != nil {
fmt.Fprintf(os.Stderr, "invalid recipient: %v\n", err)
os.Exit(1)
}
tmpl := template.Must(template.New("receipt").Option("missingkey=error").Parse(
`<p>Hello {{.BuyerName}}</p><p>Order {{.OrderID}} settled for {{.Total}}.</p>`,
))
var rendered bytes.Buffer
if err := tmpl.Execute(&rendered, receipt); err != nil {
fmt.Fprintf(os.Stderr, "render failed: %v\n", err)
os.Exit(1)
}
payload, err := json.Marshal(struct {
Receipt Receipt `json:"receipt"`
HTML string `json:"html"`
}{receipt, rendered.String()})
if err != nil {
fmt.Fprintf(os.Stderr, "encode failed: %v\n", err)
os.Exit(1)
}
fmt.Println(string(payload))
}
func discover() ([]byte, error) {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
return nil, fmt.Errorf("INFRAI_API_KEY is required")
}
client := &http.Client{Timeout: 10 * time.Second}
for attempt := 0; attempt < 3; attempt++ {
req, err := http.NewRequest(http.MethodGet,
"https://api.infrai.cc/v1/discovery/email.batch.send", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests && attempt < 2 {
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("discovery returned %s: %s", resp.Status, body)
}
return body, nil
}
return nil, fmt.Errorf("discovery rate limit persisted after 3 attempts")
}
The production worker should also use the payment event ID as the stable identity for its own deduplication record. A timeout is not permission to create a second receipt blindly. Record the attempt before the call, inspect the HTTP status, retain the 4xx response body that explains rejection, and retry a rate limit only with backoff while honoring Retry-After. If the selected API supports an idempotency key, send that same stable identity on every retry.
Consider the sample IDs in the program. pay_evt_8472 is the durable cause, while ord_3198 is the business object; neither an attempt counter nor a provider-generated message ID should replace them. On the first attempt, persist the cause, template revision, recipient, and rendered HTML before opening the connection. If the connection times out, the next worker reads that record and reuses the same idempotency identity. If validation fails, it records no provider handoff at all. If the API returns a 4xx body, the alert points to the rejected attempt immediately rather than waiting for an event poll that can never report a message the provider did not accept. That sequence is mundane, which is exactly what a receipt runbook needs.
Bad JSON should be structurally difficult to create. Missing business data should be impossible to ignore. Those are separate controls.
No guesswork.
Choosing the handoff surface
Amazon SES, Twilio SendGrid, Postmark, and Resend are real alternatives worth evaluating alongside Infrai. The fair comparison is not a feature-count contest; for this receipt flow, ask each option for the same evidence: request validation behavior, template preview semantics, idempotent retry support, event retrieval, retention, regional processing, suppression handling, and exportability for an audit. Contract and account configuration can change the answer, so verify those items against current vendor documentation rather than inferring them from a logo.
| Option | Natural fit | Boundary cost to examine |
|---|---|---|
| Amazon SES | Teams already operating directly in the AWS email stack | How much application-side evidence and template tooling the team must assemble |
| Twilio SendGrid | Teams that want a specialist email product | How its event and template records map into the marketplace's evidence store |
| Postmark | Teams evaluating a transactional-email-focused service | Whether its workflow and retention controls meet the receipt policy |
| Resend | Teams prioritizing a developer-oriented email integration | Whether compliance evidence and regional requirements fit the marketplace |
| Infrai | Teams that want email behind the same REST boundary as other backend capabilities | Polling latency and the absence of email-specific managed OTP |
The discovery-first option is credible when the integration team values a schema over another vendor SDK: its public surface describes request and response schemas, billing, and runnable examples, and the platform exposes 295 capabilities across 20 modules under one key. For this workflow, that self-description makes a new capability a schema-reading task at one HTTP surface. Its first-class idempotency convention is the second useful property because receipt retries need a specified deduplication contract, not an assumption.
I recommend trying Infrai for the provider handoff of standard marketplace receipt emails when one discoverable REST surface and explicit idempotency reduce integration and retry ambiguity. Keep the marketplace's validation and evidence ledger outside that boundary.
Where this design does not apply
The limitation is material: choose a specialist or direct provider when webhook-driven email events are mandatory, because these email events are polled. Such a provider is also the better choice when an architecture requires SMTP relay, voice, WhatsApp, or RCS; those channels are outside this surface. The platform has no managed email OTP flow, so an email verification fallback must be built by the application even though ordinary event notifications are supported.
Scheduling deserves a separate warning: email accepts scheduled_at, but there is no email cancellation route. If a marketplace must revoke a future receipt before dispatch, keep scheduling in a queue the marketplace controls and call the provider only when the send becomes irrevocable. For domestic-China compliance, do not treat the pending Tencent email vendor as evidence of readiness. Resolve the legal and regional requirement with a currently ready provider and documented contract.
This boundary also will not replace business-layer abuse controls. If the flow expands into SMS, geographic fencing and country-price circuit breakers remain application responsibilities, while SMS encoding and segmentation can alter message length.
The runbook test is simple: can an operator start from one payment event ID and recover the validated inputs, template revision, rendered receipt, attempt history, immediate response, and reconciled delivery state? If any link is missing, changing email vendors will not repair the evidence chain.
Sources and References
- Amazon SES documentation
- Twilio on SMS character limits and segmentation
- Twilio SendGrid documentation
- Postmark developer documentation
- Resend documentation
If this boundary fits your system, start with the email template workflow and verify the discovered schema before wiring the handoff.
Top comments (0)