Overview
WHAWIT learns about your deliveries from outcome events — small JSON records you emit as a unit of work starts, moves through its steps, and resolves. You can emit them two ways. Both speak the same contract, and you can mix them:- The Node SDK —
trackDelivery()from@usewhawit/collector-node. Batching, retry, tenant exclusion and OpenTelemetry trace linking are handled for you. - The HTTP endpoint —
POST /ingest/events/v1/outcomes. Any language, any runtime.
The
v1 contract is frozen. New optional fields may be added; existing field names will
not be renamed or removed. Any breaking change ships as a separate v2 route, so
instrumentation written today keeps working.The model in one paragraph
Every event carries adeliveryKey — your own identifier for the unit of work, unique within your project. All events sharing a key describe one delivery. WHAWIT assembles them into a single record: the steps it went through, when it resolved, whether it made its deadline, and what failed if anything did.
Guarantees you can build on
These are the properties that matter when you write the instrumentation:Events may arrive out of order, and more than once
Events may arrive out of order, and more than once
Assembly is idempotent and order-independent. A retried HTTP call, a duplicated queue
message, or a
delivery.resolved that beats its own step.completed all converge to the
same delivery. You do not need to order or de-duplicate on your side.Step names are yours
Step names are yours
step.name is a free string. WHAWIT has no built-in pipeline and no list of valid steps.
Whatever you send is what the delivery is made of.`tenant` is optional
`tenant` is optional
If your product is single-tenant, omit it. Inventing a placeholder value only creates a
meaningless dimension in every breakdown.
Telemetry never breaks your pipeline
Telemetry never breaks your pipeline
The SDK never throws into your code. If tracking is unconfigured, the tenant is excluded,
or the network is down after retries, you get a handle that does nothing and your delivery
proceeds untouched. Events lost after retries are counted and readable via
getDroppedEventCount().Ingestion answers 202, not 200
Ingestion answers 202, not 200
Events are queued for asynchronous assembly. A 202 means accepted, not yet processed.
Option 1 — the Node SDK
delivery.fail(error) accepts anything — an Error, a string, or a plain object. The SDK extracts the message, the type and a status code where it can find one.
Configuration
trackDelivery() configures itself from initWhawIt() or from WHAWIT_API_KEY / WHAWIT_PROJECT_ID. Call initDeliveryTracking() when you need options that have no equivalent there — above all, tenant exclusion.
string[] | (tenant, tenantKind) => boolean
Glob patterns matched against the tenant name, or a predicate. Matching deliveries never
leave your process — no event is buffered and nothing is sent.
'production' | 'test' | 'internal'
default:"production"
Applied when
trackDelivery() does not name one.boolean
default:"false"
Emit an event when a step starts, not only when it completes. Roughly triples event
volume for a three-step delivery, so enable it only if you need the in-flight view.
string
Falls back to the initialised SDK, then
WHAWIT_API_KEY.string
Falls back to the initialised SDK, then
WHAWIT_PROJECT_ID.string
Base URL of your WHAWIT ingestion API. Set this explicitly to point at your own instance.
BatchConfig
Batch size, flush cadence and retry behaviour. Shares the collector’s defaults.
Handle reference
DeliveryStepHandle
Opens a step.
options.service names the service that owns it, which is what makes
cross-team attribution work. options.attempt is a 0-based retry counter. The handle
exposes succeed(), fail(error?) and skip().DeliveryHandle
Sets or revises the deadline before the delivery resolves.
void
Resolves the delivery. A delivery resolves exactly once; later calls are ignored.
boolean
True when this delivery was filtered at source and nothing will be sent.
traceId and spanId of the work it describes and links straight back to your existing traces.
Option 2 — the HTTP endpoint
The body is a batch of up to 1000 events:
Responses
object
accepted is what was queued; filtered is what was dropped at the edge because it was
tagged test or internal.Invalid payload, or the
x-whawit-project-id header is missing.Missing or invalid API key, or the key has no access to that project.
Event types
event
The unit of work began. Carries the identity fields and, ideally,
expectedBy.event
A step began. Optional — send it only if you want the in-flight view.
event
A step finished, in any of its states:
succeeded, failed or skipped.event
Terminal. Requires
terminalState (succeeded or failed). This is the event that
computes on-time and closes the delivery.Field reference
Every field of an outcome event.Required
string
required
Your correlation key for this unit of work — an order id, a payroll run id, a campaign id.
Unique per project. Every event for the delivery carries it. Max 400 characters.
string
required
One of
delivery.started, step.started, step.completed, delivery.resolved.string
required
ISO-8601 timestamp of the event itself.
Identity
string
Which of your end customers this delivery belongs to. Optional — omit it in a
single-tenant product. Max 200 characters.
string
default:"production"
production, test or internal. Anything not production is dropped at ingestion and
never reaches a KPI.string
Your identifier for the workflow that produced this delivery. Max 200 characters.
string
Your own category for this kind of work —
order, payroll_run, campaign. Drives the
per-type breakdowns, the expected steps and the default deadline. Max 120 characters.string
When one workflow resolves into several deliveries, which one this is — an order line, a
payroll batch, a journey send-node. Max 200 characters.
Steps
object
Present on
step.started and step.completed.Outcome and timeliness
string
Required on
delivery.resolved. succeeded or failed. A delivery is failed if any step
failed.string
The deadline, ISO-8601. Falls back to the configured default for this
workflowType when
absent. Without either, the delivery is not tracked for timeliness.boolean
Send it explicitly to override; otherwise derived from
expectedBy.string
Name of the first step that failed. Max 120 characters.
Failure detail
string
transient, permanent or unknown. Omit and WHAWIT classifies it; send it and your
value wins.string
platform, customer, upstream or unknown — whose fault it was. Omit and WHAWIT
attributes it; send it and your value wins.object
Extras
object
Free-form dimensions carried onto the delivery — a carrier name, a destination, a region.
string
default:"v1"
The contract version this event was written against.
Worked examples
filtered, and it never reaches a delivery record or a KPI.
Keeping internal traffic out
This matters more than it looks. Load tests, staging pipelines and QA runs share your production code path, so without a rule they land in the same reliability number your customers are judged by — inflating it on a good day, wrecking it on a bad one. There are two lines of defence, and you want both:1
Exclude at source
Configure
excludeTenants in the SDK. Matching deliveries never leave your process:
nothing is buffered, nothing is sent, no bandwidth is spent.2
Tag what remains
Send
tenantKind: "test" or "internal" on anything you cannot filter by name. WHAWIT
drops it at the edge and counts it under filtered in the response.tenantKind is test or internal are always excluded, whether or not you configured excludeTenants.

