Pushrail Docs
Open app
Guides · Sending events

Single-event ingest

Send one canonical event to the Pushrail ingestion endpoint.

Single-event ingest

The single-event endpoint is what you call from inside a request handler, a background job, or anywhere you produce one event at a time. It is the simplest entry point and the right default, reach for batch ingest only when you have a backlog or a heavy hot path.

The event shape

Every event carries the canonical envelope: eventType and payload (what happened), source (which subsystem emitted it), customerExternalId (which of your customers it concerns), and occurredAt (when it happened in your system's clock, not Pushrail's). Optional idempotencyKey, correlationId, and requestId enable deduplication and tracing.

See Concepts for the field-level meaning and the ingest endpoint reference for the full schema.

Example request

The same request shape, three ways:

curl -X POST https://api.pushrail.io/events \
  -H "Authorization: Bearer $PUSHRAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "eventType": "subscription.renewed",
    "occurredAt": "2026-05-16T12:00:00Z",
    "source": "billing-service",
    "customerExternalId": "cust_42",
    "payload": {
      "subscriptionId": "sub_abc",
      "planId": "pro_monthly",
      "amountCents": 2900
    }
  }'
import { Pushrail } from "@pushrail/sdk";

const pushrail = new Pushrail({ apiKey: process.env.PUSHRAIL_API_KEY! });

const result = await pushrail.events.emit({
  eventType: "subscription.renewed",
  source: "billing-service",
  customerExternalId: "cust_42",
  payload: {
    subscriptionId: "sub_abc",
    planId: "pro_monthly",
    amountCents: 2900,
  },
});

console.log(result.id, result.status); // evt_..., "accepted"
from pushrail import Pushrail

client = Pushrail(api_key=os.environ["PUSHRAIL_API_KEY"])

result = client.events.emit({
    "eventType": "subscription.renewed",
    "source": "billing-service",
    "customerExternalId": "cust_42",
    "payload": {
        "subscriptionId": "sub_abc",
        "planId": "pro_monthly",
        "amountCents": 2900,
    },
})

occurredAt defaults to now() when omitted by the SDKs; prefer to set it explicitly to the timestamp from your domain event rather than from the wall clock at the call site.

Acknowledgment

A successful ingest returns 202 Accepted with a JSON body { "id": "evt_...", "status": "accepted" | "duplicate" }. The 202 is intentional, Pushrail has durably persisted the event, but delivery to destinations happens asynchronously on the worker pool. The HTTP response is the moment ingestion is committed; it is not the moment any destination has received the event.

A status of duplicate means an event with the same idempotencyKey was already accepted within the dedup window. The response is otherwise identical, so dedup is transparent to your code. See Idempotency for the window semantics.

Validation

Every event is validated against the canonical envelope schema and, if you have published an event schema for the eventType, against that schema's payload definition. Validation failures return 400 Bad Request with a structured error body listing the offending fields.

Validation is the most common source of ingest errors in practice, a typo in eventType, a missing source, or a payload field that drifted from the schema. See Error handling for the full status-code matrix and the recommended retry behavior.