Maintaining Type Safety with Our API
Introduction
Stream does not ship a client SDK, and that is a deliberate consequence of how our integrations work. You are not calling a Stream library from your application; you are building the interface. Stream sends requests to endpoints that you host, and your system sends webhooks to Stream. There is no single "Stream client" we could hand you, because half of the contract is code that lives in your codebase.
What we do publish is the contract itself: a complete OpenAPI specification for every integration type. That specification is machine-readable, and you can turn it into native types in whatever language your product is written in. Doing so moves an entire class of integration bug — a misspelled field, a missing required property, an enum value you did not know existed, a field whose type quietly changed — out of production order traffic and into your compiler or your test suite.
This page explains how to download the relevant specification and generate local types from it, and documents the specific quirks in our specs that you will need to work around.
Why This Matters More Without an SDK
Both Stream integrations are two-way, and type safety is worth applying in both directions. Taking the DSP integration as the example:
| Direction | Example operation | What generated types give you |
|---|---|---|
| Stream to you | POST /stream-dsp/v1/event | The exact shape of every event body you must be prepared to receive, so your handler cannot forget a case |
| You to Stream | POST /order/<PARTNER_ID> | A compile-time guarantee that the webhook you send is well-formed before it reaches us |
The inbound direction is the one partners most often underestimate. Our event endpoints accept a union of event bodies — the DSP event endpoint alone accepts nine distinct event types. Generated types make that union explicit in your code, so adding a tenth event type on our side becomes a compiler error in your handler rather than an event you silently drop.
The outbound direction protects you from the failure mode that is hardest to debug: a webhook that we reject, or worse, accept and interpret differently than you intended. Building the request from a generated type means the field names and shapes are correct by construction.
Downloading the Specifications
Every API reference on this site can hand you its own specification file. Open the reference for your integration and use the Download OpenAPI specification link at the top of the page:
| Integration | API reference |
|---|---|
| DSP | DSP API Reference |
| POS | POS API Reference |
| Events | Events API Reference |
| Partner API — API clients, merchants, and locations | Partner API Reference |
Save the file into your repository and point your generator at that local path. The
examples on this page assume you have saved it as openapi/stream-dsp.yaml. The
specifications are served as YAML; every generator below accepts YAML or JSON
interchangeably, so there is no need to convert it.
If you would rather receive a specification another way, or need one pinned to a specific environment, contact us and we will provide it.
Vendor the specification into your repository. Commit the downloaded file rather than fetching it during your build. These specifications are republished whenever our documentation deploys, and you want an integration change to arrive as a reviewable diff in a pull request, not as a surprise on your next CI run. Re-download deliberately, look at what changed, and regenerate.
All of the specifications are OpenAPI 3.0. See Known Quirks below for the handful of places where a strict generator needs help.
Generating Local Types
TypeScript
openapi-typescript emits a single
declaration file with no runtime dependency, which makes it a good fit for validating a
contract you implement rather than call:
npm install -D openapi-typescript typescript
npx openapi-typescript openapi/stream-dsp.yaml -o src/generated/stream-dsp.d.ts
Pull the request body types straight out of the generated operations map, so they stay
tied to the specific endpoint rather than being copied by hand:
import type { components, operations } from './generated/stream-dsp';
// Events Stream sends to the endpoint you host.
type InboundEvent =
operations['AppController_dspHandleEvent']['requestBody']['content']['application/json'];
// Events you send to Stream.
type OutboundEvent =
operations['WebhookController_streamHandleEvent']['requestBody']['content']['application/json'];
// Individual object schemas, when you need one directly.
type NewOrder = components['schemas']['NewOrder'];
Your handler signature then references the contract instead of restating it:
export async function handleStreamEvent(event: InboundEvent): Promise<void> {
// ...
}
If you would rather have a fetch client for the calls you make to Stream,
openapi-fetch pairs with the same generated
file and type-checks paths, bodies, and responses together.
Python
datamodel-code-generator turns
the schemas into Pydantic models, which gives you parsing and validation as well as type
hints:
pip install datamodel-code-generator
datamodel-codegen \
--input openapi/stream-dsp.yaml \
--input-file-type openapi \
--output-model-type pydantic_v2.BaseModel \
--output stream_dsp/models.py
This generator validates the input strictly and will currently fail on our specs until
you apply the fix described in Nested required flags.
Other Languages
openapi-generator covers Java, Kotlin, C#, Go, PHP,
Ruby, Rust, Swift, and many more:
npx @openapitools/openapi-generator-cli generate \
-i openapi/stream-dsp.yaml \
-g java \
-o ./stream-dsp-client \
--skip-validate-spec
Swap -g java for your target — run openapi-generator-cli list to see the full set.
The --skip-validate-spec flag is required today; see
Nested required flags for why, and prefer
removing the flag once you have sanitized the spec so that you still get the benefit of
validation.
Wiring Generation Into Your Build
Two habits make generated types genuinely useful rather than a one-time exercise:
Commit the generated code. Reviewers should see the type change in the same diff as the handler change. It also means a fresh checkout builds without running a generator.
Fail CI on drift. Regenerate and assert nothing moved, so a stale generated file cannot pass review:
npx openapi-typescript openapi/stream-dsp.yaml -o src/generated/stream-dsp.d.ts
git diff --exit-code src/generated/stream-dsp.d.ts
When that check fails after you refresh the specification, the diff is your changelog: it tells you exactly which fields moved before you deploy against them. Pair it with the integration changelogs for POS, DSP, and Events.
Known Quirks to Work Around
Our specifications are generated from our services and have a few rough edges. None of them prevent code generation, but each one will produce types that are weaker or stranger than you expect if you do not know about it. We are tracking all of these for cleanup.
Event Unions Are Not Discriminated
This is the most important one to understand. Our event endpoints accept a oneOf union,
and each member carries a type field identifying the event. However, type is declared
as a plain string with a default rather than as a fixed value:
"StreamWebhookNewOrderEvent": {
"type": "object",
"properties": {
"type": { "type": "string", "default": "new_order" }
}
}
Generators faithfully reproduce that as type: string. Because every member of the union
then has an identically typed type field, the union carries no information that a type
checker can use to tell the members apart — narrowing on event.type will not work, and
in TypeScript every member's fields appear simultaneously available.
The fix is to declare the literal values yourself, once, in a thin layer over the generated types. The values below are the ones our services send:
import type { components, operations } from './generated/stream-dsp';
type Schemas = components['schemas'];
// Events Stream sends to you, re-tagged so TypeScript can narrow them.
export type InboundEvent =
| (Schemas['DspLocationPauseEventDto'] & { type: 'location.paused' })
| (Schemas['DspLocationResumeEventDto'] & { type: 'location.resumed' })
| (Schemas['DspLocationBusyEventDto'] & { type: 'location.busy' })
| (Schemas['DspLocationMenuEventDto'] & { type: 'location.publish' })
| (Schemas['DspLocationMenuUpdateEventDto'] & { type: 'location.update' })
| (Schemas['DspLocationOrderAcceptedEventDto'] & { type: 'location.order.accept' })
| (Schemas['DspLocationOrderFailedEventDto'] & { type: 'location.order.fail' })
| (Schemas['DspLocationOrderUpdateEventDto'] & { type: 'location.order.notify_status_change' })
| (Schemas['DspLocationOrderCanceledEventDto'] & { type: 'location.order.cancel' });
Assert once at the boundary that the parsed body matches, and the rest of your code gets exhaustive, narrowable events:
function handle(event: InboundEvent) {
switch (event.type) {
case 'location.publish':
return ingestMenu(event); // narrowed to DspLocationMenuEventDto
case 'location.order.cancel':
return cancelOrder(event);
// ...
default: {
// Compiler error if Stream adds an event type you have not handled.
const unhandled: never = event;
throw new Error(`Unhandled event type: ${JSON.stringify(unhandled)}`);
}
}
}
That never check is the payoff: when we add an event type and you regenerate, your build
tells you where to handle it. Apply the same pattern to the outbound union, whose type
values are new_order, cancel_order, order_adjustment, store_status_update,
delivery_status_update, and menu_refresh.
Confirm the current set of events and their payloads against Handling Events and the rendered reference for your integration.
Nested required Flags Are Not Honored
A small number of nested objects in our specs mark their properties required using a
boolean required on the property itself. That is Swagger 2.0 syntax and is not valid in
OpenAPI 3.0, where required must be an array of property names on the parent object.
Generators split into two camps on this, and both outcomes are a problem:
- Strict generators reject the spec.
openapi-generator validatereportsattribute components.schemas.DeliveryDriverDetails.required is not of type 'array', anddatamodel-codegenexits with a validation error. - Lenient generators ignore the flag.
openapi-typescriptaccepts the spec but emits those properties as optional. For example,DeliveryDriverDetails.phone.numberis marked required in the spec and generates asnumber?: string.
The affected schemas are DeliveryDriverDetails (DSP and POS), Item.size_per_pack_item
(POS), and GenericEventProviderAssignDriverObject.driver (Events). Until we correct
this, either strip the offending keyword as a pre-processing step in your generation
script, or rewrite it into the array form your generator expects. The following removes
it, which unblocks strict generators:
# PyYAML is installed alongside datamodel-code-generator.
# yaml.safe_load also parses JSON, so this works on either format.
import yaml
SPEC_IN = "openapi/stream-dsp.yaml"
SPEC_OUT = "openapi/stream-dsp.sanitized.yaml"
def strip_boolean_required(node):
if isinstance(node, dict):
if isinstance(node.get("required"), bool) and (
"properties" in node or "type" in node or "$ref" in node
):
node.pop("required")
for value in node.values():
strip_boolean_required(value)
elif isinstance(node, list):
for value in node:
strip_boolean_required(value)
with open(SPEC_IN) as handle:
spec = yaml.safe_load(handle)
strip_boolean_required(spec.get("components", {}))
with open(SPEC_OUT, "w") as handle:
yaml.safe_dump(spec, handle, sort_keys=False)
Because the flag is dropped either way, treat the generated optionality of these nested fields as unreliable and check the rendered reference for what is actually always present.
No servers or Security Schemes
The specifications intentionally omit a top-level servers block, because the base URL
for the endpoints you host is yours, and the Stream base URL differs by environment.
Generated clients will therefore have no default base URL and you must supply one. See
Integration Sandbox for environment
endpoints.
Authentication is likewise described as ordinary header parameters rather than as OpenAPI security schemes, so generated clients will not build auth for you. Pass the headers explicitly:
Authorization: Bearer <TOKEN>for OAuth-authenticated calls — see Requesting a token.Stream-Webhook-Signaturefor webhooks you send to Stream — see Setting Up HMAC for Secure Webhooks.
Path Placeholders and Operation Names
Two cosmetic details worth knowing before you read generated code:
The DSP outbound webhook path is written /order/<PARTNER_ID>. Those angle brackets are a
documentation placeholder, not an OpenAPI path parameter, so generators treat the segment
as a literal string. Substitute your partner identifier when you build the URL rather than
expecting a generated parameter for it. The POS and Events equivalents use proper
templating — /pos/{partner_slug}/webhook and
/event-provider/{partner_slug}/webhook — and do generate a parameter.
Operation identifiers carry the internal controller name they were generated from, so you
will see AppController_dspHandleEvent and WebhookController_streamHandleEvent rather
than friendlier names, and generated method names follow suit. Most generators let you
override these; openapi-generator accepts an --operation-id-name-mappings flag if you
want to rename them at the boundary.
Types Are Not a Substitute for Runtime Validation
Generated types are a compile-time construct, and every event you receive arrives over the network from outside your process. A type annotation does not check anything at runtime. At your webhook boundary, do both:
- Verify the HMAC signature first, before parsing the body, as described in Setting Up HMAC for Secure Webhooks. Signature verification runs against the exact raw bytes, so do this before any JSON parsing or reserialization.
- Then validate the parsed payload against the schema. Generating runtime validators
from the same specification keeps the check honest, because it cannot drift from the
types your handler is written against. Pydantic models from the Python recipe above do
this natively; in TypeScript, generators such as
typeconvcan emit JSON Schema or Zod validators from the same source of truth.
Validating on the way in means an unexpected payload becomes a clear, logged rejection at
the edge of your system instead of an undefined that surfaces three functions later, in
the middle of an order.
Conclusion
Because you build the interface rather than call a library, the OpenAPI specification is the closest thing to a shared source of truth between your system and ours. Generating types from it costs one command in your build and turns contract drift into a build failure instead of a production incident.
If you have questions about the specifications, hit one of the quirks above in a way this page does not cover, or would like to discuss your code generation setup, please reach out to us at partners@streamorders.com. We appreciate your interest in building high quality solutions for your customers.