Implementing Headless Onboarding
This onboarding flow is only available to select white-label partners. Please reach out to your Stream representative to understand if you are eligible.
Overview
This guide explains how to integrate with the Stream Partner API to enable third-party ordering capabilities. The integration flow consists of several key steps:
- Authentication
- Organization Creation
- POS Integration
- Location Management
- DSP (Delivery Service Provider) Binding
- Menu Assignment (optional)
- Menu Publish (optional)
1. Authentication
The API uses OAuth 2.0 for authentication. You'll need to obtain credentials first. Endpoint
// Authentication Request
POST /oauth/token
{
"grant_type": "client_credentials",
"client_id": "YOUR_API_KEY",
"client_secret": "YOUR_API_SECRET",
"scopes": ["onboarding"]
}
// Response
{
"access_token": "...",
"token_type": "bearer",
"refresh_token": "...",
"expires_in": 3600,
"refresh_token_expires_in": 7200,
"scopes": ["onboarding"]
}
2. Organization Creation
First step is creating an organization for your merchant: Endpoint
POST /onboarding/org
{
"name": "Merchant Name",
"provider_id": "YOUR_INTERNAL_ID",
"user": {
"email": "merchant@example.com",
"first_name": "John",
"last_name": "Doe"
}
}
// Response
{
"org_id": "...",
"user_id": "..."
}
3. POS Integration Flow
3.1. Initialize Integration
Start the POS integration process: Endpoint
POST /onboarding/integrations
{
"org_id": "ORG_ID",
"source": "YOUR_POS_SYSTEM", // e.g., "toast"
"type": "pos",
"return_url": "YOUR_CALLBACK_URL"
}
// Response
{
"onboarding": {
"_id": "ONBOARDING_ID",
"entry_url": "URL_TO_START_OAUTH", // Redirect user here if provided
"status": "waiting_for_oauth"
}
}
3.2. Manual Integration
For POS systems that don't support OAuth or require manual integration: Endpoint
- When initializing the integration, if no
entry_urlis provided in the response, proceed with manual integration - Obtain the integration code from your POS system (if you are a POS, this can likely be inferred)
- Submit the code to complete the integration:
PUT /onboarding/integrations/{ONBOARDING_ID}
{
"code": "MANUAL_INTEGRATION_CODE"
}
// Response
{
"onboarding": {
"_id": "ONBOARDING_ID",
"status": "active",
"type": "pos",
// ... other onboarding details
}
}
3.3. OAuth Integration
For POS systems supporting OAuth: Endpoint
- Redirect the user to the
entry_urlprovided in the initialization response - Handle the OAuth callback at your
return_url - Exchange the OAuth code:
PUT /onboarding/integrations/{ONBOARDING_ID}
{
"code": "OAUTH_CODE"
}
Flow: createOnboardingIntegration returns an entry_url; once the merchant is
redirected there, the integration sits in waiting_for_oauth. To activate it,
capture the OAuth code from your return_url callback and call
connectOnboardingIntegration (PUT /onboarding/integrations/{id} with
{"code": "..."}), which exchanges the code and activates the integration.
4. Location Management
4.1. Fetch POS Locations
After integration, get available locations: Endpoint
GET /onboarding/locations?onboarding_id=ONBOARDING_ID
// Response
{
"locations": [
{
"provider_id": "POS_LOCATION_ID",
"name": "Location Name",
"address": {
"address_line_1": "123 Main St",
"postal_code": "12345",
"locality": "City",
"administrative_district_level_1": "State",
"country": "US"
},
"source": "pos_system_name"
}
]
}
4.2. Ingest Selected Locations
Import selected locations: Endpoint
This endpoint imports selected locations and their menu data from the POS system. The data stays synced via POS webhooks after initial import. You can re-sync anytime by calling this endpoint again.
POST /onboarding/locations/ingest
{
"onboarding_id": "ONBOARDING_ID",
"locations": [
{
"provider_id": "POS_LOCATION_ID"
}
]
}

5. DSP Integration
5.1. Start DSP Integration
Similar to POS integration but with type "dsp": Endpoint
POST /onboarding/integrations
{
"org_id": "ORG_ID",
"source": "DSP_NAME",
"type": "dsp",
"return_url": "YOUR_CALLBACK_URL"
}
5.2. Manual DSP Integration
Similar to manual POS integration: Endpoint
- Initialize the integration
- If no
entry_urlis provided, collect the DSP integration code - Submit the code using the same endpoint as OAuth integration
5.3. Bind DSP Locations
Connect POS locations with DSP locations: Endpoint
The binding process creates a link between locations that enables order flow. Getting this mapping wrong can result in misrouted orders.
Recommended UI flow:
- Display POS and DSP location details side-by-side for comparison
- Add confirmation step before saving
POST /onboarding/locations/bind
{
"onboarding_id": "DSP_ONBOARDING_ID",
"locations": [
{
"pos_location_id": "POS_LOCATION_ID",
"dsp_provider_id": "DSP_LOCATION_ID",
"dsp_store_name": "Store Name in DSP"
}
]
}

6. View Organization Locations
Get all locations for an organization:
This endpoint provides a way to view the current state of an organization and its locations. You can use it to verify active integrations and location statuses.
GET /onboarding/org/{ORG_ID}/locations
// Response
{
"locations": [
{
"_id": "LOCATION_ID",
"name": "Location Name",
"source": "pos_system_name",
"status": "active",
"address": {
// address details
},
"dsps": [
{
"_id": "DSP_CONNECTION_ID",
"name": "DSP Name",
"source": "dsp_name",
"status": "APPROVED"
}
]
}
]
}

7. Assign Menus to DSP Connections
By default, a catalog update publishes all of a location's menus to every DSP connection at that location. If a specific DSP connection should receive only specific menus, assign them explicitly. Endpoint
- Reference menus by the
provider_ids you supplied in your catalog. - Reference the DSP connection by its
DSP_CONNECTION_ID— thedsps[]._idreturned by View Organization Locations. - Each call is a full replace: it sets the complete list of menus for that connection. Send an empty
menu_provider_idsarray to remove the assignment and restore the default behaviour of publishing all menus.
On the next catalog update, the connection receives only its assigned menus. Connections with no assignment continue to receive all of the location's menus, so existing integrations are unaffected.
PUT /onboarding/menus/assignments
{
"org_id": "ORG_ID",
"dsp_location_id": "DSP_CONNECTION_ID",
"menu_provider_ids": ["MENU_PROVIDER_ID_1", "MENU_PROVIDER_ID_2"]
}
// Response
{
"dsp_location_id": "DSP_CONNECTION_ID",
"menu_provider_ids": ["MENU_PROVIDER_ID_1", "MENU_PROVIDER_ID_2"]
}
8. Publish Menus to a DSP Connection
Menus publish automatically on catalog updates, so you don't need this call for normal operation. Use it to push menus to a DSP now: to retry a publish that failed, to apply a new menu assignment straight away, or after fixing a location that a publish rejected. It queues the same full menu publish that Stream's own dashboard triggers. Endpoint
location_idis the Stream location ID:locations[]._idfrom View Organization Locations, not the POSprovider_id.sourceis the DSP'ssourcevalue, as indsps[].source.- The location must have an
APPROVEDorpendingconnection to that DSP. Otherwise the call returns404and nothing is published. - The publish respects the connection's menu assignment, if it has one.
POST /onboarding/menus/publish
{
"source": "doordash",
"location_id": "LOCATION_ID"
}
// Response
{
"status": "success",
"publish_outcome": "queued"
}
A success response means the publish was queued, not that the menu has reached the DSP. The publish runs in the background. publish_outcome tells you what the call did:
publish_outcome | Meaning |
|---|---|
queued | A new publish was queued. |
expedited | A publish was already scheduled to run later. It now runs immediately instead. |
already_queued | A publish is already waiting to run. No second publish was queued. |
| Absent | The DSP takes a retail inventory rather than a menu (for example Uber Eats Retail). An inventory publish was queued. |
The call returns 400 without publishing when:
| Response message | Cause | What to do |
|---|---|---|
Publish job for location already in progress | A publish for this location and DSP is running now. | Wait for it to finish, then call again if you still need to. |
Please wait for menu publish to complete before retrying | The previous publish to this DSP has not finished yet. | Wait, then call again. |
Cannot publish <source> without timezone / without phone_number | The DSP needs a field the location is missing. | Fix the location in the POS, re-ingest it, and call again. |
Cannot publish <source> without menu hours | None of the location's menus have valid hours. | Add menu hours in the POS, re-ingest, and call again. |
Don't call publish in a loop to check if it worked. Each call queues or expedites another publish, and a call while one is running returns 400. A success response cannot tell you whether the DSP accepted the menu. If a menu is still missing on the DSP after the publish had time to run, contact Stream support.
Integration Status Flow
- pending: Initial state for non-oAuth onboardings
- waiting_for_oauth: During OAuth flow
- active: Successfully connected
- deprovisioned: Integration has been disconnected
For OAuth-type integrations, status is waiting_for_oauth immediately after
creation (createOnboardingIntegration), until connectOnboardingIntegration
exchanges the OAuth code and moves it to active.
Go Deeper
The steps above get a location bound. These guides cover what happens after bind, which is where most headless integrations get stuck:
| Guide | Read it when |
|---|---|
| Flow at a Glance | You want the whole flow on one diagram, with every API call in order. |
| Activation by DSP | You need to know whether a DSP goes live at bind, after a DSP review, or only with Stream's help. |
| DSP Connection Statuses | You are reading status from the bind response or dsps[].status and need to know what each value means. |
| Tracking Activation | A bind returned pending and you need to know when it goes live. |
| Troubleshooting | A bind was rejected, a connection is stuck, or a menu is not reaching the DSP. |