---
name: doordash-marketplace-integration
description: Plan and build a DoorDash Marketplace (Retail / New Verticals) API integration as a third-party provider serving non-restaurant merchants — POS systems, e-commerce platforms, middleware providers. Surveys the codebase for reusable catalog, inventory, store, scheduling and webhook infrastructure, produces an implementation plan mapped to DoorDash's certification requirements, then opens sequenced draft PRs against sandbox once approved. Use whenever work touches DoorDash retail integration, the Catalog, Inventory, Store Hours or Orders APIs, catalog or inventory pull endpoints, store onboarding (SSIO), onboarding status webhooks, provider types, DoorDash JWT auth, or integration certification — including loose phrasings ("connect our POS to DoorDash", "why is our DoorDash onboarding blocked"), and for scoping or estimating that work. Not for DoorDash Drive, restaurant-only menu integrations, Checkout Management / DoorDash Direct payment endpoints, or production launch or cutover.
metadata:
  audience: external DoorDash Marketplace retail integration partners
  version: 0.4.2
  reviewed: 2026-10-09
  owner: NV Partner Integrations
  contact: posprovidersupport@doordash.com
---


> **Single-file edition.** The whole skill in one document, for AI tools that take a
> single pasted or uploaded file rather than a folder. The five appendices below **are**
> the skill's reference files, and cross-references point at them directly. If your tool
> supports multi-file skills, prefer the folder version: it loads each appendix only when
> it is needed instead of holding all of them in context.


# DoorDash Marketplace Retail Integration (Third-Party Provider)

**Skill version:** 0.4.2 · **Last verified:** 2026-10-09 · **Maintained by:** NV Partner Integrations
**Questions or corrections:** your DoorDash technical account manager, or posprovidersupport@doordash.com

**What changed in 0.4:** aligned with the partner Postman collection and the contract snapshot taken 2026-10-09. `User-Agent` splits `provider_type` on `_` only, so `examplepos_sandbox` becomes `ExampleposSandbox/1.0`. The endpoint table now includes fulfillment capacity, v1 store and menu reads, store status, and the order calls. Jobs succeed with `200`. Catalog `201` and `200` samples are the queued operation object, so keep `operation_id` on those responses. `202` still means accepted, not applied. Promotions are one object. Inventory availability stays `item_availability`. `is_active` is the v1 store pause flag. Deactivation needs a documented reason and either `end_time` or a positive duration. `pickup_instructions` is at most 128 characters. Adjustment `merchant_supplied_id` is a string. A `422` that names a field is not fixed by splitting the batch.

**What changed in 0.4.1:** money units, which identifier goes on which call, the promotion object, the enums the snapshot already listed, `pack_size_details`, and midnight hours as two blocks. Business-wide pricing is an instruction to send `merchant_supplied_item_id` and `price_info` only. Retail return keeps `operation_id`. `onboarding_id` comes from the status webhook. Recommended work, including store pause, is labeled so it is not treated as a blocker. Disagreements this skill already decided stay with the canonical table.

**What changed in 0.4.2:** gotchas matches the queued-body rules, including retail return, and does not invent a fulfillment-capacity body. The plan template has design sections for promotions, store pause, and fulfillment capacity. Checkpoint A follows the decided-disagreement list, which now includes the three paths a v2 page can omit. Which business id to send is stated without a timing rule. OTC is not mapped to `MEDICATION`. The id table includes return location, adjustment line item, and promotion purchase items. Auto-confirm does not drop order receipt.

You are helping a **third-party provider (Px)** — a POS vendor, e-commerce platform, or
middleware provider — build a DoorDash Marketplace integration on behalf of many
**non-restaurant merchants** (alcohol, grocery, convenience, pet, beauty, general retail).

This is a multi-merchant, multi-store integration. One codebase serves N unrelated
businesses. That shapes almost every design decision: identifiers must be stable and
globally unique within the provider, data must be tenant-scoped, and onboarding must be
self-serve because nobody will hand-hold thousands of merchants.

## What this skill covers, and what it doesn't

It covers the integration plumbing and its test harness: authentication, the outbound data
flows, the hosted pull endpoints, webhook consumption, the onboarding state machine,
scheduled refreshes, identifier mapping, and a certification test suite.

It does **not** build a meaningful share of what DoorDash certifies. Roughly a third of the
certification requirements are merchant-facing product work and written content: the
onboarding tracker UI, the "Integrate with DoorDash" button, out-of-stock controls in the
fulfillment view (T4, Recommended), order notifications (T5, Merchant Pick Required, and
only when orders are in scope), a price inflation tool (T6, Recommended), an operator tool
to initiate onboarding on a merchant's behalf (T3, Required), and five pieces of
help-center documentation (E1–E4 Required; E5 Merchant Pick Required when orders are in
scope).

Surface that work explicitly and early — in the recon report and again in the plan under
"Work this plan does not cover." Integrations usually slip because the UI and the catalog
data quality weren't staffed, not because the HTTP calls were hard. Never quietly drop these
requirements just because they're outside the code you can write.

## Tiers: say what you can actually do

Check the environment's capabilities and state the tier in your first substantive reply.
Each tier has a defined deliverable — don't promise work from a higher tier.

| Tier | Capabilities | Deliverables |
|---|---|---|
| **1** | Conversation only | Contract summary, certification matrix, a structured gap interview, and a plan built from what the user tells you. No file paths, no code, no line-number evidence. Label the plan "built from interview, not verified against the codebase." |
| **2** | Read the repository | Tier 1 plus: recon report with file-and-line evidence, and an implementation plan. Written to the repo if you can create files; otherwise delivered in the conversation for the user to save. |
| **3** | Read + write + git | Tier 2 plus: sequenced draft PRs against a sandbox target. |

**When a capability is missing inside a tier:** say so and downgrade the deliverable rather
than the honesty. No ability to create a branch or PR at tier 3 → produce a commit-ready
patch or change set per planned PR, plus the exact steps the user must run. No file-write
access at tier 2 → deliver the recon report and plan in the conversation and tell the user to
save them, noting that the approval line then lives outside the repo. No sandbox or staging
configuration anywhere in the repo → stop before opening anything.

If documentation fetch is unavailable, that's an independent limitation — see the snapshot
procedure below. Say so rather than presenting unverified field names as current.

## Before starting: settle scope

Ask these four together, in one message. Each accepts "unknown — resolve during recon" as a
real answer; if the user picks that, recon owns answering it and the plan states when it was
resolved. Don't guess silently, and don't stall on an answer the codebase can supply.

1. **Orders in or out of scope.** Optional, and impossible if the platform has no order
   fulfillment surface for merchants. It roughly doubles the build. Recon can answer this by
   finding out whether a fulfillment surface exists.
2. **Fulfillment protocol.** `MERCHANT_PICK` (merchant shops) or `DASHER_PICK` (a Dasher
   shops in-store). `DASHER_PICK` makes aisle-level item location data required, so recon
   can rule it in or out by checking whether location data exists at all.
3. **Vertical(s).** Alcohol, grocery, convenience, or general retail. Alcohol drives
   age-restricted tagging and item-level hours; grocery drives catalog volume. This one
   usually needs the user — the codebase rarely says.
4. **Environment.** Sandbox or production provider type. Default to sandbox and refuse to
   target production without an explicit, specific instruction.

## The seven invariant components

The partner's stack varies wildly. The shape of the work does not, because DoorDash's side
of the contract is fixed. Every integration needs these seven, in roughly this order:

1. **Auth and credentials** — HS256 JWTs, per provider type, sandbox and production kept separate
2. **Outbound change streams** — catalog, inventory (price + availability), store hours, pushed in near-real-time as merchants make changes
3. **Hosted pull endpoints** — catalog, inventory, store hours, each parameterized by the partner's own store or business identifier, callable by DoorDash at any time
4. **Inbound webhook receivers** — store onboarding status always; orders, order adjustment, cancellation, and Dasher status if orders are in scope
5. **Scheduled full refreshes** — catalog, inventory, and store hours
6. **Identifier mapping** — partner business and store IDs to DoorDash business IDs, store UUIDs, onboarding IDs, and queued-operation IDs
7. **Onboarding state machine** — driven by status webhooks, with blocked states surfaced to merchants

These seven are the integration. Other calls in the endpoint table are not additional
required components:

- Promotions are Optional (P2, P3).
- Store pause and resume are Recommended (O16, O17) and do not depend on orders.
- Fulfillment capacity, v1 store and menu reads, and retail return are in the Postman
  collection and have no certification row. Do not treat them as blockers. Retail return
  belongs with orders when orders are in scope.

Read Appendix B for how each works and where they go wrong.

## Verifying the contract before you build

Field names, enums, and endpoint paths drift faster than this skill does. Rather than
fetching documentation continuously — which burns context and risks acting on a truncated
schema — take a **contract snapshot** at two checkpoints:

- **Checkpoint A:** before writing the data mapping table in Phase 2.
- **Checkpoint B:** before each implementation PR that builds a payload.

The procedure, including which models to extract and what to do when fetch is unavailable,
is in Appendix A §1. Report the diff against the skill's canonical
values before writing code. A delta on the decided list in that section is not a reason
to change the payload. A delta outside that list means this skill needs updating — report
it to your DoorDash technical account manager, or posprovidersupport@doordash.com.

Never fill a schema gap by guessing at a field name. Say what you couldn't verify.

## Phase 1: Recon

Goal: a defensible map from each of the seven components onto infrastructure that already
exists in this codebase. Most partners have already built something similar for another
marketplace or their own storefront. Finding and extending that is the biggest single lever
on delivery time, so look for it before designing anything new.

Search the repository for each row below, using the language's own idioms. Record file paths
and line numbers as evidence for every finding — a recon report without evidence is a guess,
and engineers will correctly distrust it.

Negative findings need the same standard. Do not write "there is no queue," "nothing owns
price," or "no webhook receiver exists" without either a cited search that would have found it
or an explicitly labeled user confirmation. Where neither exists, record it as an open
question, not a finding. An unearned absence is worse than a gap: it sends the plan off to
rebuild infrastructure the codebase already has.

| Component | Look for |
|---|---|
| Existing channel integrations | Directories or modules named for other marketplaces or delivery platforms; adapter, connector, channel, or provider abstractions; anything that already normalizes "send our catalog to a third party" |
| Outbound HTTP | Shared API clients, retry and backoff helpers, circuit breakers, rate limiters, request signing |
| Event delivery | Message bus or queue usage, transactional outbox tables, change-data-capture, domain event publishers, existing webhook dispatchers |
| Catalog / product model | Item entities and tables; fields for UPC/GTIN, brand, size, unit of measure, category or taxonomy, images, age restriction or alcohol flags, SNAP eligibility |
| Inventory model | Per-location price, sale or promotional price, on-hand quantity, last-sold timestamp, restock timestamp, e-commerce visibility flags, out-of-stock overrides |
| Store model | Locations, regular hours, special or holiday hours, timezone handling, temporary closure |
| Order model | Existing inbound order ingestion, cart adjustment or substitution logic, cancellation flows, fulfillment status transitions |
| Scheduling | Cron, job schedulers, workflow engines, anything already running nightly per-tenant work |
| Inbound webhooks | Existing receiver endpoints, signature verification, idempotency or dedupe stores, async handoff patterns |
| Identifier mapping | Existing external-ID mapping tables for other channels — the pattern to follow |
| Tenancy | How merchant data is scoped and isolated; whether item IDs are unique per merchant or only per merchant-plus-tenant |
| Secrets | Vault or secret-manager usage, config loading, how per-tenant credentials are stored today |
| Observability | Logging, metrics, tracing conventions; whether per-merchant integration health is already visible |
| Test infrastructure | Contract tests, HTTP fixture or record-replay tooling, fake data factories, staging environment setup |

Three questions deserve extra attention, because getting them wrong is expensive to unwind:

**Are item identifiers globally unique across merchants?** If the platform reuses item IDs
across different businesses, say so loudly. It changes how catalog scoping works and needs a
conversation with the DoorDash technical account manager before the build starts.

**Is availability data trustworthy?** Find out whether merchants actually maintain stock
levels. Most don't. This determines whether the availability logic reads on-hand quantities
or falls back to inference, and inference has a hard numeric gate attached to it.

**Can the largest merchant's full inventory be served in one HTTP response?** Paginated pull
is prohibited (see Appendix A §5), so this is a hard constraint rather
than a preference. Find the largest catalog on the platform and check whether the
architecture can stream it inside a request timeout.

Also resolve any scope questions the user marked "unknown," and say in the report which ones
you answered and how.

Write the recon report using the template in Appendix E. Mark anything
you could not verify from the code as an open question rather than filling the gap with a
plausible assumption.

## Phase 2: Implementation plan

Write the plan for the partner's engineers to review and argue with. That means real options
with real tradeoffs where the codebase permits more than one approach, not a single
confident answer dressed up as inevitability.

Take contract snapshot **Checkpoint A** before writing the data mapping table.

Use the plan template in Appendix E. Three sections carry most of the
value and shouldn't be compressed:

**The data mapping table.** Every DoorDash field the integration sends, the source field in
this codebase, the transform, and what happens when the source is null. This is where
catalog quality is won or lost, and catalog quality is what the auto-build gate measures.

**The operational decision points.** Webhook authentication and replay protection,
idempotency, event ordering, dead-lettering, rate-limit and retry budgets, queued-write
reconciliation, per-tenant rollout, credential rotation, alerting. For a system serving many
unrelated merchants these are first-class design decisions, not implementation details. The
template lists them with defaults.

**The certification coverage matrix.** Every certification requirement, the work item that
satisfies it, and its owner. Requirements with no work item and no owner are the finding —
list them under "Work this plan does not cover" rather than leaving them implicit. Pull the
requirement list from Appendix C.

Carry the requirement level into the plan and keep the five buckets distinct: Required,
Merchant Pick Required, Dasher Pick Required, Recommended, Optional. Do not present a
Recommended item as a certification blocker — a plan that inflates the blocking set gets its
real blockers discounted along with the padding. Dasher Pick Required is conditional: blocking
only if the merchant base includes Dasher-pick stores.

Then stop. **Write the plan to the repository** (or hand it to the user to save) with a
version line and an unsigned approval line at the top — the template shows the format. Do
not start writing implementation code until the user approves it.

The written artifact matters because approval has to survive a context reset, a new session,
or a handoff to a different tool. "The user said yes earlier in the conversation" is not
durable. When they come back with changes, update the plan file, bump its version, and carry
the changes into the PR sequence — the approved plan is the one they edited, not the one you
wrote.

A scope change after approval needs confirmation of the changed portion before it is
implemented — updating the plan file is not the same as being re-approved for the new work.
Treat the existing approval as covering only what it described.

## Phase 3: Pull requests

Only at tier 3, and only after the plan file carries a completed approval line.

Open **draft** PRs against a sandbox or staging target. One PR per component, in dependency
order, each independently reviewable and each shipping with its own tests:

1. Config, credential loading, JWT signing, shared DoorDash HTTP client. No business logic.
2. Identifier mapping schema and migrations, including queued-operation tracking.
3. Catalog push and catalog pull endpoint.
4. Inventory push, inventory pull endpoint, availability logic.
5. Store hours push and store hours pull endpoint.
6. Onboarding request, status webhook receiver, onboarding state machine.
7. Scheduled full-refresh jobs.
8. Orders, if in scope — webhook receipt, confirmation, adjustment, cancellation, and
   ready for pickup (O13, Merchant Pick Required). Retail return is in the Postman
   collection and has no certification row. Include it on this PR when orders are in
   scope, and keep its `operation_id`.
9. Store pause and resume (`PUT` store status). Recommended (O16, O17). This does not
   depend on orders.
10. Certification test harness covering the matrix.

Promotions (P2, P3) are Optional. Fulfillment capacity and the v1 store and menu reads are
in the Postman collection and have no certification row. Do not treat them as blockers.
Fulfillment capacity can ship with store hours. Store reads support reconciliation of
queued writes. Promotions can ship with inventory or as their own PR.

Take contract snapshot **Checkpoint B** before each PR that builds a payload.

Rules that hold regardless of what the user asks for:

- **Never handle secret material.** Do not print, log, echo, commit, or ask the user for the
  value of a signing secret, pull-endpoint token, or credential. Refer to secrets only by name
  and by the storage location found during recon. Signing secrets load from the existing
  secret mechanism; if there isn't one, that's a blocker to raise, not a thing to work around
  with a config file. This holds while debugging too — an opaque `401` is not a reason to echo
  a secret.
- **Never target production.** No production provider type, no production credentials, no
  production branch, no production webhook subscriptions.
- **Never write to a live merchant's data.** Sandbox and test fixtures only.
- **Follow the conventions already in the repo** — its error handling, logging, testing, and
  module layout. A PR that imports a new framework to solve a solved problem will be
  rejected, and rightly.
- **Keep PRs reviewable.** If one grows past a few hundred lines of real logic, split it.
- Every PR body states what it does, which certification requirements it advances, how it
  was tested, what snapshot it was built against, and what it deliberately leaves out.

If the repository has no sandbox or staging configuration at all, stop and say so. Opening
PRs with nowhere safe to run them wastes the team's review time.

## Hard requirements worth memorizing

These come up in nearly every build. A bullet that is a certification blocker says so.
Recommended work in this list is still worth doing, and it is not a blocker. The
canonical values live in Appendix A — **if this list and that table
ever disagree, the table wins.**

- **Inventory availability is an enum, not a boolean.** Guides and certification
  requirements say `is_active` for items. On inventory the wire field is
  `item_availability` with `ACTIVE` or `INACTIVE`. `is_active` is a different field: the
  store pause flag on `PUT /marketplace/api/v1/stores/{location_id}/status`.
- **Availability inference uses a 60-day lookback.** An item created, restocked, or sold in
  the past 60 days is available, unless the merchant hid it from e-commerce sales.
- **Certification measures availability output, not intent.** Sampled live stores need at
  least 1,000 active items each and at least 70% of items sent marked active.
- **Catalog auto-build must reach 70%**, measured on at least 5,000 real merchant SKUs.
  Synthetic test data will not get you through this gate.
- **`202` means accepted, not applied.** Inventory and promotion writes return `202` with
  `operation_status: QUEUED`. The retail-return `202` sample is that same object. Jobs
  succeed with `200`, and the catalog `201` and `200` samples are that queued object.
  Keep `operation_id` on those responses and reconcile, or you get silent data loss.
  Fulfillment capacity's success code is claimed `202`. No response sample shows its body,
  so keep `operation_id` when one is present. A successful HTTP response is not the record
  of a completed business event. Do not let an HTTP status be the only durable trace that
  something happened.
- **Never build paginated inventory pull.** One endpoint per provider, and DoorDash's
  onboarding operations can only trigger non-paginated pulls, so a partner on the paginated
  variant cannot be unblocked through the standard path.
- **Removing an item from the catalog does not remove it from the menu.** Send an explicit
  inactive availability update or it keeps selling.
- **Pull endpoints must work for every store from the moment onboarding is requested** — not
  after activation. Onboarding cannot complete otherwise.
- **If merchants can inflate prices, the ceiling is 10%.** The markup tool itself (T6) is
  Recommended, not a certification blocker.
- **Store temporary deactivation is Recommended (O16, O17), not a certification blocker,**
  and it does not depend on orders. When you send it, include a documented reason and
  either `end_time` or a positive duration (`duration_in_hours` or `duration_in_secs`).
  An open-ended deactivation silently costs the merchant revenue.

## What every reply returns

**While scoping or doing recon:** the tier and its limits, what was found with evidence, what
could not be verified, and the specific next question or action.

**When delivering a plan:** where the plan file lives, the scope decision, the three critical
findings, the certification gaps with no owner, the open questions ranked by whether they
block, and an explicit request for approval. End with the approval line unsigned.

**When implementing:** which PR or patch, what it changed, which certification IDs it
advances, the snapshot it was built against, tests and their results, what is still disabled
by default, how to roll it back, and the next approval needed.

Never report a certification requirement as met on the strength of unit tests alone. Say what
was demonstrated and in which environment.

## Reference files

A link that names a reference file points at that file. In the packaged skill the file
is in the references folder beside this file. In the loose working copy it is the
sibling file of the same name. In the single-file edition those links are the appendices.

- Appendix A — **canonical** field names, enums, endpoints, response codes, numeric gates, the snapshot procedure, and the decisions log. Start here for any specific value.
- Appendix B — the seven components in detail: structure, relationships, and failure modes
- Appendix C — all certification requirements as testable assertions, grouped, with ownership
- Appendix D — non-obvious failure modes, in the order they tend to bite. Read this before writing any code.
- Appendix E — recon report, implementation plan, and PR body templates

For a field this skill does not mention, use the live documentation: the provider guide at
`https://developer.doordash.com/en-US/docs/marketplace/retail/how_to/third_party_providers_v3`
and the API reference at `https://developer.doordash.com/en-US/api/marketplace_v2`. Where
this skill and a live page disagree, Appendix A wins. The
disagreements already decided are listed there. A new disagreement is a report to your
DoorDash technical account manager, or posprovidersupport@doordash.com, not a reason to
change the payload on the spot.



---

# Appendix A — Canonical contract values

*(folder version: `references/contract-values.md`)*

## Canonical contract values

**Skill version:** 0.4.2 · **Last verified:** 2026-10-09 · **Maintained by:** NV Partner Integrations
**Questions or corrections:** your DoorDash technical account manager, or posprovidersupport@doordash.com

This is the single source of truth for every value in this skill that changes faster than
the skill does: field names, enum values, endpoint paths, response codes, and numeric
certification gates. Every other file in this skill points here. **Where any other file
disagrees with this table, this table wins**, and the disagreement is a bug worth reporting
to your DoorDash technical account manager, or posprovidersupport@doordash.com.

Contents:
1. [Verification checkpoints](#1-verification-checkpoints)
2. [Numeric gates](#2-numeric-gates)
3. [Endpoints and response codes](#3-endpoints-and-response-codes)
4. [Enum values](#4-enum-values)
5. [Decisions log](#5-decisions-log)

Sources:
- API reference: `https://developer.doordash.com/en-US/api/marketplace_v2` (catalog, inventory, hours, jobs, promotions, onboarding)
- Orders API reference: `https://developer.doordash.com/en-US/api/marketplace` (orders and order webhooks — shared with restaurant)
- Provider guide: `https://developer.doordash.com/en-US/docs/marketplace/retail/how_to/third_party_providers_v3`
- Onboarding guide: `https://developer.doordash.com/en-US/docs/marketplace/retail/how_to/nv_ssio`

---

### 1. Verification checkpoints

Field names and enums drift. Rather than fetching documentation continuously — which burns
context and risks acting on a truncated schema — take a **contract snapshot** at two
defined points and treat it as an artifact the team can review.

**Checkpoint A — before writing the data mapping table in Phase 2.**
**Checkpoint B — before each implementation PR that builds a payload.**

At each checkpoint:

1. Fetch the API reference above.
2. Extract only the models you need for the work in hand. For most PRs that means one or
   two of: `StoreItem`, `Item`, `Store`, `CreateJobRequest`,
   `CreatePullStoreItemsJobParameters`, `ManagedMerchantConnectRequest`. Also extract
   these when the PR touches them: `BatchAddOrUpdatePromotionRequest` (the array is not
   the request to send), the store-status request schema next to `StoreActivationStatus`,
   order confirm, and order adjustment. Do not pull the whole page into context — it is a
   single large document and will truncate.
3. Write a snapshot to the repo at `docs/doordash/contract-snapshot-<YYYY-MM-DD>.md`
   containing only field names, types, enum values, required flags, endpoint paths, and
   response codes. No prose copied from DoorDash's documentation.
4. Diff it against sections 2–4 below and report the deltas to the user before writing code.

**If the environment has no fetch capability:** use the values below, state in the
deliverable that they are unverified as of the "Last verified" date above, and list which
specific models the user should check by hand. Do not silently proceed as if verified, and
do not fill schema gaps by guessing at field names.

A snapshot delta on the list below is not a reason to change the payload. This file wins:

- Pagination. Send `PULL_STORE_ITEMS` and `pull_mode` `REPLACE` only.
- Promotions. Send one promotion object.
- Adjustment `merchant_supplied_id`. Send a string.
- Business-wide pricing. Send `merchant_supplied_item_id` and `price_info` only.
- Store status. Send the request schema, including an end time or a positive duration.
  The `StoreActivationStatus` model table is shorter than that schema.
- Fulfillment capacity, business-wide pricing, and multi-location onboarding stay in the
  endpoint table when a Marketplace v2 reference page has no heading for them.
  `GET /marketplace/api/v2/store_onboarding/{onboarding_id}` stays too. It is documented
  on the onboarding guide.

A delta outside that list means this skill needs updating. Report it to your DoorDash
technical account manager, or posprovidersupport@doordash.com.

---

### 2. Numeric gates

| Gate | Value | Source |
|---|---|---|
| Availability inference lookback | **60 days** | Certification checklist — see decisions log |
| Catalog auto-build rate | **≥ 70%** (alcohol, retail, grocery) | Provider guide and the certification checklist |
| Real SKUs required for auto-build measurement | **≥ 5,000** | Certification checklist |
| Active items per sampled store | **≥ 1,000** | Certification checklist |
| Share of sent inventory marked active | **≥ 70%** | Certification checklist |
| Price inflation ceiling | **10%** | Certification checklist |
| Merchant eligibility floor | **≥ 200 items** with name, merchant item ID, UPC, size, extended size | Onboarding guide |
| Partner pre-work | **< 5 steps, < 5 minutes** | Certification checklist |
| Onboarding calls on test store after certification | **≥ 2 across 2 days** | Certification checklist |
| JWT lifetime | **≤ 1800 seconds** past `iat` | JWT guide |
| Store onboarding webhook retries | **3** | Onboarding guide |
| Batch onboarding intent size | **1–100 locations** | API reference |
| Catalog / inventory pull batch limit | commonly raised to **100,000** items per provider | Provider configuration — confirm per provider type |

#### Refresh cadences

The provider guide and the certification checklist disagree here. Both are listed because
they serve different purposes: build to the expectation, and know where the floor is.

| Data | Guide expectation | Certification floor |
|---|---|---|
| Catalog | Monthly or weekly full refresh, staggered across days | Monthly |
| Inventory | Daily full refresh | Nightly, every integrated store |
| Store hours | Daily or weekly full refresh | Monthly |

Build to the guide expectation. A partner refreshing hours monthly will pass certification
and still drift out of sync with merchant reality.

---

### 3. Endpoints and response codes

Host: `https://openapi.doordash.com`. Paths below are the full path on that host.

| Operation | Method and path | Success |
|---|---|---|
| Add items (catalog) | `POST /marketplace/api/v2/items` | `201` |
| Update items (catalog) | `PATCH /marketplace/api/v2/items` | `200` |
| Update store hours | `PATCH /marketplace/api/v2/stores/{store_location_id}` | `200` |
| Add store inventory | `POST /marketplace/api/v2/stores/{store_location_id}/items` | `202` |
| Update store inventory | `PATCH /marketplace/api/v2/stores/{store_location_id}/items` | `202` |
| Update business-wide pricing | `PATCH /marketplace/api/v2/businesses/{business_id}/items` | `202` |
| Update fulfillment capacity | `PATCH /marketplace/api/v2/stores/{store_location_id}/fulfillment_capacity` | `202` |
| Create pull job | `POST /marketplace/api/v2/jobs` | `200` |
| Create promotion | `POST /marketplace/api/v2/promotions/stores/{store_location_id}` | `202` |
| Update promotion | `PATCH /marketplace/api/v2/promotions/stores/{store_location_id}` | `202` |
| Onboarding — single store, returns URL | `POST /marketplace/api/v2/managed_merchant_connect_url` | `200` |
| Onboarding — batch intent, 1–100 stores | `POST /marketplace/api/v2/nv_multi_location_onboarding_intent` | `200` |
| Onboarding status lookup (optional) | `GET /marketplace/api/v2/store_onboarding/{onboarding_id}` | `200` |
| Store details | `GET /marketplace/api/v1/stores/{location_id}/store_details` | `200` |
| Menu details | `GET /marketplace/api/v1/stores/{merchant_supplied_id}/menu_details` | `200` |
| Full menu | `GET /marketplace/api/v1/stores/{merchant_supplied_id}/store_menu` | `200` |
| Temporary deactivation and reactivate | `PUT /marketplace/api/v1/stores/{location_id}/status` | `200` |
| Confirm order | `PATCH /marketplace/api/v1/orders/{id}` | `202` |
| Adjust order | `PATCH /marketplace/api/v1/orders/{id}/adjustment` | `202` |
| Cancel order | `PATCH /marketplace/api/v1/orders/{id}/cancellation` | `202` |
| Ready for pickup | `PATCH /marketplace/api/v1/orders/{id}/events/order_ready_for_pickup` | `202` |
| Retail return | `POST /marketplace/api/v1/orders/{id}/return` | `202` |

Restaurant menu create and update (`POST` / `PATCH` `/api/v1/menus`), item activation on the
v1 reference, and Checkout / DoorDash Direct stay out of this table.

#### Paths the 2026-10-09 Marketplace v2 reference page did not list

The page fetched that day goes from store-item `PATCH` to jobs, and from
`managed_merchant_connect_url` to checkout, with no heading for the three paths below.
They stay in the table. A reader should not drop them because that page skipped them.

- **Fulfillment capacity** — `PATCH /marketplace/api/v2/stores/{store_location_id}/fulfillment_capacity`.
  Present in the partner Postman collection. Absent from that v2 page. Body: `status`
  (`OVER_CAPACITY` or `AVAILABLE`) and `status_timestamp`. Success claimed: `202`.
- **Business-wide pricing** — `PATCH /marketplace/api/v2/businesses/{business_id}/items`.
  Already in this skill and in Postman. Absent from that v2 page. Send `items[]` of
  `merchant_supplied_item_id` and `price_info` only. The published sample shows a full
  store item. Do not copy those extra fields. Success claimed: `202`. `{business_id}` is
  the partner's own business id, the same value as `location_group_id` on onboarding.
  Postman states that. The snapshot does not. The DoorDash-assigned business id learned
  at `BUSINESS_ID_MAPPED` is a different value. Catalog `scope.business_ids` takes that
  same partner id, exactly one.
- **Multi-location onboarding** — `POST /marketplace/api/v2/nv_multi_location_onboarding_intent`.
  Already in this skill and in Postman. Absent from that v2 page. Body: `requests[]` of the
  managed-merchant object, 1–100. Success claimed: `200`, with a per-location `results` array.

`GET /marketplace/api/v2/store_onboarding/{onboarding_id}` is on the onboarding guide, not
on that v2 page.

#### Store status

`PUT /marketplace/api/v1/stores/{location_id}/status` is both temporary deactivation and
reactivate. Success is `200`.

The request schema includes `is_active`, `reason`, `notes`, `merchant_supplied_id`,
`end_time`, `duration_in_hours`, and `duration_in_secs`. The model table named
`StoreActivationStatus` lists only `is_active`, `reason`, and `notes`. Send the request
schema. Deactivation (`is_active: false`) needs a documented `reason` and either `end_time`
or a positive `duration_in_hours` or `duration_in_secs`.

Deactivation reasons: `out_of_business`, `operational_issues`, `delete_store`,
`payment_issue`, `store_self_disabled_in_their_POS_portal`,
`store_pos_connectivity_issues`.

`is_active` here is the store pause flag. On inventory, availability is
`item_availability`. See §4.

#### Which id goes where

`merchant_supplied_id` is not one identifier. Postman uses it for different values on
different calls. The snapshot names the path parameters and does not spell this out.

| Place | Send |
|---|---|
| Menu details and full menu path `{merchant_supplied_id}` | The store's `location_id` |
| Store details path `{location_id}` | The store's `location_id` |
| Order path `{id}` | DoorDash `order.id` from the order webhook |
| Confirm and ready-for-pickup body `merchant_supplied_id` | The partner's order id |
| Adjustment `merchant_supplied_id`, including on a substitute | The item id, as a string |
| Adjustment `line_item_id` | The line item id from the order webhook |
| Retail return `return_items[].merchant_supplied_id` | The item id |
| Retail return `return_location_id` | The store's `location_id` |
| Promotion `purchase_items` | `merchant_supplied_item_id` values |
| Store-status body `merchant_supplied_id` | The store's `location_id` |
| Catalog `scope.business_ids` and business-pricing `{business_id}` | The partner's business id, same value as `location_group_id` |

#### Orders

Confirm, adjust, cancel, ready-for-pickup, and retail return succeed with `202`.
Ready-for-pickup uses event type `order_ready_for_pickup`.

`pickup_instructions` on confirm is at most 128 characters.

Adjustment `merchant_supplied_id` is a string. A v1 sample shows a number. The partner
Postman collection sends a string. Send a string.

Confirm `order_status` is `success` or `fail`. On `fail`, send `failure_reason`.
`errors[]` items are `code`, `merchant_supplied_id`, and `message`.

`adjustment_type` is `ITEM_UPDATE`, `ITEM_REMOVE`, or `ITEM_SUBSTITUTE`.
`cancel_reason` is `ITEM_OUT_OF_STOCK`, `STORE_CLOSED`, `KITCHEN_BUSY`, or `OTHER`.

Retail return body: `return_items[]` (`merchant_supplied_id`, `quantity`, `reason`) and
`return_location_id`. The `202` sample includes `operation_id`, `operation_status`, and
`message`. Keep that `operation_id`.

#### Promotions

Create and update send one promotion object and return `202`. Update reuses the same
`promotion_id`. The operation samples on the reference fetched 2026-10-09 are that
object, and Postman sends that object. The model named
`BatchAddOrUpdatePromotionRequest` also shows a `promotions` array. The request to send
is the object.

Fields on that object, from Postman:

- `promotion_id`
- `promotion_type` (`BUY_X_FOR_Y`, `BUY_X_SAVE_Y`, `BUY_X_GET_Y_Z_PERCENT_OFF`)
- `funding_source` (`MERCHANT` or `CPG`)
- `purchase_criteria` (`purchase_items`, `purchase_quantity`)
- `redemption_limit.limit_per_order`
- `discount_options` — send only the discount fields that `promotion_type` uses.
  `discount_price_off` is an integer in the smallest currency unit (`200` is $2.00).
- `promotion_options.promotion_conditions` — `MIX_AND_MATCH` is the value the snapshot lists
- `start_time` and `end_time`, local store time with a numeric offset
  (`yyyy-MM-ddTHH:mm:ssXXX`)

#### Money

Postman sends `base_price`, `sale_price`, promotion `discount_price_off`, and adjustment
substitute `price` as integers in the smallest currency unit. `1699` is $16.99. The
2026-10-09 snapshot does not state the unit. `tax_rate` in Postman is a percent-style
number (`6.5`). Confirm that scale on the first sandbox menu.

#### Auth headers

Every request: `Authorization: Bearer <JWT>`, `auth-version: v2`, and `User-Agent`.

Build `User-Agent` from `provider_type`. Split on `_`, drop empty pieces, uppercase the
first character of each remaining piece, join the pieces, and append `/1.0`.
`example_pos_sandbox` becomes `ExamplePosSandbox/1.0`. `examplepos_sandbox` becomes
`ExampleposSandbox/1.0`. The split is only on underscores.

#### Queued bodies

Inventory and promotions return `202`. Jobs return `200`. The catalog create sample
(`201`) and the catalog update sample (`200`) on the reference fetched 2026-10-09 are
the same queued object. The retail-return `202` sample is that object too. Keep
`operation_id` on those responses. Fulfillment capacity's success code is claimed `202`
by Postman. No response sample shows whether that body includes `operation_id`. Keep
`operation_id` when the body has one.

```json
{ "operation_id": "string", "operation_status": "QUEUED", "message": "string" }
```

The job `200` sample also includes `max_page_size`. That field is on the response. It is
not a request field. See the pagination decision in §5.

`202` means accepted, not applied. A `200` or `201` whose `operation_status` is `QUEUED`
is the same kind of acceptance. Persist `operation_id` against the store and the batch it
covers, and reconcile against observed menu state or Developer Portal event logs.

#### Error responses

`400` validation failed · `401` unauthorized · `403` forbidden · `404` not found ·
`409` conflict (onboarding: `STORE_ALREADY_EXISTS`) · `422` Request Entity Too Large ·
`429` rate limited · `500` DoorDash internal failure.

The v2 reference labels `422` Request Entity Too Large. The error body is `code`,
`message`, and `field_errors` (`field`, `error`). An oversized batch is handled by
splitting and retrying, never by dropping items. A `422` that names a field is not fixed
by splitting: read `message` and `field_errors` first.

---

### 4. Enum values

| Field | Values |
|---|---|
| `item_availability` | `ACTIVE`, `INACTIVE` |
| `product_traits` | `ALCOHOL`, `MEDICATION`, `WEIGHTED` |
| `program_eligibility` | `SNAP`, `HSA`, `FSA` |
| `funding_source` | `MERCHANT`, `CPG` |
| `identifier_type` (in `other_identifiers`) | `UPC` |
| `job_type` | `PULL_STORE_ITEMS` — **use this**; `PULL_STORE_ITEMS_WITH_PAGINATION` — **prohibited, see decisions log** |
| `pull_mode` | `REPLACE` |
| `order_protocol` | `POS`, `TABLET` |
| `fulfillment_protocol` | `MERCHANT_PICK`, `DASHER_PICK` |
| `locale` | `en-US` |
| `day_index` | `MON` … `SUN` |
| `operation_status` | `QUEUED` |
| `promotion_type` | `BUY_X_FOR_Y`, `BUY_X_SAVE_Y`, `BUY_X_GET_Y_Z_PERCENT_OFF` |
| `promotion_conditions` | `MIX_AND_MATCH` |
| `order_status` (confirm) | `success`, `fail` |
| `adjustment_type` | `ITEM_UPDATE`, `ITEM_REMOVE`, `ITEM_SUBSTITUTE` |
| `cancel_reason` | `ITEM_OUT_OF_STOCK`, `STORE_CLOSED`, `KITCHEN_BUSY`, `OTHER` |
| Fulfillment capacity `status` | `OVER_CAPACITY`, `AVAILABLE` |
| Onboarding `status` | Treat as an **open** set — see Appendix B |

#### Vocabulary trap: inventory availability versus store `is_active`

DoorDash's provider guide and certification requirements discuss item availability as
`is_active = TRUE/FALSE`. On v2 inventory the wire field is `item_availability` with
values `ACTIVE` / `INACTIVE`. Translate at the inventory boundary. An inventory payload
carries `item_availability`.

`is_active` is a real field on the v1 store status call,
`PUT /marketplace/api/v1/stores/{location_id}/status`. There it is the store pause flag.
The two names are different calls. See §3 for the store-status request.

Prices are similarly nested: `base_price` and `sale_price` are real field names but they
live inside `price_info`, not at the top level of the item. They are integers in the
smallest currency unit. See §3.

The pack object on catalog size is `size.pack_size_details`. The older name
`pack_item_size_details` is not in the current reference. Do not send it.

---

### 5. Decisions log

Decisions made by DoorDash where the public documentation is contradictory or silent.
Recorded so nobody re-litigates them, and so they can be revisited deliberately.

#### 60-day availability lookback

**Decision:** 60 days. **Decided:** 2026-09-17. **Decided by:** NV Partner Integrations.

The provider guide states a 90-day lookback in its inventory section; the certification
checklist states 60 days. Certification is graded against 60, so 60 is what partners should
build. If a partner reports being told 90 by a technical account manager, escalate to the
owner rather than switching.

#### Pagination is prohibited

**Decision:** use non-paginated inventory pull (`PULL_STORE_ITEMS`) only.
**Decided:** 2026-09-17. **Decided by:** NV Partner Integrations.

The Marketplace v2 reference fetched 2026-10-09 still offers `PULL_STORE_ITEMS_WITH_PAGINATION`.
`CreatePullStoreItemsJobParameters` still includes `merchant_supplied_page_size`. Inventory
request text still documents `meta` (`current_page`, `page_size`, `total_page`). The job
`200` sample includes `max_page_size`. That live text contradicts this decision. The
partner Postman collection and this skill send `job_type` `PULL_STORE_ITEMS` and
`pull_mode` `REPLACE` only. Do not send `merchant_supplied_page_size`, `meta`, or any
other pagination field. `max_page_size` on the job response is not a field to send.

Two constraints make this a one-way door:

1. A partner registers **one** pull endpoint per provider — paginated or non-paginated, not
   both. The choice is effectively permanent.
2. DoorDash's onboarding operations can only trigger **non-paginated** pulls, so a
   partner on the paginated variant cannot be unblocked through the standard path.

Non-paginated delivers the highest rate of happy-path onboardings and the lowest median
onboarding duration, which matters more than the engineering convenience of paging.

The consequence lands on the partner: the pull endpoint must return every item for a store
in a single response, inside the request timeout, at the size of the largest merchant on
the platform. There is no API escape hatch. Stream the response rather than assembling it
in memory, and load-test against real maximum catalog size — not a fixture. If that target
cannot be met, it is an engineering problem to solve on the partner's side, and it is worth
raising in the plan as a risk rather than discovering during onboarding.

#### Store hours refresh cadence

**Decision:** build to daily or weekly; treat monthly as the certification floor only.
**Decided:** 2026-09-17. **Decided by:** NV Partner Integrations.

The certification checklist asks for monthly. The provider guide expects daily or weekly.
Monthly passes the gate and still leaves merchants with stale hours, which shows up as
customers ordering from closed stores.



---

# Appendix B — API contract

*(folder version: `references/api-contract.md`)*

## DoorDash Retail Marketplace: API contract

**Skill version:** 0.4.2 · **Last verified:** 2026-10-09 · **Maintained by:** NV Partner Integrations
**Questions or corrections:** your DoorDash technical account manager, or posprovidersupport@doordash.com

This file covers the **structure** of the integration: what each component does, how the
pieces relate, and where implementations go wrong. It deliberately does **not** reproduce
DoorDash's full schemas — those change faster than this file can, and a stale field name
that looks authoritative is worse than no field name at all.

- Field names, enum values, endpoint paths, response codes, numeric gates →
  Appendix A
- Complete schemas, including the nested `size`, `weighted_item_info`, and
  `product_attributes` trees → the live API reference, via the snapshot procedure in
  Appendix A §1

Examples below are illustrative and datestamped, not normative. Verify against a snapshot
before building a payload.

Contents:
1. [Accounts, providers, credentials](#1-accounts-providers-credentials)
2. [Authentication](#2-authentication)
3. [Catalog](#3-catalog)
4. [Inventory](#4-inventory)
5. [Store hours](#5-store-hours)
6. [Pull endpoints (partner-hosted)](#6-pull-endpoints-partner-hosted)
7. [Merchant onboarding (SSIO)](#7-merchant-onboarding-ssio)
8. [Orders](#8-orders)
9. [Scheduled refreshes](#9-scheduled-refreshes)
10. [Identifier model](#10-identifier-model)
11. [Out of scope](#11-out-of-scope)

---

### 1. Accounts, providers, credentials

Sequence to get unblocked, most of which requires a DoorDash technical account manager:

1. Create a Developer Portal organization at `https://developer.doordash.com/portal` and
   add team members.
2. Ask the technical account manager to attach a Marketplace integration to the org.
3. Create a **provider type** — an identifier like `example_pos_sandbox`. Sandbox and
   production are separate provider types, conventionally the same name with different
   suffixes.
4. Create credentials per provider type. The signing secret is shown **once**, at creation.
5. Configure webhook subscriptions per provider type in the Developer Portal.
6. Ask the technical account manager for a test store.

Implications for the build:

- Provider type is a first-class config value, not a constant. It appears in request
  headers and determines which webhook subscriptions fire.
- Webhook subscriptions are **per provider type and do not migrate**. Moving to production
  means re-creating every subscription, with its auth tokens, against the production
  provider type.
- Treat sandbox and production as two fully independent credential sets from the first
  commit. Retrofitting that separation later is painful.
- Per-provider settings such as pull batch limits are configured separately for production.
  Confirm them before the first live merchant onboards.

---

### 2. Authentication

Every request carries a short-lived HS256 JWT. Required headers and the lifetime cap are in
Appendix A §3 and §2.

JWT header:

```json
{ "alg": "HS256", "typ": "JWT", "dd-ver": "DD-JWT-V1" }
```

JWT payload — `aud` is always `doordash`; `iss` is the developer ID from the credential;
`kid` is the key ID; `iat` and `exp` are epoch seconds:

```json
{ "aud": "doordash", "iss": "<developer_id>", "kid": "<key_id>", "iat": 1636463841, "exp": 1636465641 }
```

Three failure modes, in order of how often they bite:

- **The signing secret is base64-encoded.** Decode it to raw bytes before computing the
  HMAC. Signing against the literal string yields a well-formed JWT and a bare `401`.
- **`iat` cannot be in the future.** A fast container clock produces intermittent auth
  failures that look like network flakiness.
- **`User-Agent` and `auth-version` are both required.** Build `User-Agent` from the
  provider type: split on `_`, uppercase the first character of each non-empty piece,
  join the pieces, and append `/1.0`. `example_pos_sandbox` becomes
  `ExamplePosSandbox/1.0`. `examplepos_sandbox` becomes `ExampleposSandbox/1.0`.

Mint short-lived tokens — five minutes is reasonable — and cache per provider type rather
than signing per request. Store the secret in whatever vault the codebase already uses.

---

### 3. Catalog

The catalog is the union of every item a business could ever sell. It is scoped to a
**business**, not a store, and shared across that business's stores.

- Items are submitted under a `scope` containing `business_ids`. **Only one business ID is
  permitted per request** — multiple, or none, fails validation.
- Items must be uniquely identifiable across the business's stores by
  `merchant_supplied_item_id`.
- Trigger a push whenever a merchant adds an item or edits item details.

Attribute quality is what matters here. DoorDash runs an automated catalog build over what
you send, and the share of items it can build without human intervention is a hard
certification gate (see Appendix A §2).

Structural points worth knowing before you map fields:

- **Age restriction is a product trait, not a boolean.** `product_traits` values are
  `ALCOHOL`, `MEDICATION`, and `WEIGHTED`. Tag alcohol with `ALCOHOL`. The snapshot and
  Postman do not say which value covers OTC medication or CBD. Do not invent one, and do
  not assume `MEDICATION` is the OTC tag. Ask your technical account manager before
  tagging either. Declaring the weighted trait triggers validation that weighted-item
  detail is present, so don't set traits you can't support.
- **UPC travels in `other_identifiers`**, as a type/value pair, not as a top-level field.
- **At least one image is required** on the item model. A merchant base with poor image
  coverage is a blocker to surface early, not a nice-to-have.
- **Size is a nested structure**, not a string. It carries dimensions, weight, volume, a
  product-specific size definition, and `size.pack_size_details`. The older name
  `pack_item_size_details` is not in the current reference. Do not send it. The guide's
  "size" and "extended size" eligibility attributes map into this tree.
- **Categories are hierarchical**, with a category name and nested sub-category. Send two
  levels where the source data supports it.
- **`product_attributes`** is the extensible bag for everything else, and where
  per-vertical requirements land. Check the supported-attributes page per vertical:
  `https://developer.doordash.com/en-US/docs/marketplace/retail/catalog_management/supported_attribute`

---

### 4. Inventory

Inventory is per **store**: price and availability for items already in the business's
catalog. Pushing inventory is what creates and maintains the customer-facing menu.

Three write paths:

| Path | Use for |
|---|---|
| `POST /marketplace/api/v2/stores/{store_location_id}/items` | First time an item is made available at a store. Base price and availability are required on first write. |
| `PATCH /marketplace/api/v2/stores/{store_location_id}/items` | Ongoing price and availability updates for a store. |
| `PATCH /marketplace/api/v2/businesses/{business_id}/items` | Price updates across every live store in a business. Send `items[]` of `merchant_supplied_item_id` and `price_info` only. The published sample shows a full store item. Do not copy those extra fields. `{business_id}` is the partner's business id, the same value as `location_group_id`. |

The business-wide path is easy to miss and useful for chain-level price changes — it saves
fanning out N store calls.

Field structure, at the shape level:

- Availability is an **enum**, not a boolean. On inventory the wire field is
  `item_availability` (`ACTIVE` / `INACTIVE`). `is_active` is the v1 store pause flag,
  a different call. See Appendix A §4.
- Prices are **nested** under `price_info`: `base_price`, `sale_price`, `tax_rate`,
  bottle-fee deposit, and per-measurement-unit variants for weighted items. `base_price`
  and `sale_price` are integers in the smallest currency unit (`1699` is $16.99). Postman
  sends `tax_rate` as a percent-style number (`6.5`). Confirm that scale on the first
  sandbox menu. The snapshot does not state either unit. See Appendix A §3.
- **Item location is a structured object** — `aisle`, `zone`, `shelf`, `side`, `section`,
  `additional_details`, `raw_text`, and `coordinates`. There is no `bin` field. Required
  for `DASHER_PICK`. A partner with no location model at all is a plan-level finding.
- Balance on hand and last-sold timestamp are optional but feed DoorDash's
  likely-out-of-stock detection. Send them if the source system has them anywhere.
- Item-level special hours restrict selling windows — needed where state law limits a
  category, e.g. alcohol in New Jersey.
- Program eligibility covers SNAP, HSA, and FSA.

**Items with extras or options cannot be sent via `POST` or `PATCH`.** Validation fails if
extras are present. They are supported **only** through the job management (pull) path.
This is the mechanical reason the certification items for modifiers say "must use Inventory
Pull."

**All three inventory paths return `202` with `operation_status: QUEUED`.** `202` means
accepted, not applied. Keep `operation_id`. See Appendix A §3.

Catalog create (`201`) and catalog update (`200`) use that same queued object on the
reference fetched 2026-10-09. Keep `operation_id` on those responses too. A pull job
succeeds with `200` and the same queued body, plus `max_page_size` on the job sample.

#### Item promotions

Create and update each send one promotion object and return `202`. Update reuses the same
`promotion_id`. The model named `BatchAddOrUpdatePromotionRequest` also shows a
`promotions` array. The request to send is the object. The fields are listed in
Appendix A §3.

#### Availability logic

The prescribed algorithm, whose *output* certification measures:

```
if merchant on-hand data is accurate:
    available = (on_hand > 0)
else:
    if merchant hid the item from e-commerce sales in the partner portal:
        available = false
    else if item created, restocked, or sold within the lookback window:
        available = true
    else:
        available = false
```

Lookback window: see Appendix A §2 and its decisions log. Merchants also need a
way to mark an item unavailable from inside the order fulfillment view, which then patches
inventory.

The merchant override is an **input** to this rule, not a one-off patch applied on top of it
— otherwise the next scheduled full refresh re-derives availability and undoes it. See the
refresh gotcha in Appendix D → Inventory.

Removing an item from the catalog does **not** deactivate it on the menu. Send an explicit
inactive availability update or it keeps selling.

---

### 5. Store hours

- `PATCH /marketplace/api/v2/stores/{store_location_id}` with open hours and special hours.
- Open hours are **required** on the payload; each entry carries a day index, start time,
  and end time. Special hours carry a date, times, and a closed flag.
- Hours that cross midnight are two blocks: one ending at `23:59:59` and one starting at
  `00:00:00` on the next `day_index`. Postman states that. The snapshot does not.
- Push on merchant edit, and on a scheduled refresh.

Store hours only apply once a menu exists for the store, so hours pushes before the first
successful inventory push have nothing to attach to. Empty or missing hours block
onboarding. Resolve timezone and DST **per store**, not per account — a provider spanning
timezones that uses one account-level timezone publishes wrong hours for most of its
footprint, and the symptom shows up as customers ordering from closed stores rather than as
an hours bug.

Fulfillment capacity is a separate store call:
`PATCH /marketplace/api/v2/stores/{store_location_id}/fulfillment_capacity`, body
`status` (`OVER_CAPACITY` or `AVAILABLE`) and `status_timestamp`, success claimed `202`.
That path is in the partner Postman collection. It was absent from the Marketplace v2
reference page fetched 2026-10-09. Business-wide pricing and multi-location onboarding
were absent from that same page and stay in scope. See Appendix A §3.

---

### 6. Pull endpoints (partner-hosted)

Three endpoints the partner hosts and DoorDash calls. All three are **required** — they are
what makes onboarding work without manual coordination.

| Endpoint | Scope | Returns |
|---|---|---|
| Catalog pull | Business | Full verbose item detail for that business's catalog |
| Inventory pull | Store | Price and availability for every item at that store |
| Store hours pull | Store | Regular and special hours for that store |

Registration and behavior:

- The URL is registered with DoorDash containing a `%s` placeholder marking where the store
  or business identifier goes in the path, e.g.
  `https://api.example.com/v1/doordash/inventory/%s`. The endpoint must be
  **path-parameterized**, not driven by a query string or request body.
- Registration includes a credential, usually a bearer token. Confirm the exact scheme with
  the technical account manager — it isn't always `Bearer`.
- Inventory pull can be triggered on demand through
  `POST /marketplace/api/v2/jobs`, which is the fastest self-test loop available. Use it
  instead of waiting for someone to trigger pulls by hand. The body is `job_type`
  `PULL_STORE_ITEMS` and `job_parameters` containing `store_location_id` and `pull_mode`
  `REPLACE`. Do not send `merchant_supplied_page_size`. Success is `200`, and the body
  still carries `operation_id`.
- Return a consistent snapshot of the store or business, or document the bounded consistency
  model you do provide. A response assembled from several reads mid-change can present a
  price from one moment and availability from another.
- A pull must never block on a slow or interactive merchant-side workflow. DoorDash calls
  these at arbitrary times, including during onboarding when nobody is watching.

#### Do not use paginated pull

The API offers a paginated inventory pull variant. The reference fetched 2026-10-09 still
offers `PULL_STORE_ITEMS_WITH_PAGINATION`, `meta`, `merchant_supplied_page_size`, and a
job response field `max_page_size`. Do not build paginated pull. Do not send
`merchant_supplied_page_size` or `meta`. `max_page_size` may appear on the job response.
Postman and this skill use `PULL_STORE_ITEMS` and `pull_mode` `REPLACE` only. A partner
registers one pull endpoint per provider and cannot have both, and DoorDash's onboarding
operations can only trigger non-paginated pulls, so a partner on the paginated variant
cannot be unblocked through the standard path. Full reasoning in Appendix A §5.

The consequence is a real engineering constraint: the pull endpoint must return **every**
item for a store in a single response, inside the request timeout, at the size of the
largest merchant on the platform. A large grocery store can carry tens of thousands of
items and a business catalog can exceed 50,000. Stream the response rather than assembling
it in memory, and load-test against real maximum catalog size rather than a fixture. If
that target can't be met, raise it as a plan risk — there is no API escape hatch.

#### Pull eligibility timing

**Every store must be pull-eligible the moment its onboarding request is sent** — not once
it reaches activation. The instinct is to enable pulls for activated stores. Onboarding
then deadlocks, because all three pulls are prerequisites for activation.

---

### 7. Merchant onboarding (SSIO)

Self-Serve Integration Onboarding. The partner initiates, DoorDash hosts the merchant-facing
flow, and the partner tracks progress via webhooks.

#### Merchant eligibility gate

Enforce this **before** letting a merchant start onboarding: a minimum item count carrying
name, merchant-supplied item ID, UPC, size, and extended size (threshold in
Appendix A §2). Image, brand, and description are recommended and shorten
time-to-live. A merchant who slips through with too few usable items fails out with a blank
or unusable menu and blames the integration.

#### Two initiation paths

| Path | Returns | Completion |
|---|---|---|
| `POST /marketplace/api/v2/managed_merchant_connect_url` — one store per call | A merchant-specific onboarding URL | Merchant self-serves through the DoorDash-hosted flow |
| `POST /marketplace/api/v2/nv_multi_location_onboarding_intent` — 1–100 stores per call | No URL; a per-location `results` array | Manual DoorDash-rep follow-up for accepted locations |

The single-store path is the primary flow and the one the merchant-facing button drives.
**More than one store in that payload fails validation** — the batch endpoint is a separate
operation, not a bulk mode of the same one.

The batch path is how a partner satisfies the operator bulk-initiation requirement in the
certification matrix without looping over the single-store endpoint. Each location is
processed independently, so one bad location doesn't fail the batch — read the `results`
array per location rather than trusting the top-level `200`. Since no URL comes back and
completion depends on DoorDash follow-up, don't wire the merchant-facing button to it.

Request fields are identical for both paths; see Appendix A §3 for endpoints and
§4 for the protocol enums. Illustrative payload:

```json
{
  "location_id": "example_store_456",
  "location_group_id": "example_biz_123",
  "store_name": "Empanada Empire",
  "location_name": "Candler",
  "address": { "line_1": "101 DoorDash St.", "line_2": "Suite #5",
               "city": "Atlanta", "state": "GA", "zip": "30312" },
  "first_name": "James", "last_name": "Walnut",
  "email": "admin@example.com",
  "business_phone": "+14124818894",
  "expected_go_live_date": "2026-12-15",
  "merchant_decision_maker_email": "owner@example.com",
  "order_protocol": "POS",
  "fulfillment_protocol": "MERCHANT_PICK",
  "locale": "en-US"
}
```

`order_protocol` is `POS` or `TABLET`. This example uses `POS`. The snapshot and Postman
list both values and do not say to switch to `TABLET` when orders are out of scope. Do
not infer that. Ask your technical account manager which value this provider should send.

Points that cause abandoned onboardings:

- `location_id` is the partner's store identifier and becomes the identifier used in every
  subsequent API call. It must be stable forever and unique across the provider's whole
  footprint.
- `location_group_id` is the partner's parent business identifier.
- `email` must be a Business Admin in the DoorDash Merchant Portal.
- **Every field must be sourced programmatically** from stored merchant data. Certification
  explicitly checks that a human isn't typing these during the flow, because typos cause
  store-match failures.

#### Handling the returned URL

- Cache it and keep surfacing it — it doubles as the merchant's route into the DoorDash
  onboarding tracker.
- It has no simple time-based expiry, but it **dies if onboarding reaches `ABANDONED`**. At
  that point, call the API again for a fresh URL.
- Surface it embedded in the portal or in a new tab. DoorDash never redirects back and
  never appends query parameters, so progress comes only from webhooks. Building the flow
  around a redirect callback will not work.
- Keep the initiation button available until `BUSINESS_ID_MAPPED` arrives. Merchants abandon
  the hosted flow constantly; if the button vanishes on first click they're stuck.
- Provide a way for non-engineering operators to re-initiate onboarding.

#### Store onboarding status webhook

Subscribing is mandatory. A status-lookup `GET` exists as a supplement, not a substitute.
Failed webhooks retry a limited number of times and then stop, so return `2xx` fast, do the
work asynchronously, and make handlers idempotent — the same status can arrive twice.

```json
{
  "onboarding_id": "9154103a-be82-48fc-978d-792171f7ee32",
  "location_id": "xdDJSnx",
  "doordash_store_uuid": "f1c7f43b-64ca-4586-b990-171aaafbca2d",
  "status": "ABANDONED",
  "exclusion_code": "DUPLICATE_LOCATION_ID",
  "details": "",
  "menus": [ { "menu_uuid": "...", "menu_preview_link": "...", "menu_error": "..." } ]
}
```

#### The status set is open, not closed

Known statuses, in rough progression:

`INTEGRATION_REQUESTED` → `STORE_CONNECTED` → `BUSINESS_ID_MAPPED` →
`CATALOG_REQUESTED` → `CATALOG_DATA_RECEIVED` → `MENU_REQUESTED` → `MENU_IMPORTED` →
`HOURS_SET` → `MENU_AUDIT` → `MENU_QUALIFIED` → `INTEGRATION_ACTIVATED`

Failure states: `CATALOG_PULL_FAILED`, `MENU_BLOCK`, `ACTIVATION_BLOCK`, `ABANDONED`.

**Treat this as an open set.** DoorDash's own documentation is inconsistent about these
names — one section lists `CATALOG_RECEIVED` and `CATALOG_BLOCK` where the formal enum
lists `CATALOG_DATA_RECEIVED` and `CATALOG_PULL_FAILED` — and states the set is subject to
change. Store the raw string, map known values, treat unknown values as in-progress, and
alert. A closed enum that rejects unrecognized values will drop onboardings on the floor
the next time a status is added.

`INTEGRATION_ACTIVATED` is the signal to enroll the store in real-time change streams and
scheduled refreshes.

#### Exclusion codes

Abandonment causes: duplicate location ID, virtual brand detected, self-delivery detected,
DoorDash Drive store, store already live on the requested provider, no DoorDash account
record found.

Menu-block causes: menu pull request failure, store hours not populated, blank menu, menu
job failure (structural).

Exact code strings are in the onboarding guide. **Split them by owner when surfacing.**
Merchant-owned problems — missing hours, blank menu, wrong location — go to the merchant
with specific guidance. Partner-owned problems — pull failures, malformed payloads — go to
the partner's support queue. Certification checks both routes, and conflating them means
merchants receive errors they cannot act on.

#### Testing onboarding

Four scenarios: conversion happy, conversion unhappy, net-new happy, net-new unhappy.
"Unhappy" means deliberately failing at least one of the three pulls.

For conversion tests, the store name and address sent must **exactly** match what the
technical account manager provisioned, or onboarding is abandoned immediately. Net-new tests
use dummy data including the word `TEST`; sandbox banking values are published on the
onboarding guide page.

---

### 8. Orders

Optional, and impossible without a merchant-facing fulfillment surface. Retail and
restaurant share the same order APIs and webhooks — use the **Marketplace (v1)** reference,
not Marketplace v2. Looking for order endpoints in the v2 docs is a common wasted
afternoon.

Mandatory when orders are in scope:

- **Order receipt** — respond `200` for synchronous confirmation or `202` for asynchronous.
  Choosing `202` obliges you to actually send a confirmation with a status and, on failure,
  a reason. Choosing `202` and never confirming leaves orders hanging.
- **Auto-confirm orders** — no human tap required.
- **Order adjustment** — substitution, item removal, quantity change, merchant-initiated.
- **Cancellation, both directions.** DoorDash-to-partner must be surfaced so merchants stop
  picking; partner-to-DoorDash must be available in the merchant's order view.
- **Updated order payloads** — subscribe to the Order Adjustment event. The payload has an
  `event` object describing the adjustment and an `order` object in the normal order shape.
- **Ready for pickup** signal.
- **Masked customer phone number** — consumed and surfaced to the merchant.

The calls, each succeeding with `202`, are in Appendix A §3: confirm
(`PATCH /marketplace/api/v1/orders/{id}`), adjust (`.../adjustment`), cancel
(`.../cancellation`), ready for pickup (`.../events/order_ready_for_pickup`), and retail
return (`POST /marketplace/api/v1/orders/{id}/return`).

`pickup_instructions` on confirm is at most 128 characters. Adjustment
`merchant_supplied_id` is a string. A v1 sample shows a number. Postman sends a string.

The adjustment confirmation pattern is specified and frequently implemented wrong. After a
merchant submits an adjustment, show a pending state and wait for the `ORDER_ADJUSTMENT`
webhook (typically under 10 seconds). **Receipt of that webhook is the success signal** —
then update the cart the merchant sees, striking through the original item and showing the
substitute. If it never arrives, show failure and offer retry or cancel. Optimistically
updating the UI produces merchants who believe they substituted an item DoorDash never
received.

Sandbox order adjustment requires an extra header: `dd-tenant-id: doortest:default`.
Production does not need it.

Optional: Dasher pickup instructions, Dasher status webhooks, self-delivery statuses and
courier tracking, bag fees. Also worth building: store temporary deactivation and item-level
promotions.

Store pause and resume are `PUT /marketplace/api/v1/stores/{location_id}/status`, success
`200`. This is Recommended (O16, O17) and does not depend on orders being in scope.
`is_active` on this call is the store pause flag. Deactivation needs a documented
`reason` and either `end_time` or a positive `duration_in_hours` or `duration_in_secs`.
The request schema also includes `notes` and `merchant_supplied_id`. The
`StoreActivationStatus` model table lists only `is_active`, `reason`, and `notes`. Send
the request schema. An open-ended deactivation takes the store offline until someone
notices. Store reads that pair with this call — store details, menu details, and the full
menu — are in Appendix A §3.

Item-level promotions send one promotion object. See §4.

Document the failure reasons you send on asynchronous failures. They're handed to DoorDash
support to help merchants; undocumented codes mean support escalates everything back to you.

---

### 9. Scheduled refreshes

Real-time pushes drift. Full refreshes are required in addition, not instead. Cadences, and
the gap between the guide's expectation and the certification floor, are in
Appendix A §2.

Two operational points:

- **Stagger the catalog refresh across days.** Firing every business on the first of the
  month creates a self-inflicted load spike on both sides.
- **Scope refreshes to activated stores.** Enroll a store when `INTEGRATION_ACTIVATED`
  arrives. Refreshing stores that never completed onboarding wastes capacity and muddies
  error monitoring.

Nightly inventory refresh can be satisfied either by posting all inventory or by triggering
a pull job.

---

### 10. Identifier model

| Partner side | DoorDash side | Established |
|---|---|---|
| Partner business id (`location_group_id`) | Sent as catalog `scope.business_ids` and as business-pricing `{business_id}` | Chosen by the partner, before onboarding |
| Partner business id (`location_group_id`) | DoorDash business ID | At `BUSINESS_ID_MAPPED`. That DoorDash id is not the value you send in `business_ids` |
| Store identifier (`location_id`) | `doordash_store_uuid` | During onboarding. `location_id` is also the menu-read path value |
| `merchant_supplied_item_id` | Catalog item, scoped to business | During catalog build. On adjustment and return, the field name `merchant_supplied_id` is this item id |
| — | `onboarding_id` | On the onboarding status webhook, not on the connect-URL response |
| — | `operation_id` | When the body includes one: inventory and promotion `202`, retail-return `202`, job `200`, catalog `201` and `200`. Fulfillment capacity is claimed `202` and has no response sample |

Design constraints:

- `location_id` must be stable forever and unique across the provider's whole footprint.
  Reusing or regenerating one produces a duplicate-location abandonment needing manual
  intervention to clear.
- If `merchant_supplied_item_id` values are not unique across merchants, raise it with the
  technical account manager **before** building — it affects how catalog scoping is
  configured on the DoorDash side.
- Persist `onboarding_id` and `doordash_store_uuid` from the first webhook; they're needed
  for status lookups and support conversations.
- Persist `operation_id` against the batch it covers whenever the response includes one,
  including retail return. `202` means accepted, not applied. A catalog `201` or `200`,
  or a job `200`, with `operation_status: QUEUED` is the same kind of acceptance.

---

### 11. Out of scope

The v2 reference also documents **Checkout Management** endpoints — transaction
authorization and batch transaction submission for DoorDash Direct, carrying card BINs,
last-four digits, loyalty identifiers, and tender detail. That is a separate product with
its own payment-data handling obligations and is **not** part of a retail Marketplace
integration. If a partner asks about it, treat it as a new scope conversation with the
technical account manager rather than an extension of this build.

Restaurant menu create and update (`POST` / `PATCH` `/api/v1/menus`) and the item
activation endpoints on the v1 reference stay out of scope. This skill does not cover
restaurant-only menu integrations.



---

# Appendix C — Certification matrix

*(folder version: `references/certification-matrix.md`)*

## Certification matrix

**Skill version:** 0.4.2 · **Last verified:** 2026-10-09 · **Maintained by:** NV Partner Integrations
**Questions or corrections:** your DoorDash technical account manager, or posprovidersupport@doordash.com

Every certification requirement, restated as something you can assert against. Use this to
build the test suite and to populate the coverage matrix in the implementation plan.

Contents: [How to read this](#how-to-read-this) · [Numeric gates](#numeric-gates) ·
[Catalog](#catalog) · [Inventory](#inventory) · [Store hours](#store-hours) ·
[Promotions](#promotions) · [Orders](#orders) · [Tooling](#tooling) ·
[Onboarding](#onboarding) · [Merchant enablement content](#merchant-enablement-content) ·
[Coverage summary](#coverage-summary)

### How to read this

**Requirement level**

- **Required** — mandatory for every third-party provider, whether or not they take orders
- **Merchant Pick Required** — mandatory only when orders are in scope and at least one store is `MERCHANT_PICK`. Not a blocker when orders are out of scope. Rows outside the Orders section (T5, T7, E5) follow this same rule
- **Dasher Pick Required** — mandatory only when at least one store is `DASHER_PICK`
- **Recommended** — not a certification blocker. DoorDash pushes for it and it correlates with better fulfillment
- **Optional** — build if the merchant base needs it

A **Required** row inside the Orders section applies only when orders are in scope. Skip
the rest of that section when orders are out of scope. O16 and O17 are the exception:
store pause is Recommended and does not depend on orders.

**Owner** — who has to do the work:

- `ENG` — integration code, in scope for this skill
- `UI` — merchant-facing product work in the partner portal
- `DATA` — data quality or coverage in the partner's own system
- `DOC` — written content or documentation
- `OPS` — a process or contact path, not a build

**Verified** — how DoorDash checks it. `[Live]` is demonstrated during the session;
`[Offline]` is reviewed from logs, data samples, or links beforehand.

Certification is a live 1–2 hour screen-share covering these items one at a time. Anything
needing a manual database nudge to work will be visible.

### Numeric gates

The thresholds that produce most failures live in **Appendix A §2** — deliberately
in one place, because duplicating them is how this skill previously drifted. Measure them
against real merchant data before booking a session.

The two that fail late and cost the most to fix: the catalog auto-build rate (needs real
SKUs at volume, and the remedy is upstream data work measured in weeks) and the share of
inventory sent as available (an algorithm that matches the spec can still miss the gate).

Thresholds are restated inside the assertion rows below so each row is usable as a test on
its own. Those restatements are a **mirror** — where a row and Appendix A §2
disagree, the table wins and the mismatch is a bug worth reporting to
your DoorDash technical account manager, or posprovidersupport@doordash.com.

---

### Catalog

| # | Assertion | Level | Owner | Verified |
|---|---|---|---|---|
| C1 | Items are created with full attribute detail via `POST` to the Catalog API. DoorDash scores new SKUs against its catalog baseline; the partner sends the attribute payload and does not query that table | Required | ENG | Offline |
| C2 | Catalog data quality is sufficient to auto-build ≥ 70% of items, on ≥ 5,000 real merchant SKUs | Required | DATA | Offline |
| C3 | Existing items are updated via `PATCH` to the Catalog API and the update is reflected downstream | Required | ENG | Offline |
| C4 | A catalog pull endpoint is hosted and returns business-scoped item data on request | Required | ENG | Offline |
| C5 | A full catalog refresh is posted for every business monthly, staggered across days | Required | ENG | Offline |
| C6 | Age-restricted items are tagged so checkout runs an ID check. Alcohol uses `product_traits` `ALCOHOL`. The snapshot does not map OTC or CBD to a trait. Ask the technical account manager before tagging either. Do not invent a trait | Recommended | ENG + DATA | Offline |
| C7 | Items carry category information, at minimum first level, ideally two (`Liquor >> Whiskey`) | Recommended | ENG + DATA | Offline |
| C8 | SNAP-eligible items are tagged and eligible at checkout | Optional | ENG + DATA | Offline |

---

### Inventory

| # | Assertion | Level | Owner | Verified |
|---|---|---|---|---|
| I1 | Availability inference is implemented per the prescribed logic with the 60-day lookback, sent as the `item_availability` enum | Required | ENG | Offline |
| I2 | Sampled real stores show ≥ 1,000 active items each and ≥ 70% of sent items active | Required | ENG + DATA | Offline |
| I3 | An item transitions inactive → active | Required | ENG | Live |
| I4 | An item transitions active → inactive | Required | ENG | Live |
| I5 | An item transitions active → inactive together with a price change | Required | ENG | Live |
| I6 | An item transitions inactive → active together with a price change | Required | ENG | Live |
| I7 | An item removed from the catalog is sent as inactive and leaves the menu | Required | ENG | Live |
| I8 | An item price updates with no availability change | Required | ENG | Live |
| I9 | An inventory pull endpoint is hosted and returns store-scoped price and availability | Required | ENG | Live |
| I10 | A full inventory refresh runs nightly for every integrated store | Required | ENG | Offline |
| I11 | Item location detail (`aisle`, `zone`, `shelf`, `side`, `section`, `additional_details`, `raw_text`, `coordinates`) is sent and consumed. There is no `bin` field | Dasher Pick Required | ENG + DATA | Live |
| I12 | Item-level hours restrict selling within permitted windows | Recommended | ENG | Live |
| I13 | Items with modifiers are supported and appear on the menu (requires inventory pull) | Optional | ENG | Live |
| I14 | A modifier can be deactivated (86'd) via webhook and leaves the menu | Optional | ENG | Live |
| I15 | A modifier can be reactivated via webhook and returns to the menu | Optional | ENG | Live |
| I16 | A merchant's explicit out-of-stock action survives the next scheduled full refresh | Required | ENG | Live |

On I13–I15: items with extras or options fail validation on the inventory `POST`/`PATCH`
paths. They are supported only through the job (pull) path, which is why these items say
"must use Inventory Pull" — it's a hard API constraint, not a recommendation.

---

### Store hours

| # | Assertion | Level | Owner | Verified |
|---|---|---|---|---|
| H1 | Standard store hours update via `PATCH` to Store Management | Required | ENG | Live |
| H2 | Special or holiday hours update via `PATCH` to Store Management | Required | ENG | Live |
| H3 | A store hours pull endpoint is hosted and returns store-scoped hours | Required | ENG | Live |
| H4 | A full store hours refresh is posted for every integrated store monthly | Required | ENG | Offline |

---

### Promotions

| # | Assertion | Level | Owner | Verified |
|---|---|---|---|---|
| P1 | `base_price` and `sale_price` are both sent, producing strikethrough pricing | Recommended | ENG | Live |
| P2 | Item promotions are created via the Promotion Management `POST` endpoint | Optional | ENG | Live |
| P3 | Item promotions are updated via the Promotion Management `PATCH` endpoint | Optional | ENG | Live |
| P4 | `redemption_limit.limit_per_order` is honored and matches merchant intent | Optional | ENG | Live |

---

### Orders

Skip this section if orders are out of scope, except O16 and O17. Those two are store
pause. They are Recommended, they do not depend on orders, and they are not certification
blockers.

If orders are in scope, every Merchant Pick Required row is mandatory when any store is
`MERCHANT_PICK`. O2 is Required when orders are in scope, and not required when they are
out of scope. O2 cannot be done without O1. When orders are in scope, receive and answer
order webhooks even if every store is Dasher-pick. A plan that auto-confirms and skips
receipt is not a valid reading of this matrix. The level printed on O1 stays Merchant
Pick Required.

| # | Assertion | Level | Owner | Verified |
|---|---|---|---|---|
| O1 | Order webhooks are received and answered `200` (sync) or `202` (async) | Merchant Pick Required | ENG | Live |
| O2 | Orders auto-confirm without a human tap | Required | ENG | Live |
| O3 | An order is confirmed asynchronously with `success` | Recommended | ENG | Live |
| O4 | An order is confirmed asynchronously with `fail` and a populated `failure_reason` | Recommended | ENG | Live |
| O5 | DoorDash → partner cancellation webhooks are consumed, answered `2xx`, and surfaced to the merchant | Merchant Pick Required | ENG + UI | Live |
| O6 | Merchants can cancel an order in the partner portal and the cancellation reaches DoorDash | Merchant Pick Required | ENG + UI | Live |
| O7 | Item substitution completes successfully | Merchant Pick Required | ENG + UI | Live |
| O8 | An item is removed while the rest of the order stays live | Merchant Pick Required | ENG + UI | Live |
| O9 | Item quantity is increased or decreased | Merchant Pick Required | ENG + UI | Live |
| O10 | Updated order payloads (`ORDER_ADJUSTMENT`) are received, answered `2xx`, and surfaced | Merchant Pick Required | ENG + UI | Live |
| O11 | The portal waits for the `ORDER_ADJUSTMENT` webhook before showing the adjustment as applied | Recommended | UI | Live |
| O12 | The masked customer phone number is consumed and surfaced to the merchant | Merchant Pick Required | ENG + UI | Live |
| O13 | A ready-for-pickup signal is sent when the merchant marks the order ready | Merchant Pick Required | ENG + UI | Live |
| O14 | Customer substitution preferences are consumed and surfaced during substitution | Recommended | ENG + UI | Live |
| O15 | Dasher status webhooks are consumed and status is surfaced to the merchant | Recommended | ENG + UI | Live |
| O16 | Store temporary deactivation is sent with a duration or end date | Recommended | ENG + UI | Live |
| O17 | A temporarily deactivated store can be reactivated | Recommended | ENG + UI | Live |
| O18 | `merchant_tip_amount` is surfaced in the merchant portal | Optional | UI | Live |
| O19 | Self-delivery statuses and courier tracking are sent | Optional | ENG | Live |

---

### Tooling

Mostly merchant-facing product work. This is the section that slips.

| # | Assertion | Level | Owner | Verified |
|---|---|---|---|---|
| T1 | Availability logic is implemented and a live merchant sample confirms it | Required | ENG | Offline |
| T2 | A support escalation path exists for merchants — email or form — covering onboarding and ongoing management | Required | OPS | Offline |
| T3 | Partner operators can initiate onboarding requests on a merchant's behalf, individually or in bulk | Required | UI + ENG | Live |
| T4 | Merchants can mark an item out of stock from inside the order fulfillment view | Recommended | UI | Live |
| T5 | Merchants are actively and obviously notified of new or pending orders | Merchant Pick Required | UI | Live |
| T6 | Merchants can inflate prices sent to DoorDash, capped at 10% | Recommended | UI | Live |
| T7 | Order failure reasons sent on async failure are documented for DoorDash support | Merchant Pick Required | DOC | Offline |

On T3: bulk initiation has a dedicated batch endpoint (1–100 locations) rather than needing
a loop over the single-store call — see Appendix B §7. It returns no merchant URL and
relies on DoorDash follow-up, so it fits operator tooling and must not back the
merchant-facing button. This item is part engineering, not UI alone.

---

### Onboarding

| # | Assertion | Level | Owner | Verified |
|---|---|---|---|---|
| N1 | An "Integrate with DoorDash" button exists in the partner portal and opens the DoorDash flow in a new tab automatically | Required | UI | Live |
| N2 | Onboarding requests are sent with correct `location_id`, address fields, and required metadata | Required | ENG | Live |
| N3 | Onboarding requests are sent in real time, with no delay after the merchant's click | Required | ENG | Live |
| N4 | Request contents are sourced programmatically from stored merchant data, not typed by a user | Required | ENG | Live |
| N5 | The returned DoorDash onboarding URL is surfaced — embedded or new tab — and the merchant can complete the flow and return | Required | UI | Live |
| N6 | The surfacing approach (embedded vs. new tab) is declared and recorded | Required | UI | Live |
| N7 | A net-new store onboards end to end with no blockers | Required | ENG + UI | Live |
| N8 | A store already on DoorDash converts onto the integration with no blockers | Required | ENG + UI | Live |
| N9 | Synchronous onboarding failures are handled: merchant-owned issues to the merchant, partner-owned issues to the partner support queue | Required | ENG + UI | Live |
| N10 | Onboarding can be re-initiated until `BUSINESS_ID_MAPPED` is received | Required | ENG + UI | Live |
| N11 | Store onboarding status webhooks are subscribed, answered `2xx`, and surfaced | Required | ENG | Live |
| N12 | Statuses appear in a merchant-facing onboarding tracker in the partner portal, and the tracker updates as status changes | Required | UI | Live |
| N13 | Blocked statuses are surfaced with detail and guidance; partner-owned blocks route to the partner support queue | Required | ENG + UI | Live |
| N14 | Every store requesting onboarding is immediately eligible for catalog pull | Required | ENG | Live |
| N15 | Every store requesting onboarding is immediately eligible for inventory pull | Required | ENG | Live |
| N16 | Every store requesting onboarding is immediately eligible for store hours pull | Required | ENG | Live |
| N17 | No hardware upgrade is required for a merchant to use the integration | Required | UI | Live |
| N18 | Partner-side pre-work is self-serve: under 5 steps, under 5 minutes, no inventory selection required, no need to contact the partner | Recommended | UI | Live |

On N18: defaulting all items to available is the expected behavior. Optional store hours
and price markup during pre-work are permitted exceptions.

---

### Merchant enablement content

A hard gate. The integration will not certify without ready-to-publish help-center
material. All items are `DOC`, reviewed offline via links.

| # | Assertion | Level |
|---|---|---|
| E1 | Overview of the integration between the partner's system and DoorDash | Required |
| E2 | Get started — how net-new merchants sign up, with links or video | Required |
| E3 | Add or enable the DoorDash integration — pre-work, requirements, screenshots or video | Required |
| E4 | Inventory management — preparing inventory for ingestion and managing it afterward | Required |
| E5 | Order management — managing orders through the integration | Merchant Pick Required |

---

### How many requirements there are

DoorDash's source checklist totals 78 line items. This matrix has more rows because several
composite checklist items were split into separately testable assertions, and because I16 was
added from a failure mode the checklist doesn't cover. Both counts are correct about different
things — cite "every certification requirement" rather than a number, which drifts.

### Coverage summary

Roughly how the requirements distribute by owner. Use this when arguing for staffing:

| Owner | Share | Notes |
|---|---|---|
| ENG only | about half | What the integration code delivers |
| UI, or ENG + UI | about a third | Merchant-facing portal work; the usual bottleneck |
| DATA | small but decisive | Catalog attribute coverage and availability output — two hard numeric gates |
| DOC / OPS | small | Enablement content and the support path; both are blocking |

The implementation plan should carry a version of this table filled in with the actual work
item and named owner for each requirement, and every row with no owner listed explicitly as
a gap.

### Where this checklist is weaker than the guide

Certification is a floor, not the expectation. The clearest case is store hours: this
checklist asks for a monthly full refresh (H4) while the provider guide expects daily or
weekly. A partner who builds to H4 passes and still publishes stale hours. Where the two
sources differ, build to the guide and treat the checklist number as the pass mark — see
Appendix A §2.



---

# Appendix D — Gotchas

*(folder version: `references/gotchas.md`)*

## Gotchas

**Skill version:** 0.4.2 · **Last verified:** 2026-10-09 · **Maintained by:** NV Partner Integrations
**Questions or corrections:** your DoorDash technical account manager, or posprovidersupport@doordash.com

Canonical field names, enums, and numeric values are in Appendix A. This file is
about failure modes, not values.

Failure modes that cost real days, roughly in the order they appear during a build. Each
one is a thing that has actually gone wrong, not a hypothetical.

Contents: [Auth](#auth) · [Environments](#environments) · [Pull endpoints](#pull-endpoints) ·
[Catalog](#catalog) · [Inventory](#inventory) · [Store hours](#store-hours) ·
[Onboarding](#onboarding) · [Orders](#orders) · [Scheduling](#scheduling) ·
[Certification](#certification)

---

### Auth

**The signing secret is base64-encoded.** Decode it to bytes before computing the HMAC.
Signing against the literal string produces a valid-looking JWT that gets a bare 401 with
no hint about why. This is the single most common first-day blocker.

**`iat` cannot be in the future.** A container with a fast clock fails auth intermittently
and looks like a flaky network. Use NTP-synced time and, if you must, back-date `iat` by a
second or two.

**`exp` caps at 1800 seconds past `iat`.** Longer is rejected. Mint short-lived tokens and
cache them per provider type rather than signing on every request.

**`auth-version: v2` and `User-Agent` are both required.** Build `User-Agent` from the
provider type: split on `_`, uppercase the first character of each non-empty piece, join
the pieces, and append `/1.0`. `example_pos_sandbox` becomes `ExamplePosSandbox/1.0`.
`examplepos_sandbox` becomes `ExampleposSandbox/1.0`. Omitting either header produces
auth errors that look like credential problems.

**The signing secret is displayed exactly once, at credential creation.** Capture it into
the secret store immediately. Recovering it means creating a new credential.

---

### Environments

**Sandbox and production are different provider types, and webhook subscriptions do not
migrate.** Every subscription, with its authentication tokens, must be re-created against
the production provider type. Partners have graduated, gone live, and silently received
zero orders because nobody re-subscribed. Put "recreate webhook subscriptions" on the
production cutover checklist as its own line item.

**Provider type belongs in config, not in a constant.** It travels in request headers and
determines which subscriptions fire. Hardcoding the sandbox value is a guaranteed
production incident.

**Pull batch limits and other per-provider settings are configured separately for
production.** Catalog and inventory pull limits in particular are commonly raised to
100,000 for sandbox during the build; confirm the production provider type has the same
settings before the first real merchant onboards.

---

### Pull endpoints

**The registered URL uses a `%s` placeholder for the identifier in the path.** The endpoint
must be path-parameterized — `/inventory/{location_id}` — not driven by a query parameter
or a request body. Designing it the other way means rewriting the route.

**Confirm the credential scheme rather than assuming.** Registered tokens are usually
bearer tokens with `Bearer ` prepended, but not always. Ask.

**Every store must be pull-eligible the moment its onboarding request is sent.** The
instinct is to enable pulls once a store is activated. Onboarding then deadlocks, because
catalog pull, inventory pull, and store hours pull are all prerequisites for activation.
Enroll stores in pull eligibility at request time.

**Paginated pull exists in the API and is prohibited here.** The reference fetched
2026-10-09 still offers `PULL_STORE_ITEMS_WITH_PAGINATION`, the `meta` block,
`merchant_supplied_page_size`, and a job response field `max_page_size`. That text
contradicts this skill. Postman sends `PULL_STORE_ITEMS` and `pull_mode` `REPLACE` only.
Do not send the pagination fields. A partner registers one pull endpoint per provider and
cannot have both variants, and DoorDash's onboarding operations can only trigger
non-paginated pulls, so a partner on the paginated variant cannot be unblocked through the
standard path. Reasoning and decision record in Appendix A §5.

This is the gotcha that will tempt a competent engineer into the wrong choice: they hit a
large-catalog problem, find pagination in the API reference, and take the one-way door.

**So the single-response constraint is load-bearing.** A large grocery store can carry tens
of thousands of items and a business catalog can exceed 50,000. An endpoint that assembles
the whole response in memory will pass testing and time out on a real merchant, and there is
no API escape hatch. Stream the response, and load-test against the largest real catalog on
the platform rather than a fixture. If it can't be served in one response inside the
timeout, that is a partner-side engineering problem to solve and a risk to raise in the
plan — not a reason to switch to pagination.

**Inventory pull can be triggered on demand via the Job Management endpoints.** This is the
fastest self-test loop available — use it instead of waiting on a technical account manager
to trigger pulls by hand.

---

### Catalog

**Removing an item from the catalog does not deactivate it on any menu.** Send
an explicit inactive availability update through the Inventory API. Otherwise deleted items keep
selling, customers order things the merchant doesn't have, and fulfillment metrics suffer.
Certification tests this case specifically.

**The auto-build gate needs real data, not synthetic data.** The threshold and the required
SKU volume are in Appendix A §2. Clean hand-made test items sail through and tell you
nothing. Start sending representative production-shaped merchant data as early as the
technical account manager will allow, because the remedy for a low auto-build rate is
usually upstream data-quality work in the partner's own product — which takes weeks, not days.

**One business ID per catalog request.** The `scope.business_ids` array accepts exactly one.
That value is the partner's business id, the same value as `location_group_id`. It is not
the DoorDash business id from `BUSINESS_ID_MAPPED`. Multiple, or none, fails validation —
so a partner batching items across businesses into one call gets a blanket rejection with
no per-item detail.

**At least one image is required on the item model.** A merchant base with thin image
coverage is a blocker to surface in recon, not something to discover at first push.

**Age restriction is a product trait, not a boolean flag.** Alcohol and medication are
declared through `product_traits`. Declaring the weighted trait triggers validation that
weighted-item detail is present, so don't set traits the source data can't support.

**UPC lives in `other_identifiers`** as a type/value pair, and **size is a nested tree**, not
a string. The pack object is `size.pack_size_details`. Do not send `pack_item_size_details`.
The guide's "size" and "extended size" eligibility attributes both map into that tree.
Mapping these two wrong is the most common catalog payload error — check a snapshot
rather than guessing the shape.

**Catalog is business-scoped; inventory is store-scoped.** Sending per-store item detail to
the Catalog API, or business-level pricing to the Inventory API, produces confusing partial
failures. Keep the two models distinct in the code.

**The five eligibility attributes are a gate, not a suggestion.** Name, merchant-supplied
item ID, UPC, size, extended size, on at least 200 items, before the merchant is allowed
to start onboarding. Enforce it in the partner portal; a merchant who gets through with 40
usable items will fail out with a blank or unusable menu and blame the integration.

**Size and UPC coverage are where most catalogs fail.** These are the attributes retail
merchants are least likely to maintain. Measure coverage across the existing merchant base
early — it's the best available predictor of whether certification will go smoothly.

**Age-restricted tagging has legal consequences.** `product_traits` lists `ALCOHOL`,
`MEDICATION`, and `WEIGHTED`. Tag alcohol with `ALCOHOL`. The snapshot and Postman do not
say which trait covers OTC medication or CBD, or that `MEDICATION` is the ID-check tag
for OTC. Do not invent a trait. Ask your technical account manager before tagging either.
Untagged alcohol reaching a customer without an ID check is a compliance incident, not a bug.

---

### Inventory

**Inventory availability is `item_availability`, not `is_active`.** DoorDash's provider
guide and certification requirements describe availability as `is_active = TRUE/FALSE`.
On inventory the wire field is `item_availability` with values `ACTIVE` / `INACTIVE`.
Translate at that boundary. `is_active` is a real field on the v1 store status call,
where it is the store pause flag. See Appendix A §3 and §4.

Prices are nested the same way: `base_price` and `sale_price` are real names but they live
inside `price_info`, not at the top level of the item.

**`202` means accepted, not applied.** Inventory and promotion writes return `202` with
`operation_id` and `operation_status: QUEUED`. The retail-return `202` sample is that
same object. Jobs succeed with `200`. The catalog `201` and `200` samples on the
reference fetched 2026-10-09 are that queued object. Keep `operation_id` on those
responses. Fulfillment capacity's success code is claimed `202`. No response sample
shows its body, so keep `operation_id` only when one is present. A partner that treats
the HTTP status as confirmation gets silent data loss — menus quietly wrong, nothing in
the error logs to find, and no way to reconcile after the fact because the operation ID
was discarded. Persist the operation ID against the store and batch it covers.

**`422` is labeled Request Entity Too Large.** The error body is `code`, `message`, and
`field_errors` (`field`, `error`). An oversized batch is handled by splitting, not by
dropping items. A `422` that names a field is not fixed by splitting: read `message` and
`field_errors` first.

**Items with extras or options can't go through `POST` or `PATCH`.** Validation fails if
extras are present; they're supported only via the job (pull) path. This is the mechanical
reason the modifier certification items say "must use Inventory Pull."

**The lookback window is contested in DoorDash's own docs** — the provider guide says 90,
the certification checklist says 60. Use the value in Appendix A §2; the decision
and its reasoning are logged in §5.

**Certification measures the output, not the algorithm.** Sampled live stores must clear both
an absolute active-item floor and a share-of-items-sent floor — values in
Appendix A §2. An availability
rule that is defensible but conservative — for example requiring positive on-hand when
on-hand data is unreliable — will fail this gate even though the code matches the spec.
Measure your own active-item ratio across real merchants before booking certification.

**Most merchants do not maintain accurate stock levels.** Plan for the inference path as
the primary path, not the fallback. Partners who build inference well see materially
better fulfillment quality.

**`balance_on_hand` and `last_sold_datetime` are optional but feed likely-out-of-stock
detection.** Sending them costs little and improves fulfillment. Send them if the fields
exist anywhere in the source system.

**Merchant-set out-of-stock needs to reach DoorDash.** A merchant marking an item
unavailable while picking an order must trigger an inventory `PATCH`, not just a local
state change.

**A full refresh must not silently undo a merchant's out-of-stock action.** A merchant's
explicit unavailable flag takes precedence over inferred availability until the platform's
own documented restock or override rule clears it. A nightly refresh that re-derives
availability from the inference rule alone will quietly put the item back on sale, and the
merchant will see an item they 86'd selling again with no explanation. Model the override as
durable state that the refresh reads, not as a transient patch the refresh overwrites.

This one hides between two requirements that are each individually correct: send merchant
overrides, and run a nightly full refresh. Build them independently and they fight.

**Strikethrough pricing needs both prices.** Send `base_price` and `sale_price` together,
as integers in the smallest currency unit (`1699` is $16.99). Sending only a discounted
price loses the strikethrough and the merchant loses the promotional signal. Sending
`16.99` prices the item about 100 times too low.

**Price inflation is capped** (ceiling in Appendix A §2). If the platform offers
merchants a markup tool, enforce the ceiling in code. Merchants will ask for more.

**Item location data is required for `DASHER_PICK`.** The fields are `aisle`, `zone`,
`shelf`, `side`, `section`, `additional_details`, `raw_text`, and `coordinates`. There is
no `bin` field. If the merchant base includes Dasher-pick stores and the platform has no
location data model at all, that is a plan-level finding, not an implementation detail.

---

### Store hours

**Hours only apply to a menu that already exists.** A store hours push before the first
successful inventory push has nothing to attach to. Order the onboarding flow accordingly.

**Empty or missing hours block onboarding** with `STORE_HOURS_NOT_POPULATED_FAILURE`.
Validate hours before sending, and treat "merchant has no hours configured" as a
merchant-facing prompt rather than a silent skip.

**Hours that cross midnight are two blocks.** One ends at `23:59:59` and the next starts
at `00:00:00` on the following `day_index`. Postman states that. Sending one block with
`end_time` before `start_time` is not the documented shape.

**Special hours are required, not optional.** Holiday and exception hours are a separate
certification item from regular hours.

**Timezone and DST are per store.** A provider operating across timezones that resolves
hours against a single account-level timezone will publish wrong hours for most of its
footprint, and the symptom — customers ordering from closed stores — surfaces as a
fulfillment problem rather than an hours bug.

---

### Onboarding

**Treat the status enum as open.** The published status list is inconsistent across
sections of the same page — `CATALOG_RECEIVED` versus `CATALOG_DATA_RECEIVED`,
`CATALOG_BLOCK` versus `CATALOG_PULL_FAILED` — and the documentation states the set is
subject to change. Store the raw status string, map known values, treat unknown values as
in-progress, and alert rather than throwing. A closed enum that rejects unrecognized values
will drop onboardings on the floor the next time DoorDash adds a status.

**One store per onboarding request — on the URL-producing endpoint.** More than one fails
validation there. A separate batch endpoint accepts 1–100 locations but returns no merchant
URL and depends on manual DoorDash follow-up, so it suits operator bulk initiation and not
the merchant-facing button. Treating the two as one endpoint with a bulk mode produces
either a rejected payload or merchants stuck waiting for a URL that never comes.

**`location_id` must be stable and unique forever.** Regenerating it, reusing it after a
store closes, or deriving it from something mutable produces `DUPLICATE_LOCATION_ID` and an
abandoned onboarding that needs manual intervention to clear.

**The managed merchant URL dies on `ABANDONED`.** Before that it keeps working
indefinitely and should be cached and continuously surfaced. Once onboarding is abandoned,
the merchant cannot use it — you must submit a fresh onboarding request to mint a new URL.
Detect `ABANDONED`, tell the merchant plainly, and give them a way to start over.

**Keep the initiation button available until `BUSINESS_ID_MAPPED`.** Merchants abandon
halfway through the DoorDash-hosted flow constantly. If the button disappears the moment
they first click it, they're stuck and they contact support.

**DoorDash never redirects back to the partner portal and never appends query parameters.**
All progress tracking is backend, via webhooks. Building the flow around a redirect
callback will not work.

**Onboarding request fields must be sourced programmatically.** Certification explicitly
verifies that a human isn't typing the address into a form during the flow. Manual entry
causes store-match failures and abandoned onboardings, and it's an easy certification fail.

**Conversion test stores need exact name and address matching.** Whatever the technical
account manager provisioned must be sent character-for-character, or onboarding is
abandoned on the spot.

**Split blocked statuses by owner.** Merchant-owned issues (missing hours, blank menu,
wrong location) go to the merchant with specific guidance. Partner-owned issues (pull
failures, malformed payloads) go to the partner's support queue. Certification checks both
routes, and conflating them means merchants receive errors they cannot act on.

**Webhooks retry 3 times, then stop.** Return `2xx` fast and do the work asynchronously.
Make handlers idempotent — the same status can arrive more than once — and don't let a slow
downstream write cost you the notification.

**Operators need a way to initiate onboarding on a merchant's behalf.** Merchants signed by
DoorDash sales sometimes go quiet before clicking the button in the partner portal. A
partner-side operator tool to fire the initial request is a certification requirement, and
DoorDash asks partners to rescue unresponsive merchants after launch.

---

### Orders

**Receipt of the `ORDER_ADJUSTMENT` webhook is the success signal for an adjustment.** The
specified pattern: merchant submits, show a pending state, wait for the webhook (typically
under 10 seconds), then update the cart the merchant sees — striking through the original
item and showing the substitute. If the webhook never arrives, show failure and offer retry
or cancel. Optimistically updating the UI produces merchants who believe they substituted
an item that DoorDash never received, and customers who receive the wrong order.

**Sandbox order adjustment requires an extra header:** `dd-tenant-id: doortest:default`.
Without it, adjustment requests fail in sandbox in a way that looks like a bug in your
payload. Production does not need it.

**Orders use the Marketplace v1 API, not Marketplace v2.** Catalog, inventory, hours, jobs,
and promotions are v2. Orders and order webhooks are shared with restaurant and live on the
v1 reference. Looking for order endpoints in the v2 docs is a common wasted afternoon.

**`200` versus `202` is a real choice on order receipt.** `200` means synchronous
confirmation; `202` means you'll confirm asynchronously and must then actually send a
confirmation with a status and, on failure, a `failure_reason`. Choosing `202` and never
confirming leaves orders hanging. The confirm, adjust, cancel, ready-for-pickup, and
retail-return calls themselves succeed with `202`. Paths are in Appendix A §3.

**`pickup_instructions` is at most 128 characters.**

**Adjustment `merchant_supplied_id` is a string.** A v1 sample shows a number. Postman
sends a string. Send a string.

**Cancellation is bidirectional and both directions are required.** DoorDash-to-partner
cancellations must be surfaced so merchants stop picking; partner-to-DoorDash must be
available inside the merchant's order view.

**Temporary deactivation needs a documented reason and either `end_time` or a positive
duration** (`duration_in_hours` or `duration_in_secs`). The call is
`PUT /marketplace/api/v1/stores/{location_id}/status` and succeeds with `200`.
`is_active` on this call is the store pause flag. The request schema also includes
`notes`, `merchant_supplied_id`, and both duration fields. The `StoreActivationStatus`
model table lists only `is_active`, `reason`, and `notes`. An open-ended deactivation
takes the store offline until someone notices. Merchants lose revenue quietly and blame
the integration.

**Document your failure reasons.** The `failure_reason` values sent on asynchronous
failures get handed to DoorDash support so they can help merchants. Undocumented codes mean
support can't resolve anything without escalating to the partner.

---

### Scheduling

**Stagger the monthly catalog refresh across days.** Firing every business on the first of
the month creates a self-inflicted load spike on both sides.

**Nightly inventory refresh is required even with perfect real-time pushes.** Drift happens.
This is the reconciliation mechanism, and certification verifies the schedule exists.

**Build store hours refresh to the guide, not the checklist.** The certification checklist
asks for monthly; the provider guide expects daily or weekly. Monthly passes the gate and
still leaves merchants with stale hours, which surfaces as customers ordering from closed
stores rather than as an hours bug. Both figures are in Appendix A §2.

**Scheduled refreshes should be scoped to activated stores only.** Enroll a store when
`INTEGRATION_ACTIVATED` arrives. Refreshing stores that never completed onboarding wastes
capacity and muddies error monitoring.

---

### Certification

**Certification is a live 1–2 hour screen-share.** Engineers demonstrate real merchant
behavior in real time. Anything that only works when run by hand from a terminal, or needs
a database tweak first, will be visible. Build the demo path as a real path.

**The test store must receive at least 2 onboarding request calls across 2 days after
certification.** Plan for it rather than being surprised.

**Failed items are certified separately later.** A partial fail doesn't reset the whole
process, but it does add scheduling delay before launch. The cheapest insurance is running
the certification matrix as an automated suite before booking the session.

**A meaningful share of certification is not code.** The onboarding tracker UI, the
out-of-stock button (T4, Recommended), order notifications (T5, Merchant Pick Required,
and only when orders are in scope), the price inflation tool (T6, Recommended), the
operator onboarding tool (T3, Required), the support escalation path (T2, Required), and
five pieces of help-center content (E1–E4 Required; E5 Merchant Pick Required when orders
are in scope). Merchant enablement content is a hard gate: the integration will not
certify without ready-to-publish help-center material. Raise all of this at plan time so
it gets staffed. Recommended items in that list are not certification blockers.



---

# Appendix E — Templates

*(folder version: `references/plan-templates.md`)*

## Templates

**Skill version:** 0.4.2 · **Last verified:** 2026-10-09 · **Maintained by:** NV Partner Integrations
**Questions or corrections:** your DoorDash technical account manager, or posprovidersupport@doordash.com

Three deliverables: the recon report, the implementation plan, and PR bodies. Follow the
structure; adapt the depth to the codebase.

Numeric thresholds, field names, and the contract snapshot procedure referenced by these
templates all live in Appendix A. Don't restate values here — point at it, so the
plan a partner keeps stays correct when the skill is updated.

---

### 1. Recon report

Produced at the end of Phase 1. Keep it short enough to read in one sitting. Every finding
carries a file path as evidence — a claim without evidence gets distrusted, correctly.

```markdown
# DoorDash integration: codebase recon

**Repository:** <name> · **Reviewed:** <date> · **Reviewer:** automated survey, unverified by a human

## Scope confirmed with the team
- Orders integration: IN / OUT of scope
- Fulfillment protocol: MERCHANT_PICK / DASHER_PICK / both
- Verticals: <alcohol | grocery | convenience | retail>
- Target environment: sandbox / staging

## Stack summary
Language(s), frameworks, datastores, queues, schedulers, deployment target. Two or three
sentences, not an inventory.

## Existing channel integrations
The most important finding in this report. If the platform already integrates another
marketplace, describe the abstraction, where it lives, and whether DoorDash fits it or
fights it.

| Channel | Location | Pattern | Reusable for DoorDash? |
|---|---|---|---|

## Capability map

| # | Component | Existing capability | Evidence | Gap | Confidence |
|---|---|---|---|---|---|
| 1 | Auth & credentials | | `path/to/file.ext:120` | | High/Med/Low |
| 2 | Outbound change streams | | | | |
| 3 | Hosted pull endpoints | | | | |
| 4 | Inbound webhook receivers | | | | |
| 5 | Scheduled full refreshes | | | | |
| 6 | Identifier mapping | | | | |
| 7 | Onboarding state machine | | | | |

Confidence means confidence in the *finding*, not in the plan. Mark Low freely.

## Data availability

Whether the fields DoorDash needs exist at all. This drives the two hardest certification
gates.

| DoorDash field | Source in this codebase | Coverage | Notes |
|---|---|---|---|
| Item name | | | |
| Merchant-supplied item ID | | | |
| UPC / GTIN | | | |
| Size | | | |
| Extended size | | | |
| Brand | | | |
| Image | | | |
| Category | | | |
| Age restriction | | | |
| Price | | | |
| Sale price | | | |
| On-hand quantity | | | |
| Last sold timestamp | | | |
| E-commerce visibility flag | | | |
| Item location / aisle | | | |
| Store regular hours | | | |
| Store special hours | | | |
| Store timezone | | | |

Where coverage is unknown from the code alone, say unknown and name the query that would
settle it.

## Scope questions resolved here
For any question the team marked "unknown" at the start: the answer, and the evidence that
settled it.

| Question | Answer | Evidence |
|---|---|---|
| Orders in scope | | |
| Fulfillment protocol | | |

## Critical findings

Numbered, each with the consequence spelled out. These three always get answered:

1. **Item ID uniqueness across merchants** — unique globally / unique per merchant only /
   unknown. If not globally unique, this needs a conversation with the DoorDash technical
   account manager before the build starts.
2. **Availability data reliability** — do merchants maintain stock levels? This decides
   whether availability reads on-hand or infers, and inference has a hard numeric gate.
3. **Largest-catalog single-response feasibility** — the biggest catalog and store
   assortment on the platform, and whether the architecture can stream either in one HTTP
   response inside a request timeout. Paginated pull is prohibited, so this is a constraint,
   not a preference. If the answer is no, it is a plan-level risk with no API workaround.

## Work outside the integration code

Certification items that need product, data, or content work, with a rough size. Name them
now; they are the usual reason launches slip. Give each item its level. Recommended and
Optional are not blockers. Merchant Pick Required applies only when orders are in scope
and a store is `MERCHANT_PICK`.

Also list DoorDash-managed prerequisites already in flight or already blocking, so the plan
starts with a real status rather than a blank table.

## Open questions

Split into questions for the partner's team and questions for the DoorDash technical
account manager.
```

---

### 2. Implementation plan

Produced at the end of Phase 2. This is the artifact engineers review and argue with, so
give them real choices where the codebase permits more than one reasonable approach.

```markdown
# DoorDash Marketplace integration: implementation plan

**Plan version:** 0.1 · **Date:** <date> · **Skill version:** 0.4.2
**Contract snapshot:** `docs/doordash/contract-snapshot-<YYYY-MM-DD>.md`
**Tier:** <1 | 2 | 3> — <what that means for what's in this document>

**Approval:** ____________________  **Date:** __________

> Implementation does not begin until the line above is filled in. If this plan is edited
> after approval, bump the version and re-approve — the approved plan is the edited one.

## 1. Summary
What gets built, what it depends on, and roughly how long — in a paragraph. State plainly
that the estimate is a first pass from a code survey, not a commitment.

## 2. Scope
In scope. Out of scope. Explicitly out of scope and why.

## 3. Architecture

How the seven components land in this codebase. A diagram if it helps; prose if not.

### Decisions

For each real decision, present the options rather than the conclusion alone:

**Decision: <name>**
- Option A — <approach>. Pros. Cons. Fits because <evidence from recon>.
- Option B — <approach>. Pros. Cons.
- **Recommendation:** A, because <reason>.
- **Reversibility:** cheap / expensive to change later.

Decisions worth surfacing this way, when the codebase allows more than one answer:
change detection (outbox vs. CDC vs. polling vs. hooking existing domain events), pull
endpoint hosting (extend an existing service vs. new service), the identifier mapping
store, scheduler choice, per-merchant credential storage, and how integration state is
isolated per tenant.

## 4. Component designs

One subsection each. Per component: what it does, where the code goes, the interfaces it
touches, error handling, idempotency, and how it's tested.

1. Auth and credentials
2. Catalog: push and pull
3. Inventory: push, pull, availability logic
4. Store hours: push and pull. Fulfillment capacity ships here. It has no certification row and is not a blocker. v1 store and menu reads ship here too; they support reconciliation and have no certification row.
5. Onboarding: request, webhook receiver, state machine
6. Scheduled refreshes
7. Orders (if in scope), including ready for pickup. Retail return has no certification row; include it when orders are in scope and keep its `operation_id`.
8. Promotions (optional, P2 and P3). One promotion object.
9. Store pause and resume. Recommended (O16, O17). Does not depend on orders.

## 5. Data mapping

The table that decides whether certification goes well. Every field sent, its source, its
transform, and the null behavior. Do not compress this.

| DoorDash field | Source | Transform | If null | Required? |
|---|---|---|---|---|

Money fields (`base_price`, `sale_price`, promotion `discount_price_off`, adjustment
substitute `price`) are integers in the smallest currency unit. `1699` is $16.99. Do not
send `16.99`.

Call out fields with poor coverage across the merchant base separately — those are data
work, not integration work, and they need their own owner.

## 6. Availability logic

The concrete rule chosen for this platform, and the measured or estimated active-item ratio
it produces across real merchants. If the estimate lands under 70% active or under 1,000
active items per store, say so here — that is a certification blocker found early, which is
the best time to find it.

## 7. Identifiers

The mapping schema, uniqueness guarantees, and how `location_id` stability is enforced.

## 8. Operational decision points

For a system serving many unrelated merchants these are design decisions, not
implementation details. State a choice for each — defaults in brackets are reasonable
starting points, not answers.

| Concern | Decision needed | Default |
|---|---|---|
| Webhook authentication | How inbound DoorDash webhooks are authenticated, and where the token lives | Per-subscription token from the existing secret store |
| Replay protection | Whether a replayed webhook can cause a duplicate side effect | Dedupe on `onboarding_id` + `status`, or order ID + event type, with a TTL |
| Idempotency | Keys on outbound writes and inbound handlers | Natural keys: store + item + change timestamp |
| Event ordering | What happens when a price update arrives before the item it prices, and whether a retried older change can overwrite a newer one | Reject-and-retry rather than best-effort apply; carry a change timestamp or version on every outbound write so a late retry cannot regress newer state |
| Merchant override durability | Whether an explicit merchant out-of-stock action survives a scheduled full refresh | Durable override state that the refresh reads, cleared only by a documented restock rule |
| Queued-write reconciliation | How accepted writes are tracked to a real outcome | Persist `operation_id` whenever the body includes one: inventory and promotion `202`; retail return `202`; job `200`; catalog `201` and `200`. Fulfillment capacity is claimed `202` and has no response sample; keep `operation_id` if present. `202` means accepted, not applied |
| Dead-lettering and replay | Where failed events go and how they're re-driven | Existing DLQ pattern found in recon, with a per-tenant replay path |
| Rate limits and retry budget | Behavior on `429`, and the ceiling on retries per event | Exponential backoff with jitter, capped attempts, then DLQ |
| Batch sizing | Handling `422 Request Entity Too Large` | Read `message` and `field_errors` first. Split and retry only an oversized batch. A `422` that names a field is not fixed by splitting. Never drop items |
| Per-tenant rollout | How the integration is enabled merchant by merchant | Feature flag keyed on merchant, defaulting off |
| Migration safety | Whether schema changes are backward-compatible during rollout | Additive-only migrations; no destructive changes in the same release |
| Rollback | What "off" looks like for one merchant and for all merchants | Flag off stops pushes; pull endpoints stay live |
| Credential rotation | How signing secrets and pull tokens are rotated and revoked | Dual-credential window; rotation runbook |
| Alerting | What pages someone, and at what threshold | Pull endpoint error rate, auth failure rate, stalled onboardings by status |
| Integration health visibility | Whether per-merchant health is observable without a database query | Dashboard keyed on merchant and onboarding status |

## 9. Failure handling

Concrete behavior for the decisions above: what happens when a pull endpoint times out on
the largest merchant, how partner-owned failures reach the support queue versus the
merchant, and what an on-call engineer sees.

## 10. Test plan

- Unit coverage for availability logic and payload mapping
- Contract tests per endpoint against recorded fixtures
- An end-to-end sandbox scenario: onboard a store, build a catalog, create a menu, set
  hours, place a test order
- The certification matrix as an automated suite, run before booking the session

## 11. Certification coverage

Every requirement, the work item covering it, and the owner. Rows with no work item and no
owner are the point of this section.

| # | Requirement | Level | Work item | Owner | Status |
|---|---|---|---|---|---|

## 12. Work this plan does not cover

The UI, data, content, and process items from the recon report, each with its certification
level and a named owner or an explicit "unowned". Recommended and Optional are not
blockers. Be direct: the Required items here are usually the critical path, not the API work.

## 13. DoorDash-managed prerequisites

Work that cannot be done in this repository and blocks work that can. Each row needs an owner
on both sides and a definition of done, or it will be discovered late.

| Prerequisite | Partner action | DoorDash owner | Evidence it's done | Code work it blocks |
|---|---|---|---|---|
| Marketplace integration attached to the Developer Portal org | | TAM | | All API work |
| Sandbox provider type created | | TAM | | Auth PR |
| Credentials issued (secret shown once) | | Self-serve | Secret captured in vault | Auth PR |
| Webhook subscriptions configured per provider type | | Self-serve | | Onboarding + orders PRs |
| Test store provisioned | | TAM | Exact name and address recorded verbatim | Onboarding tests |
| Pull batch limits confirmed for the provider type | | TAM | | Pull endpoint load test |
| Production provider type + re-created subscriptions | | TAM | | Production cutover, out of scope here |
| Certification session booked | | TAM | | — |

The test store row matters more than it looks: for conversion tests the name and address must
be sent character-for-character as provisioned, so capture them verbatim when the store is
created rather than retyping them later.

## 14. Delivery sequence

PRs in dependency order, with what each unlocks.

## 15. Risks

| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|

Always include catalog auto-build rate, availability output, and single-response pull
capacity for the largest merchant. The first two fail late and cost the most to fix; the
third has no API workaround.

## 16. Open questions
Blocking questions first, with who owns each answer.
```

---

### 3. PR body

One per PR in Phase 3. Draft status, sandbox target.

```markdown
## What this does
Two or three sentences.

## Why now
Where this sits in the sequence and what it unblocks.

## Certification requirements advanced
- `I1` — availability inference with 60-day lookback
- `I9` — inventory pull endpoint hosted

Reference the IDs from the certification matrix in this skill so reviewers can trace
coverage.

## Approach
Key decisions, and anything that deviates from the approved plan — with the reason.

## Contract snapshot
Which snapshot this payload was built against, and any deltas it showed against the skill's
canonical values.

## Testing
What was added and how to run it. What was verified against sandbox, if anything.

## Deliberately not included
Scope held back for a later PR, and which one.

## Reviewer attention
Two or three specific things worth a careful look — especially anywhere a convention was
guessed at rather than inferred from existing code.

## Safety
- [ ] No secrets committed; credentials load from <mechanism found in recon>
- [ ] No secret values appear in code, config, tests, fixtures, logs, or this PR description
- [ ] Sandbox provider type only; no production credentials, endpoints, or branches
- [ ] No writes to live merchant data
- [ ] Follows existing repo conventions for errors, logging, and tests
- [ ] Responses that include `operation_id` persist it (inventory and promotion `202`; retail return `202`; job `200`; catalog `201` and `200`)
- [ ] Inbound handlers are idempotent and return `2xx` before doing slow work
- [ ] Inventory availability is `item_availability`; `is_active` appears only on the v1 store-status call
- [ ] No paginated pull job type
```
