> ## 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 API Contract

> The complete HTTP and gRPC contract for the StateSet Sync Server.

# API Contract

This document captures the runtime contract for the HTTP and gRPC surfaces: versioning, envelopes, error codes, rate limits, pagination, and idempotency expectations.

## Base URLs & Versioning

* HTTP base path: `/v1` (example: `http://localhost:8080/v1/tenants/{tenant_id}/orders`).
* OpenAPI: `/api-docs/openapi.json` and interactive docs at `/docs/` (alias `/swagger-ui/`).
* Version headers: responses include `x-api-version` (current `1.0.0`) and `x-api-supported-versions` (minimum supported version). Requests may optionally send `x-api-version`; unsupported versions return `400`.
* Deprecation: `deprecation`/`sunset` headers are wired in the middleware for future sunset notices (none active now).

## Authentication

* HTTP (tenant): `x-stateset-api-key: <tenant-key>` per request.
* HTTP (operator / dashboard): `x-stateset-admin-key: <platform-admin-key>` is restricted to platform-admin `/v1/admin/*` calls and org-bound operator-safe tenant reads inside the WorkOS-authenticated dashboard proxy.
* gRPC: same key via metadata header `x-stateset-api-key` on port `50051`.

### Webhook Authentication

* Shopify webhook ingress (`/v1/tenants/{tenant_id}/webhooks/shopify`) uses `x-shopify-hmac-sha256`.
* Amazon SNS webhook ingress (`/v1/tenants/{tenant_id}/webhooks/amazon`) uses SNS RSA signature verification.
* TikTok webhook ingress (`/v1/tenants/{tenant_id}/webhooks/tiktok`) uses HMAC SHA-256 signature verification from `x-tts-signature`/`x-tiktok-signature`.
* Walmart webhook ingress (`/v1/tenants/{tenant_id}/webhooks/walmart`) uses HMAC SHA-256 signature verification from `x-walmart-signature`.
* eBay webhook ingress (`/v1/tenants/{tenant_id}/webhooks/ebay`) uses HMAC SHA-256 signature verification from `x-ebay-signature`.
* Etsy webhook ingress (`/v1/tenants/{tenant_id}/webhooks/etsy`) uses HMAC SHA-256 signature verification from `x-etsy-signature`.
* WooCommerce webhook ingress (`/v1/tenants/{tenant_id}/webhooks/woocommerce`) uses HMAC SHA-256 signature verification from `x-wc-webhook-signature`.
* ACP webhook ingress (`/v1/tenants/{tenant_id}/webhooks/acp/orders`) uses HMAC SHA-256 with:
  * `x-acp-timestamp`: Unix seconds/milliseconds or RFC3339 timestamp
  * `x-acp-signature`: HMAC of `"{timestamp}.{raw_body}"` (hex or base64; `v1=`/`sha256=` prefixes supported)
* Shopify secret is read from `{TENANT_ID}_SHOPIFY_WEBHOOK_SECRET` (for tenant `acme`: `ACME_SHOPIFY_WEBHOOK_SECRET`).
* TikTok secret is read from `{TENANT_ID}_TIKTOK_WEBHOOK_SECRET` (for tenant `acme`: `ACME_TIKTOK_WEBHOOK_SECRET`).
* Walmart secret is read from `{TENANT_ID}_WALMART_WEBHOOK_SECRET`.
* eBay secret is read from `{TENANT_ID}_EBAY_WEBHOOK_SECRET`.
* Etsy secret is read from `{TENANT_ID}_ETSY_WEBHOOK_SECRET`.
* WooCommerce secret is read from `{TENANT_ID}_WOOCOMMERCE_WEBHOOK_SECRET`.
* ACP secret is read from `{TENANT_ID}_ACP_WEBHOOK_SECRET` (for tenant `acme`: `ACME_ACP_WEBHOOK_SECRET`).
* Unsigned ACP webhooks are rejected unless `STATESET_ALLOW_UNSIGNED_ACP_WEBHOOKS=true`.

## Response Envelopes & Correlation

* Success shape:

```json theme={null}
{
  "meta": { "requestId": "da02c39f-2d9b-4e3c-8aa2-6c4f3bda9d54" },
  "data": { "statesetOrderId": "c2c2d8e1-9c05-4c0a-83b7-bc3a66dc7b2f" }
}
```

* Error shape:

```json theme={null}
{
  "meta": { "requestId": "da02c39f-2d9b-4e3c-8aa2-6c4f3bda9d54" },
  "error": {
    "code": "validation_failed",
    "message": "shopifyOrderId is required",
    "details": {
      "cause": "timeout"
    }
  }
}
```

* Every response also carries `x-request-id`; it matches `meta.requestId` and can be used for support tickets and trace correlation.

## Error Codes

| HTTP | code                            | When it happens                                                          |
| ---- | ------------------------------- | ------------------------------------------------------------------------ |
| 400  | `integration_not_configured`    | Optional integrations (Amazon/TikTok/etc.) are disabled for the tenant.  |
| 404  | `tenant_not_found`, `not_found` | Tenant or resource not present.                                          |
| 422  | `validation_failed`             | Payload validation failed (field-level messages in `message`/`details`). |
| 424  | `integration_missing`           | Tenant lacks the requested integration.                                  |
| 429  | `rate_limit_exceeded`           | Global/IP rate limit hit (see headers and `details.retryAfterSeconds`).  |
| 502  | `upstream_error`                | Downstream (NetSuite/Shopify/DCL/Cart.com/etc.) returned an error.       |
| 500  | `internal_error`                | Unhandled or unexpected error.                                           |

## Rate Limiting

* Keyed by client IP via `tower_governor`. Bucket: burst `500`, refill one token every `100ms` (\~10 requests/second sustained).
* On `429` responses:
  * Headers: `x-ratelimit-limit`, `x-ratelimit-remaining`, `x-ratelimit-after`, `Retry-After` (seconds), `x-request-id`.
  * Body: standard error envelope with `code=rate_limit_exceeded` and `details.retryAfterSeconds`.
* Per-tenant concurrency is additionally shaped by semaphores for each integration to keep within vendor quotas.

## Idempotency & Retries

* `POST /v1/tenants/{tenant_id}/orders` is idempotent on `shopify_order_id`; if the order already exists in StateSet the existing `stateset_order_id` is returned without creating a duplicate.
* `POST /v1/tenants/{tenant_id}/orders/batch` accepts up to `200` orders and returns per-item success/failure details; set `stopOnError=true` to fail-fast (otherwise it continues through all entries).
* Batch imports support preview mode with `dryRun=true` (request body) or `?dry_run=true` (query param). In dry-run mode the API validates each item, reports `action` (`would_create` / `would_reuse_existing` / `failed`), and performs zero writes.
* `POST /v1/tenants/{tenant_id}/orders/backfill/shopify/{shopify_order_id}` is an operator repair endpoint: it fetches the order snapshot from Shopify and persists it locally when missing. It returns `201` when it inserted a new row and `200` when the order already existed locally.
* All write endpoints (`POST`/`PUT`/`PATCH`/`DELETE`) also accept an `idempotency-key` header. Successful responses are cached for 24h and replayed on retry with `idempotency-replayed: true`; if a matching request is still running, the server returns `409 operation_in_progress`. When PostgreSQL is enabled, idempotency state is shared across replicas; otherwise it falls back to process-local memory.
* Sync triggers (`/sync/*`) enqueue background jobs; use `/v1/tenants/{tenant_id}/jobs/{job_id}` to poll instead of holding the original request open. Triggering twice will create two jobs, so prefer client-side de-duplication when retrying.
* `GET /v1/tenants/{tenant_id}/jobs/throughput` returns time-bucketed job analytics (created/finished/succeeded/failed) for dashboard charts. Query params: `hours` (default `24`) and `bucket_minutes` (default `60`).
* Downstream calls use exponential backoff (see `src/resilience.rs`) and classify retryable upstream errors (timeouts, 5xx, 429) automatically.
* Webhook operator tooling supports both single replay (`POST /v1/tenants/{tenant_id}/webhook-events/{webhook_id}/replay`) and bulk replay (`POST /v1/tenants/{tenant_id}/webhook-events/replay?state=failed&limit=50`).
* Bulk replay supports additional safety controls:
  * exact topic filtering via `topic`
  * preview mode via `dry_run=true` (returns selected ids without queueing)
  * source-level throttling via `max_per_source=<n>`
* Targeted replay is available via `POST /v1/tenants/{tenant_id}/webhook-events/replay-by-id` with JSON body (`webhookIds`, optional `dryRun`).

## Auditability

* Bulk and operator repair actions emit `data_access` audit events (resource types: `orders_batch_import`, `orders_batch_import_preview`, `orders_backfill_shopify`, `webhook_events_bulk_replay`, `webhook_event_replay`).
* Audit entries are best-effort persisted to the database (when configured) and always logged to structured application logs.
* `x-stateset-user-email` and `user-agent` headers are captured when present for operator attribution.
* Admin audit queries support `resource_type` filtering: `GET /v1/admin/audit-logs?event_type=data_access&resource_type=orders_batch_import`.

## Admin Onboarding Readiness

* `GET /v1/admin/tenants/{tenant_id}/readiness` returns a scored onboarding report for customer go-live checks.
* `GET /v1/admin/tenants/{tenant_id}/onboarding-plan` returns prioritized action items derived from the readiness report.
* `PATCH /v1/admin/tenants/{tenant_id}/onboarding-plan/actions/{action_id}` persists operator workflow state for each action (`pending`, `in_progress`, `done`, `blocked`) with optional notes.
* `PATCH /v1/admin/tenants/{tenant_id}/onboarding-plan/actions` supports bulk workflow updates for multiple actions in a single request.
* `GET /v1/admin/tenants/{tenant_id}/onboarding-plan/actions/history` returns append-only action history with actor/timestamp context.
* `PATCH /v1/admin/tenants/{tenant_id}/onboarding-profile` upserts tenant onboarding metadata (`ownerEmail`, `ownerName`, `goLiveTargetAt`, `notes`).
* `GET /v1/admin/onboarding/portfolio` returns a cross-tenant onboarding pipeline summary (scores, risk, blockers, workflow counts, next action, owner/go-live metadata). Supports optional filters via `readinessStatus`, `integration`, `hasBlockers`, `minRiskScore`, `minBlockedActionAgeHours`, `ownerAssigned`, and `overdueGoLive`; sorting via `sortBy` (`risk|score|tenant_id`) + `sortOrder` (`asc|desc`); includes summary counters/thresholds for `highRiskTenants`, `staleBlockerTenants`, `unassignedOwnerTenants`, and `overdueGoLiveTenants`; use `includeIntegrationTests=true` to run live health checks in the aggregation response.
* The response includes:
  * `score` (0-100) and `grade` (`A+`..`F`)
  * `overallStatus` (`ready`, `attention_required`, `not_ready`)
  * required check results (tenant runtime, integration config, channel/destination coverage, auth readiness, Shopify webhook secret, connectivity)
  * per-integration connectivity test results
  * guide readiness for Shopify -> NetSuite and Shopify -> DCL with missing requirements

## Pagination & Filtering

* `GET /v1/tenants/{tenant_id}/orders` supports `page` (default `1`), `page_size` (default `50`, max `100`), `status`, `created_after`, `created_before`, `order_number`, `shopify_order_id`, `tags`.
* Response uses `PaginatedResponse<T>`: `items` plus `pagination` metadata (`page`, `pageSize`, `totalItems`, `totalPages`, `hasNext`, `hasPrevious`).

Example:

```bash theme={null}
curl -H "x-stateset-api-key: <key>" \
  "http://localhost:8080/v1/tenants/acme/orders?page=1&page_size=50&status=fulfilled"
```

## Examples

* Create order (idempotent on Shopify order id):

```bash theme={null}
curl -X POST "http://localhost:8080/v1/tenants/acme/orders" \
  -H "content-type: application/json" \
  -H "x-stateset-api-key: <key>" \
  -d '{
        "shopifyOrderId": "1234567890",
        "orderNumber": "M-1001",
        "createdAt": "2024-01-01T00:00:00Z",
        "tags": ["vip"],
        "lineItems": [{"title": "Widget", "quantity": 1, "price": 42.0}]
      }'
```

* Create orders in batch (up to 200 per request):

```bash theme={null}
curl -X POST "http://localhost:8080/v1/tenants/acme/orders/batch" \
  -H "content-type: application/json" \
  -H "x-stateset-api-key: <key>" \
  -d '{
        "stopOnError": false,
        "orders": [
          {
            "shopifyOrderId": "1234567890",
            "orderNumber": "M-1001",
            "createdAt": "2024-01-01T00:00:00Z",
            "tags": ["vip"],
            "lineItems": [{"title": "Widget", "quantity": 1, "price": 42.0}]
          },
          {
            "shopifyOrderId": "1234567891",
            "orderNumber": "M-1002",
            "createdAt": "2024-01-01T00:00:00Z",
            "lineItems": [{"title": "Widget", "quantity": 2, "price": 42.0}]
          }
        ]
      }'
```

* Error response (validation): `422` with body shown above and `x-request-id` for correlation.

## gRPC quick start

```bash theme={null}
grpcurl -plaintext \
  -H "x-stateset-api-key: <key>" \
  -d '{"tenant_id": "acme", "stateset_order": {"shopify_order_id": "123", "order_number": "M-1001", "line_items": [{"title": "Widget", "quantity": 1, "price": 42.0}]}}' \
  localhost:50051 stateset.sync.v1.OrderIntegrationService/SubmitOrder
```
