> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stateset.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sync Server gRPC Dispatch Flow

> How an order moves from stateset-api to the Sync Server over gRPC — the service contract, the fan-out, partial failure, and what is safe to retry.

# Sync Server gRPC Dispatch Flow

The Sync Server is the fan-out point. `stateset-api` hands it one order; it submits that order to
every downstream system the tenant has configured — StateSet, NetSuite, DCL, Cart.com — and reports
per-target results.

The important consequence, which shapes everything below: **one call can partially succeed.**

## The flow

```
order created in stateset-api
        │
        ├─▶ outbox row written in the same transaction as the order
        ▼
   outbox worker claims the row
        │
        ├─▶ gRPC SubmitOrder ──▶ Sync Server
        │                            ├─ authenticate tenant
        │                            ├─ validate payload
        │                            └─ fan out ──┬─▶ StateSet
        │                                         ├─▶ NetSuite
        │                                         ├─▶ DCL
        │                                         └─▶ Cart.com
        ▼
   mark delivered, or schedule a retry
```

The outbox row is written **in the same transaction** as the order. That's what makes dispatch
at-least-once rather than best-effort: if the process dies after committing the order, the row is
still there to be claimed.

## Service contract

`stateset.sync.v1.OrderIntegrationService`, default port `50051`:

| RPC                        | Purpose                                                        |
| -------------------------- | -------------------------------------------------------------- |
| `SubmitOrder`              | The fan-out entry point — one order to every configured target |
| `CreateNetSuiteSalesOrder` | NetSuite only                                                  |
| `CreateDclBatch`           | DCL only, batched                                              |
| `CreateCartOrder`          | Cart.com only                                                  |

### `SubmitOrderRequest`

| Field            | Type                   | Notes                                              |
| ---------------- | ---------------------- | -------------------------------------------------- |
| `tenant_id`      | `string`               | Required. Resolves the tenant's integration config |
| `stateset_order` | `CreateOrderPayload`   | The canonical order                                |
| `netsuite`       | `NetSuiteOrderPayload` | Optional per-target payload                        |
| `dcl`            | `DclOrderPayload`      | Optional                                           |
| `cart`           | `CartComOrderPayload`  | Optional                                           |

`CreateOrderPayload` carries `shopify_order_id`, `order_number`, `line_items[]`, `created_at`,
`tags[]`, `location_id`, `workflow_id`, and `netsuite_id`. Each line item carries `sku`, `title`,
`quantity`, `price`, `variant_id`, and `shopify_line_item_id`.

### `SubmitOrderResponse`

| Field     | Type                       | Notes                                                 |
| --------- | -------------------------- | ----------------------------------------------------- |
| `success` | `bool`                     | Whether the submission succeeded overall              |
| `result`  | `OrderSubmissionResult`    | Per-target results                                    |
| `errors`  | `repeated ProcessingError` | Per-target failures, each with `target` and `message` |

<Warning>
  **`success` and a non-empty `errors` list are not mutually exclusive.** `OrderSubmissionResult`
  carries a separate result per target — `stateset_order_id`, `netsuite`, `dcl`, `cart` — precisely
  because NetSuite can accept an order in the same call where DCL rejects it.

  Always inspect `errors[]` and the per-target results. Branching on `success` alone will silently
  lose a failed downstream submission.
</Warning>

## Authentication

Credentials go in gRPC **metadata**, not the payload. Two schemes, tried in order:

| Metadata key                         | Scheme           |
| ------------------------------------ | ---------------- |
| `authorization`                      | `Bearer <token>` |
| `x-stateset-api-key`, or `x-api-key` | API key          |

An unparseable bearer token fails `UNAUTHENTICATED`. Keys are matched against the tenant's active
key set, and a successful match updates that key's last-used timestamp asynchronously.

## Status codes and what they mean

The server maps its internal errors onto gRPC statuses deliberately. This mapping *is* the retry
policy:

| Status                | Cause                                                                                  | Retry?                                                  |
| --------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `INVALID_ARGUMENT`    | Validation failure — missing `tenant_id`, absent `created_at`, unparseable DCL payload | **No.** Deterministic; a retry produces the same error  |
| `NOT_FOUND`           | Unknown tenant or record                                                               | **No**                                                  |
| `FAILED_PRECONDITION` | The integration is not configured for this tenant                                      | **No.** Needs a config change, not another attempt      |
| `UNAVAILABLE`         | Upstream integration error                                                             | **Yes**, with backoff — the upstream is down, not wrong |
| `DEADLINE_EXCEEDED`   | Timeout                                                                                | **Yes**, with care — see below                          |
| `INTERNAL`            | Unexpected server-side failure                                                         | Retry once, then escalate                               |

<Note>
  `FAILED_PRECONDITION` is the one worth special handling. It means the tenant has no configuration
  for that integration — retrying forever will never fix it, and it usually indicates onboarding was
  left incomplete rather than a transient fault. Alert on it instead of burying it in a retry queue.
</Note>

## Retrying safely

At-least-once delivery means **duplicates are expected**, and the fan-out makes them asymmetric.

<Warning>
  A `DEADLINE_EXCEEDED` does not tell you whether downstream systems accepted the order. The call may
  have timed out *after* NetSuite created the sales order. A naive retry of the whole `SubmitOrder`
  re-submits to every target, including the ones that already succeeded.
</Warning>

Two things make this tractable:

1. **Retry per target, not per call.** When you have partial results, use the single-target RPCs —
   `CreateNetSuiteSalesOrder`, `CreateDclBatch`, `CreateCartOrder` — to complete only what failed.
2. **Key downstream deduplication on `shopify_order_id`.** It is stable across retries in a way that
   an internal ID generated per attempt is not.

Only mark the outbox row delivered when every configured target has a result. A row marked delivered
on a partially-successful response is an order that is permanently half-integrated with nothing left
to reconcile it.

## Related

* [StateSet API → Sync Server dispatch](/stateset-sync-server-grpc-dispatch)
* [API contract](/stateset-sync-server-api-contract)
* [API basics](/stateset-sync-server-api-basics)
