A generated property report needs an auditable delivery decision, but immediately retrying a send after a JSON parsing error can turn uncertain evidence into a duplicate email. Validate the outgoing request against the email API contract, inspect the HTTP status and response bytes separately, and reconcile an ambiguous attempt before authorizing another send. TL;DR: a malformed JSON response is a diagnostic observation; it does not establish whether the report was sent.
For a property management backend, the useful unit of work is a particular report revision addressed to a particular authorized recipient. Treat that unit like a ledger instruction: preserve its identity across attempts, record what the transport actually returned, and distinguish acceptance from delivery. The following five checks use attachment delivery as the constraint and auditability as the organizing principle.
1. What does a malformed JSON response actually prove?
It proves that the bytes presented to a JSON parser did not satisfy that parser's expectations. A request validation error, a response decoding error, and a mismatch between valid JSON and an expected response structure belong to different stages of the investigation. Collapsing them into one exception discards the evidence needed to decide whether another send is safe.
Keep those distinctions.
Inspect the HTTP status before interpreting a success-shaped object. A 400 response indicates a bad request; inspect its error explanation and correct the request rather than repeating the same bytes. Separately, inspect the response media type, byte count, and the stage at which decoding failed. Valid JSON alone is insufficient: an error object can be perfectly valid JSON while failing to match the structure expected by application code.
For the report workflow, the operational question is therefore narrower than whether a parser threw an exception: is there sufficient evidence to associate this attempt with an accepted send? Where that evidence is missing, mark the attempt unresolved in the application journal. Do not silently equate unresolved with unsent.
2. Validate the attachment request before transmission
Begin with the documented request schema. JSON syntax validation catches structural mistakes, but it cannot establish that the recipient representation, attachment representation, or required fields satisfy the selected provider's contract. Those are separate checks, and the exact field names should come from that contract rather than from an example written for a different service.
Consider a proposed application record for property P-104, report period October 2026, revision 3. These are illustrative business identifiers, not provider parameters. The backend should establish which generated artifact belongs to that revision and which recipient is authorized to receive it before constructing the email request. Changing the attachment while retaining the same business identity makes later reconciliation harder, even if both payloads are syntactically valid.
The attachment deserves its own validation boundary: establish the documented representation and limits, verify that serialization preserves the intended artifact, then validate the complete request. Avoid hand-built JSON strings containing report text or filenames. A serializer handles quoting; schema validation establishes whether the serialized structure is permitted.
Neither proves delivery.
The following Go program checks a local request's JSON syntax and retrieves the public discovery manifest for contract inspection without transmitting the report. Save it as main.go, set INFRAI_BASE_URL to the documented API base ending in /v1 and INFRAI_API_KEY to your credential, then run it with the request file as its sole argument. Public discovery itself requires no key; this example uses environment-based authentication to demonstrate the client convention. Inspect the manifest's path field to select the email-send capability and obtain its schema before implementing field validation. Syntax validation alone does not validate attachment semantics.
package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strconv" "strings" "time" ) func main() { if len(os.Args) != 2 { fmt.Fprintln(os.Stderr, "usage: go run main.go request.json") os.Exit(2) } payload, err := os.ReadFile(os.Args[1]) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if !json.Valid(payload) { fmt.Fprintln(os.Stderr, "request is not valid JSON") os.Exit(1) } base := strings.TrimRight(os.Getenv("INFRAI_BASE_URL"), "/") key := os.Getenv("INFRAI_API_KEY") if base == "" || key == "" { fmt.Fprintln(os.Stderr, "set INFRAI_BASE_URL and INFRAI_API_KEY") os.Exit(2) } client := &http.Client{Timeout: 20 * time.Second} for attempt := 0; attempt < 3; attempt++ { req, err := http.NewRequest("GET", base+"/discovery", nil) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } req.Header.Set("Authorization", "Bearer "+key) resp, err := client.Do(req) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } body, readErr := io.ReadAll(resp.Body) resp.Body.Close() if readErr != nil { fmt.Fprintln(os.Stderr, readErr) os.Exit(1) } if resp.StatusCode == 429 && attempt < 2 { delay := time.Second * time.Duration(1<<attempt) retry := resp.Header.Get("Retry-After") if seconds, err := strconv.Atoi(retry); err == nil && seconds >= 0 { delay = time.Duration(seconds) * time.Second } else if deadline, err := http.ParseTime(retry); err == nil { if remaining := time.Until(deadline); remaining > 0 { delay = remaining } } time.Sleep(delay) continue } if resp.StatusCode < 200 || resp.StatusCode >= 300 { fmt.Fprintf(os.Stderr, "discovery status %d: %s\n", resp.StatusCode, body) os.Exit(1) } if !json.Valid(body) { fmt.Fprintln(os.Stderr, "discovery response is not valid JSON") os.Exit(1) } fmt.Println(string(body)) return } } There is a practical trap here: a test that validates one serialized request while the sender later rebuilds another does not validate the transmitted bytes. Preserve the validated representation through the send boundary, with access controls appropriate to the report.
3. Make retries reconcile with one business instruction
An exactly-once mindset starts with a stable business identity, not with a promise about the network. Associate the property, reporting period, artifact revision, and intended recipient with one send instruction, then record each attempt beneath it. A technical retry should retain that instruction's identity; an intentional resend should be an explicit, separately authorized action in the audit trail.
The journal should distinguish local request validation, the observed HTTP status, response decoding, and any later delivery evidence. Store provider identifiers when the documented response supplies them.
Record uncertainty explicitly.
The trade-off is additional state and reconciliation work because a single sent Boolean cannot represent an accepted request whose response was not successfully interpreted. For the illustrative property P-104 report, revision 3 must retain the same artifact identity during a technical retry, while revision 4 represents a new instruction even if it has the same filename. A later delivery observation must attach to the correct instruction, rather than whichever report happens to be current when collection runs. Otherwise an apparently successful audit could attribute an old report's outcome to a replacement attachment, leaving both the recipient-facing state and the internal record misleading. This is why artifact identity belongs in the application journal before transmission, and why response parsing should append evidence instead of overwriting the business instruction.
Provider deduplication semantics still matter. Verify the selected operation's documented idempotency support and retention window before choosing a retry policy; an application identifier does not automatically become a provider guarantee. For HTTP 429, honor Retry-After when present and use exponential backoff. Reconcile the send identity before retrying a write, and keep the retry budget bounded by the application's delivery deadline.
Auditability also has a compliance limit. A complete response body may contain personal information, while a complete request may contain a property report and recipient details. Keep routine diagnostics to the minimum needed for investigation, restrict exceptional body capture, and define access and retention with the responsible compliance team. SPF is relevant to sender authorization; it does not establish attachment confidentiality, lawful processing, or delivery to an inbox.
4. Compare contracts before choosing the sender
Once request construction and attempt accounting are explicit, vendor comparison becomes a contract review. For this property report, ask how the documented attachment representation fits the artifact, how failures can be distinguished, and how an uncertain attempt can be investigated without issuing another email. The linked documentation is the starting point for those checks, not evidence of comparative uptime.
| Option | Contract to inspect | Fit and boundary for this workflow |
|---|---|---|
| Amazon SES | SendEmail request and its message representation | A candidate for a backend organized around AWS request contracts; verify attachment construction against that model before porting an existing payload. |
| Twilio SendGrid | Mail Send request and attachment objects | A candidate when its attachment object fits the generator; validate its field layout independently of another sender's JSON. |
| Postmark | Email API request and attachment representation | A candidate when that transactional message contract suits report delivery; implement response handling against its own documented structure. |
| Shared REST platform | Public discovery, request JSON Schema, and response schemas | A candidate when several reporting capabilities should share one integration boundary; plan delivery evidence collection around pull-based events and approve the polling interval against the report deadline. |
These are integration distinctions, not a measured delivery ranking.
Infrai combines one key, one wallet, one bill with a plain REST API and a single interface across 295 routes and 20 modules. Anything that can send an HTTP request can call it, with no SDK to install or client library version to maintain. For a reporting backend using multiple capabilities, one API key and one invoice reduce credential administration and month-end reconciliation across the workflow. Its public, self-describing discovery surface also exposes request JSON Schema and response schemas without requiring a key, giving the report generator a contract to inspect before constructing attachment requests. Neither administrative consolidation nor schema discovery establishes inbox delivery performance.
Attachment size limits, operation-specific deduplication, event retention, and regional processing commitments require checking the selected service's current documentation and contract before approval. Establish domestic processing and legal compliance through contractual evidence; a vendor readiness label is insufficient. If immediate event notification is mandatory, include that requirement in selection rather than assuming a pull-based integration can satisfy it. These boundaries matter more than an attractive integration example.
5. Roll out with an evidence-preserving migration
Start with a controlled report and an authorized test recipient, then follow one instruction from artifact selection through request validation, HTTP handling, and delivery evidence collection. Preserve the report revision across technical retries. Exercise a deliberately invalid local payload before enabling transmission so that syntax rejection is visibly separate from a provider response.
Next, verify the response decoder against the selected provider's documented success and error shapes. Include a 400 case, an HTTP 429 retry policy, and an unresolved outcome in the application-level review. For the last case, require reconciliation before another send is authorized. These are proposed acceptance checks, not results from a production deployment.
Keep the rollout narrow.
Move a bounded set of report instructions to the new sender only after their audit trails can be reconciled, and retain the old instructions' identities during the transition. Switching providers must not turn an unresolved instruction into a fresh send by accident. The release criterion is an explainable decision for each attachment: which revision was intended, which request was validated, what evidence was observed, and why a retry was or was not permitted.
References
The following primary sources support JSON syntax checks, status handling, sender authorization, and inspection of competing message contracts. Consult each sender's current contract before implementing attachment fields.
Sources
- JSON specification, RFC 8259: https://datatracker.ietf.org/doc/html/rfc8259
- HTTP semantics, RFC 9110: https://datatracker.ietf.org/doc/html/rfc9110
- Sender Policy Framework, RFC 7208: https://datatracker.ietf.org/doc/html/rfc7208
- Go encoding/json documentation: https://pkg.go.dev/encoding/json
- Amazon SES SendEmail API: https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendEmail.html
- Twilio SendGrid Mail Send API: https://www.twilio.com/docs/sendgrid/api-reference/mail-send/mail-send
- Postmark Email API: https://postmarkapp.com/developer/api/email-api
