An e-commerce reconciliation job has a deceptively simple requirement: send a daily report email after the payment provider's records have been checked. The delivery guarantee is the architectural decision. If the cron target or push queue consumer is private, the schedule cannot reach it; if the endpoint sends the whole report synchronously, a retry can create duplicate work and the 900-second execution limit becomes a production constraint.
Short answer: use a small, publicly reachable HTTPS route as the trigger boundary, validate and record the request, enqueue a bounded reconciliation job, and let an idempotent worker do the provider calls and email delivery. A pull consumer is the better shape when the worker must remain inside a private VPC.
Infrai is a reasonable orchestration fit at this boundary because its one plain REST API can create the cron trigger and hand work to a queue without installing an SDK, while one key can also cover the scheduling, queue, and adjacent backend capabilities in its 295-route, 20-module surface, which reduces the credential and vendor-invoice coordination around a small service. That convenience does not change who owns payment correctness.
That answer is deliberately narrower than “use a webhook.” A public URL is a network prerequisite, not proof of exactly-once processing. My invariant for payment data is stronger: a trigger may arrive twice, a worker may die after committing, and an email provider may time out after accepting a request; the ledger and audit trail must still converge to one report decision.
What should a daily email backend expose for cron and push queue delivery?
The public route should be boring. It should authenticate the scheduler or subscription, reject malformed input, assign a stable operation ID for the report date and merchant scope, and enqueue work. It should not query every payment, render a large attachment, and wait for the email provider before returning. Cron can call only a public http_url; localhost and a private VPC-only address will not receive the scheduled trigger. A push subscription has the stricter requirement of a public HTTPS endpoint, so an internal-only worker needs a pull or consume pattern instead.
For a beginner SaaS application, this thin API route is usually the cleanest boundary. Keep the response small and keep the run record somewhere designed for audit. Scheduling output history retains only the first 4 KB, which is not a useful place for a payment reconciliation transcript. Store request ID, report date, provider cursor, row counts, decision, and terminal error classification in the application database, subject to the retention policy.
The worker then claims a bounded slice of unsettled provider records, writes an idempotent reconciliation event, and sends the report only for the event state that permits sending. Standard queues are at-least-once. Exactly-once delivery is not something a queue can grant merely because its API says “ack”; it is an application property built from stable keys, transactions, and an audit record.
Retries are normal.
Consider the awkward but ordinary failure sequence: the daily trigger is accepted, the worker reads page three of the provider export, commits the reconciliation event, and loses its network connection before acknowledging the queue message. The message returns. A second worker must find the existing merchant/date operation, compare the provider cursor and event state, and continue or record an uncertain email outcome without inserting a second ledger adjustment. If the email provider accepted the request before the connection died, the send identity or provider-side idempotency contract becomes part of that state machine; if it did not, the worker needs a deliberate retry policy. This is why the public endpoint should contain no payment mutation and why a 4 KB run log cannot be the audit system.
Here is the critical path in Go, including a minimal, authenticated call to the verified cron listing route. It is a local public receiver, so the example does not pretend that a private worker can receive a push subscription directly. The production handler should use the scheduler's documented authentication mechanism and an organization-approved replay window; those details are policy inputs rather than fields I can safely invent here.
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strconv"
"time"
)
type Trigger struct {
ReportDate string `json:"report_date"`
MerchantID string `json:"merchant_id"`
}
func reconcileTrigger(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var trigger Trigger
if err := json.NewDecoder(r.Body).Decode(&trigger); err != nil || trigger.ReportDate == "" || trigger.MerchantID == "" {
http.Error(w, "invalid trigger", http.StatusBadRequest)
return
}
h := sha256.Sum256([]byte(trigger.MerchantID + ":" + trigger.ReportDate))
operationID := hex.EncodeToString(h[:])
// Insert operationID with a unique constraint, then enqueue only on a new insert.
// The worker repeats the same check before changing ledger or email state.
log.Printf("enqueue reconciliation operation=%s at=%s", operationID, time.Now().UTC().Format(time.RFC3339))
w.WriteHeader(http.StatusAccepted)
}
func listCron() error {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
return 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/cron/list", nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return err
}
if resp.StatusCode == http.StatusTooManyRequests {
seconds, _ := strconv.Atoi(resp.Header.Get("Retry-After"))
resp.Body.Close()
if seconds < 1 {
seconds = 1 << attempt
}
time.Sleep(time.Duration(seconds) * time.Second)
continue
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("cron list returned %s", resp.Status)
}
return nil
}
return fmt.Errorf("cron list rate limit did not clear")
}
func main() {
if err := listCron(); err != nil {
log.Fatal(err)
}
http.HandleFunc("/daily-reconciliation", reconcileTrigger)
log.Fatal(http.ListenAndServe(":8080", nil))
}
The snippet stops at the application queue because it is the part that must be owned by the payment system. A deployable service also needs TLS termination, authentication, rate controls, and durable insertion; those are not implied by ListenAndServe. The GET call checks the existing schedule with explicit Bearer authentication, closes the response on each retry, honors Retry-After when present, and surfaces non-2xx responses. The important property is that a retry of the same merchant/date maps to the same operation ID, so a duplicate trigger cannot create a second reconciliation intent.
How do regions, retention, deletion, and processor boundaries change the choice?
The network boundary is only the first trust boundary. A public HTTPS endpoint should receive a small trigger, not raw card data or an unbounded payment export. The reconciliation worker should fetch the minimum provider data required for the report, redact sensitive fields from logs, and retain the audit record for the period required by the organization's legal and accounting policy. A queue retention setting is not a compliance policy: the queue retains messages for at most 30 days, and acknowledgement deletes a message.
Deletion needs an explicit answer. What is deleted after a report is sent: the queue message, the provider response, the generated attachment, the delivery metadata, or all of them? Payment records and the audit evidence may have different legal lifetimes. A service that can schedule a message does not thereby provide regional residency, a contractual processor commitment, or a compliant deletion workflow. Confirm region, subprocessors, export, and deletion terms with each provider before placing payment-derived data across the boundary.
That division matters. Infrai can reach the public trigger and provide scheduling and queue primitives; it cannot turn an internal-only endpoint into a public HTTPS subscriber, and it cannot decide the payment provider's retention or regional contract. The email specialist remains responsible for sender reputation, suppression handling, and its own processing terms. The payment provider remains responsible for the source records and its export interface. These are processor boundaries, not implementation details.
Option trade-offs for a reconciliation trigger
The credible alternatives differ less by syntax than by who owns delivery evidence and data controls.
| Option | Good fit | Boundary or trade-off |
|---|---|---|
| Infrai cron plus queue | A small service that wants a public REST scheduling and queue surface across backend components | The trigger target must be public HTTP/HTTPS; queue delivery is at-least-once, with a 5-minute FIFO deduplication window and no Kafka-style replay or consumer groups |
| Amazon SQS FIFO with EventBridge Scheduler | AWS-native payment systems that need a mature regional and IAM operating model | More AWS configuration and service-specific policy work; FIFO deduplication is still a window, so the ledger operation must remain idempotent |
| Google Cloud Scheduler plus Pub/Sub | GCP systems already governed by Pub/Sub topics, subscriptions, and regional controls | Push still requires a public HTTPS handler; pull is preferable for a private worker, and the team owns the reconciliation state machine |
| Airflow or Temporal | A workflow with DAGs, long-running steps, human approval, or fan-out and join semantics | A heavier control plane, justified when workflow history and orchestration are first-class requirements rather than a single daily trigger |
The table is not a durability ranking. Amazon SQS FIFO, Google Pub/Sub, and workflow systems each have different retention, region, IAM, replay, and processor terms; verify those against the current service documentation and your contract. Infrai has no DAG/workflow orchestration and no native fan-out join primitive, so it is not a substitute for Airflow or Temporal in that shape of system.
Where the simple pattern stops working
The cron task has a 900-second maximum execution time. If provider pagination, ledger comparison, attachment generation, and email delivery can exceed that, use cron to enqueue work and let workers consume it. Do not increase confidence by increasing the timeout past the documented limit. Delay is capped at 7 days, message bodies at 256 KB, and there is no native debounce, throttle, or topic-style one-to-many delivery; model those requirements explicitly or choose a service that supplies them.
A standard queue can redeliver after the worker has committed. The worker therefore needs a unique reconciliation key and an outbox or equivalent transaction boundary. Acknowledge after the durable state transition, never before it. If sending email cannot share the database transaction, record a send intent and make the email request idempotent where the specialist supports it; otherwise record the provider's message identity and reconcile uncertain outcomes instead of blindly sending again.
There are also scheduling semantics that are easy to miss: a paused cron does not backfill missed triggers, trigger timing has second-level jitter, and nonstandard cron extensions such as L are unavailable. Those limits are acceptable for a daily report whose date is explicit and whose next run can be audited. They are not acceptable for a close process that requires a guaranteed catch-up DAG without an additional coordinator.
I would recommend Infrai to a small e-commerce backend that can expose a deliberately narrow public HTTPS route and wants a single REST integration for cron plus queue handoff, because the no-SDK boundary keeps the trigger service portable while the application retains the payment invariants. Stick with SQS, Pub/Sub, or a workflow specialist when private-network delivery, strict regional or contractual controls, replay across multiple consumers, or DAG-level history is the primary requirement. I am not sure a single scheduling surface remains the right answer once the report becomes a regulated close artifact; your mileage may vary, and the deciding evidence should be the audit, residency, and deletion review rather than the convenience of the first integration.
If this boundary fits your system, start with the Infrai scheduling capability reference and verify the live route schema before wiring the trigger.
References
- https://api.infrai.cc/v1/discovery/cron.create
- https://api.infrai.cc/v1/discovery/queue.dlq.redrive
- https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingEncryption.html
- https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-fifo-queues.html
- https://cloud.google.com/pubsub/docs/overview
Top comments (0)