@spree/docs 0.1.179 → 0.1.181
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-reference/seller-api/authentication.md +109 -0
- package/dist/api-reference/seller-api/errors.md +130 -0
- package/dist/api-reference/seller-api/introduction.md +85 -0
- package/dist/api-reference/store.yaml +1272 -210
- package/dist/developer/core-concepts/companies-and-catalogs.md +81 -0
- package/package.json +1 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Authenticate Seller API requests with JWTs and the seller header"
|
|
3
|
+
sidebarTitle: "Authentication"
|
|
4
|
+
description: "Authenticate Spree Seller API requests with a seller-audience JWT and select which seller to act as using the X-Spree-Seller-Id header."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
The Seller API has exactly one authentication method: a **JWT issued to a signed-in seller**. There is deliberately no secret API key on this branch — a credential that could act as a seller without a seller signing in is exactly what the separate token audience exists to prevent.
|
|
8
|
+
|
|
9
|
+
## Two things every request needs
|
|
10
|
+
|
|
11
|
+
1. `Authorization: Bearer <token>` — the JWT returned by login.
|
|
12
|
+
2. `X-Spree-Seller-Id: <seller_id>` — which seller the signed-in user is acting as.
|
|
13
|
+
|
|
14
|
+
The only authenticated endpoint that does not need the second is `GET /api/v3/seller/me`, because that is what tells the panel which seller to name.
|
|
15
|
+
|
|
16
|
+
## Signing in
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { createSellerClient } from '@spree/seller-sdk'
|
|
20
|
+
|
|
21
|
+
const client = createSellerClient({ baseUrl: 'https://store.example.com' })
|
|
22
|
+
|
|
23
|
+
const { token, user, sellers } = await client.auth.login({
|
|
24
|
+
email: 'seller@example.com',
|
|
25
|
+
password: 'password123',
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The response carries three things: the access token, the team member who signed in, and **every seller this user may act for**.
|
|
30
|
+
|
|
31
|
+
> **NOTE:** The refresh token is **not** in the response body. It is set as an HttpOnly cookie scoped to `/api/v3/seller/auth`, so a session issued for the seller panel cannot be redeemed on any other surface.
|
|
32
|
+
|
|
33
|
+
### Membership is required, not just credentials
|
|
34
|
+
|
|
35
|
+
A marketplace's own staff share the same user class as sellers. Authenticating is therefore not enough: a user who runs no seller is refused with `401`, because issuing a token would hand out an audience its holder can do nothing with.
|
|
36
|
+
|
|
37
|
+
## Choosing a seller
|
|
38
|
+
|
|
39
|
+
A person may run more than one seller — so capability is per seller, and a request that names none has no tenant at all.
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
client.setToken(token)
|
|
43
|
+
client.setSeller(sellers[0].id)
|
|
44
|
+
|
|
45
|
+
// Every subsequent call now carries both headers
|
|
46
|
+
const { data: products } = await client.products.list()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Sending a seller ID the caller has no role on resolves to nothing, so it reads as **"no such seller"** rather than "denied" — which is also what stops the header being used to enumerate the marketplace's sellers.
|
|
50
|
+
|
|
51
|
+
> **WARNING:** A request with a valid token but no `X-Spree-Seller-Id` header — or one naming a seller the caller does not belong to — is rejected with `403`. There is no fallback to a default seller.
|
|
52
|
+
|
|
53
|
+
## Token audiences
|
|
54
|
+
|
|
55
|
+
Every Spree JWT carries an audience, and each surface accepts only its own:
|
|
56
|
+
|
|
57
|
+
| Surface | Audience | Accepted by |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| Storefront | `store_api` | Store API |
|
|
60
|
+
| Back office | `admin_api` | Admin API |
|
|
61
|
+
| Seller panel | `seller_api` | Seller API |
|
|
62
|
+
|
|
63
|
+
An admin token presented to the Seller API is a `401`, and a seller token presented to the Admin API is likewise refused. The refresh endpoint narrows by audience too, so a refresh token minted elsewhere cannot be exchanged for a seller session.
|
|
64
|
+
|
|
65
|
+
## Refreshing a session
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
const { token } = await client.auth.refresh()
|
|
69
|
+
client.setToken(token)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
No request body and no `Authorization` header — the cookie alone authenticates the call, and a fresh refresh cookie is rotated in.
|
|
73
|
+
|
|
74
|
+
Membership is re-checked here, so a user whose last seller role was revoked mid-session is refused at their next refresh rather than continuing until the access token happens to expire.
|
|
75
|
+
|
|
76
|
+
## Signing out
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
await client.auth.logout()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Revokes the refresh token server-side and clears the cookie.
|
|
83
|
+
|
|
84
|
+
## Accepting an invitation
|
|
85
|
+
|
|
86
|
+
Someone invited onto a seller's team arrives through an emailed link carrying an invitation ID and a token. Both acceptance endpoints are unauthenticated — the link **is** the credential.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// Read the invitation to decide what to ask for
|
|
90
|
+
const invitation = await client.auth.lookupInvitation(invitationId, token)
|
|
91
|
+
|
|
92
|
+
// Accept, and land in the panel already signed in
|
|
93
|
+
const { token: jwt, sellers } = await client.auth.acceptInvitation(invitationId, token, {
|
|
94
|
+
password: 'password123',
|
|
95
|
+
password_confirmation: 'password123',
|
|
96
|
+
first_name: 'Robin',
|
|
97
|
+
last_name: 'Ellis',
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The invited email address is never taken from the request — it always comes from the invitation itself, which is what stops the link being redirected to another address.
|
|
102
|
+
|
|
103
|
+
`password` sets a new password when no account exists for that address; when one does, the same field is how the person proves the account is theirs.
|
|
104
|
+
|
|
105
|
+
> **NOTE:** A wrong token is indistinguishable from an unknown invitation: both answer `404`. So does an invitation onto the marketplace's own staff rather than a seller.
|
|
106
|
+
|
|
107
|
+
## Rate limiting
|
|
108
|
+
|
|
109
|
+
Sign-in, refresh, provider discovery, and invitation acceptance are all rate limited, and answer `429` with a `rate_limit_exceeded` code when a client exceeds the window.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Seller API error responses, status codes, and handling"
|
|
3
|
+
sidebarTitle: "Errors"
|
|
4
|
+
description: "Reference for the Spree Seller API error response format, HTTP status codes, and what a 403 versus a 404 means on the seller branch."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
The Seller API uses the same error format as the rest of the Spree v3 API. Every error response carries a machine-readable `code` and a human-readable `message`.
|
|
8
|
+
|
|
9
|
+
## Error response format
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"error": {
|
|
14
|
+
"code": "record_not_found",
|
|
15
|
+
"message": "Product not found"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Validation errors include a `details` field with per-field messages:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"error": {
|
|
25
|
+
"code": "validation_error",
|
|
26
|
+
"message": "Validation failed",
|
|
27
|
+
"details": {
|
|
28
|
+
"name": ["can't be blank"]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Permission errors name the key the caller was missing:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"error": {
|
|
39
|
+
"code": "access_denied",
|
|
40
|
+
"message": "Missing permission: write_products",
|
|
41
|
+
"details": {
|
|
42
|
+
"required_permission": "write_products"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Status codes
|
|
49
|
+
|
|
50
|
+
| Status | Meaning on the seller branch |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `200` | Success |
|
|
53
|
+
| `201` | Resource created |
|
|
54
|
+
| `204` | Success, no body — deletes and logout |
|
|
55
|
+
| `401` | No token, an expired one, or a token minted for another surface |
|
|
56
|
+
| `403` | Authenticated, but no seller named — or the acting seller's role lacks the permission |
|
|
57
|
+
| `404` | The record does not exist **for this seller** |
|
|
58
|
+
| `422` | Validation failed, or a workflow refused the request |
|
|
59
|
+
| `429` | Rate limit exceeded |
|
|
60
|
+
|
|
61
|
+
## 403 versus 404 — the distinction that matters
|
|
62
|
+
|
|
63
|
+
This is the most important thing to understand about the seller branch, because the two codes answer different questions.
|
|
64
|
+
|
|
65
|
+
**`403` means "you have not told me who you are acting as, or you may not do this."** It comes from one of two places:
|
|
66
|
+
|
|
67
|
+
- No `X-Spree-Seller-Id` header, or one naming a seller the caller has no role on.
|
|
68
|
+
- The acting seller's role lacks the permission key the action requires.
|
|
69
|
+
|
|
70
|
+
**`404` means "no such record, for you."** Every lookup is rooted in the acting seller, so an ID belonging to another seller — or to the marketplace operator — is simply not found.
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"error": {
|
|
75
|
+
"code": "record_not_found",
|
|
76
|
+
"message": "Product not found"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This is deliberate. Answering `403` for another seller's product would confirm the record exists, which is how a seller could probe the marketplace's catalog one ID at a time. The seller is never told the difference between "this belongs to someone else" and "this does not exist".
|
|
82
|
+
|
|
83
|
+
## Common error codes
|
|
84
|
+
|
|
85
|
+
| Code | Status | When |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `authentication_failed` | 401 | Bad credentials, or a user who runs no seller |
|
|
88
|
+
| `invalid_refresh_token` | 401 | The refresh cookie is missing, expired, or for another audience |
|
|
89
|
+
| `access_denied` | 403 | No seller named, or a missing permission key |
|
|
90
|
+
| `record_not_found` | 404 | The record does not belong to the acting seller |
|
|
91
|
+
| `validation_error` | 422 | Model validation failed; see `details` |
|
|
92
|
+
| `processing_error` | 422 | A workflow refused — for example, submitting for review with requirements outstanding |
|
|
93
|
+
| `parameter_missing` | 400 | A required parameter was absent |
|
|
94
|
+
| `rate_limit_exceeded` | 429 | Too many requests to an auth endpoint |
|
|
95
|
+
|
|
96
|
+
## Handling errors with the SDK
|
|
97
|
+
|
|
98
|
+
The SDK throws a `SpreeError` carrying the code, status, and details:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { SpreeError } from '@spree/seller-sdk'
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
await client.products.create({ name: '' })
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error instanceof SpreeError) {
|
|
107
|
+
if (error.status === 422) {
|
|
108
|
+
// error.details → { name: ["can't be blank"] }
|
|
109
|
+
showFieldErrors(error.details)
|
|
110
|
+
} else if (error.status === 403) {
|
|
111
|
+
showMessage("You don't have permission to do that.")
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Requirements that block submission
|
|
118
|
+
|
|
119
|
+
Submitting for review with something required still outstanding returns `422` with a message naming what is blocking. The seller's status is unchanged — nothing partial happens.
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"error": {
|
|
124
|
+
"code": "processing_error",
|
|
125
|
+
"message": "Add a billing address"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Read the checklist from `GET /api/v3/seller/onboarding` to see each requirement's `status` and whether it is `blocking`.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Spree Seller API introduction and SDK quick start"
|
|
3
|
+
sidebarTitle: "Introduction"
|
|
4
|
+
description: "Overview of the Spree Seller API — the marketplace seller panel for managing a seller's own catalog, team, stock locations, and onboarding."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
import { Since } from '/snippets/since.mdx';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
The Seller API is a REST API for the **marketplace seller panel** — where a seller runs their own shop inside someone else's marketplace. It powers a seller's catalog, their team, their stock locations, and the onboarding checklist the marketplace asks them to complete.
|
|
11
|
+
|
|
12
|
+
All routes are prefixed with `/api/v3/seller`. During development the API is available under `http://localhost:3000/api/v3/seller`. For production, replace `http://localhost:3000` with your Spree application URL.
|
|
13
|
+
|
|
14
|
+
## A branch of its own, not a narrowing of the Admin API
|
|
15
|
+
|
|
16
|
+
Sellers never call the Admin API. Every Seller API endpoint is scoped server-side to the seller the request acts as, which makes cross-seller access impossible by construction rather than by rule: a product ID belonging to another seller answers `404`, not `403`.
|
|
17
|
+
|
|
18
|
+
The store is **derived from the seller**, never sent alongside it, so no header a seller controls can widen what they reach.
|
|
19
|
+
|
|
20
|
+
| | Seller API | Admin API |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| **Purpose** | Run one seller's shop | Run the whole marketplace |
|
|
23
|
+
| **Audience** | Marketplace sellers and their staff | The marketplace operator's staff |
|
|
24
|
+
| **Authentication** | Seller JWT only (`seller_api` audience) | Secret API key (`sk_…`) or admin JWT |
|
|
25
|
+
| **Tenancy** | Scoped to the acting seller | Scoped to the store |
|
|
26
|
+
| **Server-to-server credential** | None, by design | Secret API keys |
|
|
27
|
+
|
|
28
|
+
The marketplace operator manages sellers — approving them, reviewing what they submit, setting commission — through the Admin API's own [seller endpoints](../admin-api/introduction.md), not through this API.
|
|
29
|
+
|
|
30
|
+
## Using the SDK
|
|
31
|
+
|
|
32
|
+
We recommend `@spree/seller-sdk` for interacting with the Seller API. It provides typed clients, automatic retries, and handles the seller header for you.
|
|
33
|
+
|
|
34
|
+
### Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @spree/seller-sdk
|
|
38
|
+
# or
|
|
39
|
+
yarn add @spree/seller-sdk
|
|
40
|
+
# or
|
|
41
|
+
pnpm add @spree/seller-sdk
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Quick start
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { createSellerClient } from '@spree/seller-sdk'
|
|
48
|
+
|
|
49
|
+
const client = createSellerClient({
|
|
50
|
+
baseUrl: 'http://localhost:3000',
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Sign in, then pick which seller to act as
|
|
54
|
+
const { token, sellers } = await client.auth.login({
|
|
55
|
+
email: 'seller@example.com',
|
|
56
|
+
password: 'password123',
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
client.setToken(token)
|
|
60
|
+
client.setSeller(sellers[0].id)
|
|
61
|
+
|
|
62
|
+
const { data: products } = await client.products.list({ limit: 25 })
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## What a seller can do
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
- **Catalog** — List, create, update, and delete the products they own outright.
|
|
69
|
+
- **Profile** — Maintain presentation, contact details, addresses, and tax registration.
|
|
70
|
+
- **Team** — Invite colleagues, list the team, and revoke access.
|
|
71
|
+
- **Onboarding** — Read the marketplace's checklist and submit against each requirement.
|
|
72
|
+
- **Stock locations** — Manage where they keep stock, and so where returns are sent.
|
|
73
|
+
- **Uploads** — Presign direct uploads for the documents onboarding asks for.
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
## What a seller cannot do
|
|
77
|
+
|
|
78
|
+
Some fields are readable but never writable, because they belong to the marketplace rather than the seller:
|
|
79
|
+
|
|
80
|
+
- **`status`** — the seller lifecycle belongs to the operator's workflows. A seller says they are ready via `POST /api/v3/seller/onboarding/submit_for_review`; the operator decides.
|
|
81
|
+
- **`slug`** — renaming a storefront address would break every link pointing at it.
|
|
82
|
+
- **Settlement and commission terms** — what the marketplace charges is the marketplace's to set.
|
|
83
|
+
- **Tax category, delivery profile, and promotionability on products** — marketplace-wide merchandising settings.
|
|
84
|
+
|
|
85
|
+
Sending these fields is not an error; they are simply ignored.
|