@picked/client 0.0.1-dev.1777616114652 → 0.0.1-dev.1777867216429
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-Dn57QBPm.js → api-Xp22knXn.js} +1 -1
- package/dist/{api-BryLRI2Y.cjs → api-qBG8rsTJ.cjs} +1 -1
- package/dist/cjs/form/index.js +1 -1
- package/dist/cjs/index.js +2 -2
- package/dist/cjs/plugins/create-experience/index.js +1 -1
- package/dist/cjs/plugins/dynamic-experiences/index.js +1 -1
- package/dist/cjs/plugins/experience-browser-storage/index.js +1 -1
- package/dist/cjs/plugins/history-browser-storage/index.js +1 -1
- package/dist/cjs/plugins/responses-browser-storage/index.js +1 -1
- package/dist/cjs/server/index.js +1 -1
- package/dist/docs/api/authentication.md +376 -0
- package/dist/docs/api/authorization-and-scopes.md +262 -0
- package/dist/docs/api/error-handling.md +211 -0
- package/dist/docs/api/index.md +172 -11
- package/dist/docs/api/pagination.md +176 -0
- package/dist/docs/image-optimisation-service.md +72 -0
- package/dist/docs/sdk/plugins/dynamic-experiences.md +2 -2
- package/dist/docs/workspaces.mdx +60 -1
- package/dist/esm/form/index.js +40 -33
- package/dist/esm/index.js +295 -170
- package/dist/esm/plugins/create-experience/index.js +1 -1
- package/dist/esm/plugins/dynamic-experiences/index.js +1 -1
- package/dist/esm/plugins/experience-browser-storage/index.js +1 -1
- package/dist/esm/plugins/history-browser-storage/index.js +1 -1
- package/dist/esm/plugins/responses-browser-storage/index.js +1 -1
- package/dist/esm/server/index.js +1 -1
- package/dist/experience-instance-BWniatU5.cjs +1 -0
- package/dist/experience-instance-C4UJM092.js +506 -0
- package/dist/types/entities/experience-instance/experience-instance.d.ts.map +1 -1
- package/dist/types/entities/history/history.d.ts +9 -0
- package/dist/types/entities/history/history.d.ts.map +1 -1
- package/dist/types/form/create-form.d.ts +55 -0
- package/dist/types/form/create-form.d.ts.map +1 -1
- package/package.json +1 -11
- package/dist/cjs/plugins/dom-observer/index.js +0 -1
- package/dist/docs/reference/index.md +0 -11
- package/dist/docs/tooling/index.md +0 -19
- package/dist/esm/plugins/dom-observer/index.js +0 -81
- package/dist/experience-BhteyYBl.cjs +0 -1
- package/dist/experience-CoMdlvyn.js +0 -136
- package/dist/experience-instance-CZDcicPj.cjs +0 -1
- package/dist/experience-instance-IkIw31Cg.js +0 -387
- package/dist/nodes-ChVU1VZO.js +0 -101
- package/dist/nodes-DDDs-rC2.cjs +0 -1
- package/dist/types/plugins/dom-observer/index.d.ts +0 -3
- package/dist/types/plugins/dom-observer/index.d.ts.map +0 -1
- package/dist/types/plugins/dom-observer/plugin.d.ts +0 -39
- package/dist/types/plugins/dom-observer/plugin.d.ts.map +0 -1
- /package/dist/docs/{tooling/chrome-extension.md → chrome-extension.md} +0 -0
- /package/dist/docs/{tooling/gtm-template.md → gtm-template.md} +0 -0
- /package/dist/docs/{tooling/mcp.md → mcp.md} +0 -0
- /package/dist/docs/{tooling/vite-plugin.md → vite-plugin.md} +0 -0
- /package/dist/{id-Bbodazn8.cjs → id-Bv44ilEo.cjs} +0 -0
- /package/dist/{id-B-N2zNbS.js → id-CRqy981z.js} +0 -0
- /package/dist/{storage-hOHCCXvP.js → storage-6Xk0uAxB.js} +0 -0
- /package/dist/{storage-BXSlLTRa.cjs → storage-B6lQtc4-.cjs} +0 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: docs
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Authorization and scopes
|
|
6
|
+
|
|
7
|
+
The Picked API uses a scope-based authorization model. Every authenticated caller (session user or API key) carries a set of scopes that determine which operations they can perform. This guide explains the scope system, roles, and how permissions are resolved.
|
|
8
|
+
|
|
9
|
+
## Scope naming format
|
|
10
|
+
|
|
11
|
+
Scopes follow the `entity/action` convention:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
organization/read
|
|
15
|
+
organization_members/create
|
|
16
|
+
organization_projects/delete
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- **entity** — a lowercase name using underscores for multi-word entities (e.g., `organization_members`, `organization_api_keys`)
|
|
20
|
+
- **action** — one of `create`, `read`, `update`, or `delete`
|
|
21
|
+
|
|
22
|
+
In the GraphQL schema, scopes are represented as the `Scope` enum using uppercase with underscores (e.g., `ORGANIZATION_READ`, `ORGANIZATION_MEMBERS_CREATE`). Both formats refer to the same scope.
|
|
23
|
+
|
|
24
|
+
## Full scope list
|
|
25
|
+
|
|
26
|
+
The following scopes are available:
|
|
27
|
+
|
|
28
|
+
### Organization
|
|
29
|
+
|
|
30
|
+
| Scope | GraphQL Enum | Description |
|
|
31
|
+
|-------|-------------|-------------|
|
|
32
|
+
| `organization/create` | `ORGANIZATION_CREATE` | Create new organizations |
|
|
33
|
+
| `organization/read` | `ORGANIZATION_READ` | View organization details |
|
|
34
|
+
| `organization/update` | `ORGANIZATION_UPDATE` | Update organization name, slug, and settings |
|
|
35
|
+
| `organization/delete` | `ORGANIZATION_DELETE` | Delete an organization |
|
|
36
|
+
|
|
37
|
+
### Organization members
|
|
38
|
+
|
|
39
|
+
| Scope | GraphQL Enum | Description |
|
|
40
|
+
|-------|-------------|-------------|
|
|
41
|
+
| `organization_members/create` | `ORGANIZATION_MEMBERS_CREATE` | Add members to an organization |
|
|
42
|
+
| `organization_members/read` | `ORGANIZATION_MEMBERS_READ` | View organization member list |
|
|
43
|
+
| `organization_members/update` | `ORGANIZATION_MEMBERS_UPDATE` | Update member roles |
|
|
44
|
+
| `organization_members/delete` | `ORGANIZATION_MEMBERS_DELETE` | Remove members from an organization |
|
|
45
|
+
|
|
46
|
+
### Organization invitations
|
|
47
|
+
|
|
48
|
+
| Scope | GraphQL Enum | Description |
|
|
49
|
+
|-------|-------------|-------------|
|
|
50
|
+
| `organization_invitations/create` | `ORGANIZATION_INVITATIONS_CREATE` | Send invitations to join an organization |
|
|
51
|
+
| `organization_invitations/read` | `ORGANIZATION_INVITATIONS_READ` | View pending invitations |
|
|
52
|
+
| `organization_invitations/delete` | `ORGANIZATION_INVITATIONS_DELETE` | Revoke pending invitations |
|
|
53
|
+
|
|
54
|
+
### Organization projects
|
|
55
|
+
|
|
56
|
+
| Scope | GraphQL Enum | Description |
|
|
57
|
+
|-------|-------------|-------------|
|
|
58
|
+
| `organization_projects/create` | `ORGANIZATION_PROJECTS_CREATE` | Create new projects |
|
|
59
|
+
| `organization_projects/read` | `ORGANIZATION_PROJECTS_READ` | View project details |
|
|
60
|
+
| `organization_projects/update` | `ORGANIZATION_PROJECTS_UPDATE` | Update project settings |
|
|
61
|
+
| `organization_projects/delete` | `ORGANIZATION_PROJECTS_DELETE` | Delete projects |
|
|
62
|
+
|
|
63
|
+
### Organization API keys
|
|
64
|
+
|
|
65
|
+
| Scope | GraphQL Enum | Description |
|
|
66
|
+
|-------|-------------|-------------|
|
|
67
|
+
| `organization_api_keys/create` | `ORGANIZATION_API_KEYS_CREATE` | Create new API keys |
|
|
68
|
+
| `organization_api_keys/read` | `ORGANIZATION_API_KEYS_READ` | View API keys (prefix only, not the full key) |
|
|
69
|
+
| `organization_api_keys/delete` | `ORGANIZATION_API_KEYS_DELETE` | Revoke API keys |
|
|
70
|
+
|
|
71
|
+
### Organization integrations
|
|
72
|
+
|
|
73
|
+
| Scope | GraphQL Enum | Description |
|
|
74
|
+
|-------|-------------|-------------|
|
|
75
|
+
| `organization_integrations/create` | `ORGANIZATION_INTEGRATIONS_CREATE` | Set up new integrations |
|
|
76
|
+
| `organization_integrations/read` | `ORGANIZATION_INTEGRATIONS_READ` | View integration configurations |
|
|
77
|
+
| `organization_integrations/update` | `ORGANIZATION_INTEGRATIONS_UPDATE` | Modify integration settings |
|
|
78
|
+
| `organization_integrations/delete` | `ORGANIZATION_INTEGRATIONS_DELETE` | Remove integrations |
|
|
79
|
+
|
|
80
|
+
### Organization reports
|
|
81
|
+
|
|
82
|
+
| Scope | GraphQL Enum | Description |
|
|
83
|
+
|-------|-------------|-------------|
|
|
84
|
+
| `organization_reports/create` | `ORGANIZATION_REPORTS_CREATE` | Create new reports |
|
|
85
|
+
| `organization_reports/read` | `ORGANIZATION_REPORTS_READ` | View reports |
|
|
86
|
+
| `organization_reports/update` | `ORGANIZATION_REPORTS_UPDATE` | Update report settings |
|
|
87
|
+
| `organization_reports/delete` | `ORGANIZATION_REPORTS_DELETE` | Delete reports |
|
|
88
|
+
|
|
89
|
+
## Roles
|
|
90
|
+
|
|
91
|
+
Organization members are assigned one of two roles:
|
|
92
|
+
|
|
93
|
+
### Owner
|
|
94
|
+
|
|
95
|
+
Owners have full control within their organization. Their default scopes include:
|
|
96
|
+
|
|
97
|
+
- All `organization/*` scopes (`read`, `update`, `delete`)
|
|
98
|
+
- All `organization_members/*` scopes
|
|
99
|
+
- All `organization_invitations/*` scopes
|
|
100
|
+
- `organization_projects/read`, `organization_projects/update`, `organization_projects/delete`
|
|
101
|
+
- All `organization_api_keys/*` scopes
|
|
102
|
+
- All `organization_integrations/*` scopes
|
|
103
|
+
- `organization_reports/read`
|
|
104
|
+
|
|
105
|
+
### Member
|
|
106
|
+
|
|
107
|
+
Members have broad access but cannot delete the organization or manage other members. Their default scopes include:
|
|
108
|
+
|
|
109
|
+
- `organization/read`, `organization/update`
|
|
110
|
+
- `organization_members/read`
|
|
111
|
+
- `organization_invitations/read`
|
|
112
|
+
- `organization_projects/read`, `organization_projects/update`, `organization_projects/delete`
|
|
113
|
+
- All `organization_api_keys/*` scopes
|
|
114
|
+
- All `organization_integrations/*` scopes
|
|
115
|
+
- `organization_reports/read`
|
|
116
|
+
|
|
117
|
+
The key differences between owner and member:
|
|
118
|
+
|
|
119
|
+
| Capability | Owner | Member |
|
|
120
|
+
|-----------|-------|--------|
|
|
121
|
+
| Delete the organization | Yes | No |
|
|
122
|
+
| Invite new members | Yes | No |
|
|
123
|
+
| Remove members | Yes | No |
|
|
124
|
+
| Update member roles | Yes | No |
|
|
125
|
+
| Revoke invitations | Yes | No |
|
|
126
|
+
| Manage projects and integrations | Yes | Yes |
|
|
127
|
+
| Manage API keys | Yes | Yes |
|
|
128
|
+
| View reports | Yes | Yes |
|
|
129
|
+
|
|
130
|
+
## How scopes are resolved
|
|
131
|
+
|
|
132
|
+
When an authenticated request reaches the API, the authorization system resolves scopes as follows:
|
|
133
|
+
|
|
134
|
+
1. **Session-authenticated users** — the API looks up the caller's role within the target organization and assigns the default scopes for that role.
|
|
135
|
+
2. **API key requests** — the API uses the scopes that were assigned to the key at creation time. These scopes are always a subset of the creating user's scopes.
|
|
136
|
+
|
|
137
|
+
For operations that target a specific organization (most queries and mutations), the caller must have the required scope within that organization. For root-level operations like `organizationCreate`, the scope is resolved at the account level.
|
|
138
|
+
|
|
139
|
+
If the caller lacks the required scope, the API returns an authorization error with a `FORBIDDEN` code. See [Error Handling](./error-handling.md) for details.
|
|
140
|
+
|
|
141
|
+
## Operations and required scopes
|
|
142
|
+
|
|
143
|
+
The following table maps common operations to the scopes they require:
|
|
144
|
+
|
|
145
|
+
### Queries
|
|
146
|
+
|
|
147
|
+
| Query | Required Scope |
|
|
148
|
+
|-------|---------------|
|
|
149
|
+
| `organization` | `organization/read` |
|
|
150
|
+
| `organizations` | `organization/read` |
|
|
151
|
+
| `project` | `organization_projects/read` |
|
|
152
|
+
| `apiKeys` | `organization_api_keys/read` |
|
|
153
|
+
| `shopifyWebhooks` | `organization_integrations/read` |
|
|
154
|
+
| `shopifyMetafieldConfigs` | `organization_integrations/read` |
|
|
155
|
+
| `shopifyProduct` / `shopifyProducts` | `organization_integrations/read` |
|
|
156
|
+
| `report` | `organization_reports/read` |
|
|
157
|
+
|
|
158
|
+
### Mutations
|
|
159
|
+
|
|
160
|
+
| Mutation | Required Scope |
|
|
161
|
+
|----------|---------------|
|
|
162
|
+
| `organizationCreate` | `organization/create` |
|
|
163
|
+
| `organizationUpdate` | `organization/update` |
|
|
164
|
+
| `organizationDelete` | `organization/delete` |
|
|
165
|
+
| `organizationMemberInvite` | `organization_invitations/create` |
|
|
166
|
+
| `organizationInvitationRevoke` | `organization_invitations/delete` |
|
|
167
|
+
| `organizationMemberRemove` | `organization_members/delete` |
|
|
168
|
+
| `organizationMemberRoleUpdate` | `organization_members/update` |
|
|
169
|
+
| `projectCreate` | `organization_projects/create` |
|
|
170
|
+
| `apiKeyCreate` | `organization_api_keys/create` |
|
|
171
|
+
| `apiKeyRevoke` | `organization_api_keys/delete` |
|
|
172
|
+
| `shopifyWebhookCreate` | `organization_integrations/create` |
|
|
173
|
+
| `shopifyWebhookUpdate` | `organization_integrations/update` |
|
|
174
|
+
| `shopifyWebhookDelete` | `organization_integrations/delete` |
|
|
175
|
+
| `shopifyMetafieldConfigCreate` | `organization_integrations/create` |
|
|
176
|
+
| `shopifyMetafieldConfigUpdate` | `organization_integrations/update` |
|
|
177
|
+
| `shopifyMetafieldConfigDelete` | `organization_integrations/delete` |
|
|
178
|
+
| `reportCreate` | `organization_reports/create` |
|
|
179
|
+
| `reportUpdate` | `organization_reports/update` |
|
|
180
|
+
| `reportDelete` | `organization_reports/delete` |
|
|
181
|
+
|
|
182
|
+
Some mutations do not require organization-level scopes because they operate on the caller's own data:
|
|
183
|
+
|
|
184
|
+
| Mutation | Requirement |
|
|
185
|
+
|----------|------------|
|
|
186
|
+
| `sessionCreate` / `sessionRefresh` | No authentication required |
|
|
187
|
+
| `accountCreate` / `accountVerifyEmail` | No authentication required |
|
|
188
|
+
| `oauthAuthorize` / `oauthCallback` | No authentication required |
|
|
189
|
+
| `magicLinkSend` / `magicLinkVerify` | No authentication required |
|
|
190
|
+
| `passwordResetSend` / `passwordReset` | No authentication required |
|
|
191
|
+
| `passwordUpdate` | Authenticated session (no specific scope) |
|
|
192
|
+
| `organizationInvitationAccept` / `organizationInvitationDecline` | Authenticated session (no specific scope) |
|
|
193
|
+
| `mfaFactorEnroll` / `mfaChallengeSend` / `mfaChallengeVerify` / `mfaFactorUnenroll` | Authenticated session (no specific scope) |
|
|
194
|
+
|
|
195
|
+
## Querying scopes
|
|
196
|
+
|
|
197
|
+
### Root-level scopes
|
|
198
|
+
|
|
199
|
+
The `viewerScopes` query returns the root-level scopes for the authenticated caller:
|
|
200
|
+
|
|
201
|
+
```graphql
|
|
202
|
+
query {
|
|
203
|
+
viewerScopes
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Response:
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{
|
|
211
|
+
"data": {
|
|
212
|
+
"viewerScopes": [
|
|
213
|
+
"ORGANIZATION_CREATE"
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Organization-level scopes
|
|
220
|
+
|
|
221
|
+
Each `Organization` object exposes a `viewerScopes` field that returns the scopes the caller has within that specific organization:
|
|
222
|
+
|
|
223
|
+
```graphql
|
|
224
|
+
query {
|
|
225
|
+
organization(input: { slug: "my-company" }) {
|
|
226
|
+
id
|
|
227
|
+
name
|
|
228
|
+
viewerScopes
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Response:
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
{
|
|
237
|
+
"data": {
|
|
238
|
+
"organization": {
|
|
239
|
+
"id": "org_abc123",
|
|
240
|
+
"name": "My Company",
|
|
241
|
+
"viewerScopes": [
|
|
242
|
+
"ORGANIZATION_READ",
|
|
243
|
+
"ORGANIZATION_UPDATE",
|
|
244
|
+
"ORGANIZATION_MEMBERS_READ",
|
|
245
|
+
"ORGANIZATION_PROJECTS_READ",
|
|
246
|
+
"ORGANIZATION_PROJECTS_UPDATE",
|
|
247
|
+
"ORGANIZATION_PROJECTS_DELETE",
|
|
248
|
+
"ORGANIZATION_API_KEYS_CREATE",
|
|
249
|
+
"ORGANIZATION_API_KEYS_READ",
|
|
250
|
+
"ORGANIZATION_API_KEYS_DELETE",
|
|
251
|
+
"ORGANIZATION_INTEGRATIONS_CREATE",
|
|
252
|
+
"ORGANIZATION_INTEGRATIONS_READ",
|
|
253
|
+
"ORGANIZATION_INTEGRATIONS_UPDATE",
|
|
254
|
+
"ORGANIZATION_INTEGRATIONS_DELETE",
|
|
255
|
+
"ORGANIZATION_REPORTS_READ"
|
|
256
|
+
]
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
This is useful for building UIs that adapt based on the caller's permissions — for example, hiding a "Delete Organization" button when the caller lacks `ORGANIZATION_DELETE`.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: docs
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Error handling
|
|
6
|
+
|
|
7
|
+
The Picked API returns errors using the standard GraphQL error format. This guide covers the error response shape, all error codes, and practical strategies for handling errors.
|
|
8
|
+
|
|
9
|
+
## Error response shape
|
|
10
|
+
|
|
11
|
+
When an operation fails, the API returns an `errors` array in the response body. Each error object contains:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"data": null,
|
|
16
|
+
"errors": [
|
|
17
|
+
{
|
|
18
|
+
"message": "A human-readable description of the error",
|
|
19
|
+
"path": ["mutationName"],
|
|
20
|
+
"extensions": {
|
|
21
|
+
"code": "ERROR_CODE"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Field | Description |
|
|
29
|
+
|-------|-------------|
|
|
30
|
+
| `message` | A human-readable error description. Suitable for logging but not for programmatic handling. |
|
|
31
|
+
| `path` | The path to the field in the query that caused the error. |
|
|
32
|
+
| `extensions.code` | A machine-readable error code. Use this for programmatic error handling. |
|
|
33
|
+
|
|
34
|
+
Multiple errors may appear in a single response. The `data` field may be `null` if the entire operation failed, or it may contain partial data if only some fields failed.
|
|
35
|
+
|
|
36
|
+
## Authentication error codes
|
|
37
|
+
|
|
38
|
+
These errors relate to session creation, account management, OAuth, magic links, password operations, and MFA. They map to the `AuthErrorCode` enum in the schema.
|
|
39
|
+
|
|
40
|
+
| Code | Description |
|
|
41
|
+
|------|-------------|
|
|
42
|
+
| `INVALID_CREDENTIALS` | The email/password combination is incorrect. |
|
|
43
|
+
| `EMAIL_NOT_CONFIRMED` | The account exists but the email address has not been verified. See [Authentication](/api/authentication#email-verification). |
|
|
44
|
+
| `USER_NOT_FOUND` | No account exists with the provided email address. |
|
|
45
|
+
| `USER_ALREADY_EXISTS` | An account with this email already exists. Returned during account creation. |
|
|
46
|
+
| `WEAK_PASSWORD` | The provided password does not meet strength requirements. |
|
|
47
|
+
| `INVALID_TOKEN` | The token (verification, reset, or magic link) is invalid or malformed. |
|
|
48
|
+
| `TOKEN_EXPIRED` | The token has expired. The caller should request a new one. |
|
|
49
|
+
| `MFA_CHALLENGE_FAILED` | The MFA verification code is incorrect. |
|
|
50
|
+
| `MFA_FACTOR_NOT_FOUND` | The specified MFA factor does not exist or does not belong to the caller. |
|
|
51
|
+
| `OAUTH_PROVIDER_ERROR` | An error occurred communicating with the OAuth provider (e.g., Google). |
|
|
52
|
+
| `RATE_LIMITED` | Too many requests in a short period. The caller should wait and retry. |
|
|
53
|
+
|
|
54
|
+
### Example: invalid credentials
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"data": {
|
|
59
|
+
"sessionCreate": {
|
|
60
|
+
"session": null,
|
|
61
|
+
"requiresMfa": false,
|
|
62
|
+
"mfaFactors": null
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"errors": [
|
|
66
|
+
{
|
|
67
|
+
"message": "Invalid credentials",
|
|
68
|
+
"path": ["sessionCreate"],
|
|
69
|
+
"extensions": {
|
|
70
|
+
"code": "INVALID_CREDENTIALS"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Authorization error codes
|
|
78
|
+
|
|
79
|
+
Authorization errors occur when the caller lacks the required [scope](./authorization-and-scopes.md) for an operation.
|
|
80
|
+
|
|
81
|
+
| Code | Description |
|
|
82
|
+
|------|-------------|
|
|
83
|
+
| `UNAUTHORIZED` | No valid authentication was provided. The request is missing the `Authorization` header or the `x-api-key` header, or the token/key is invalid. |
|
|
84
|
+
| `FORBIDDEN` | The caller is authenticated but does not have the required scope for the operation. |
|
|
85
|
+
|
|
86
|
+
### Example: insufficient scope
|
|
87
|
+
|
|
88
|
+
A caller with only `organization/read` attempting to delete an organization receives:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"data": {
|
|
93
|
+
"organizationDelete": null
|
|
94
|
+
},
|
|
95
|
+
"errors": [
|
|
96
|
+
{
|
|
97
|
+
"message": "Insufficient permissions",
|
|
98
|
+
"path": ["organizationDelete"],
|
|
99
|
+
"extensions": {
|
|
100
|
+
"code": "FORBIDDEN"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Organization, member, and invitation error codes
|
|
108
|
+
|
|
109
|
+
These errors relate to organization management, member operations, and invitation workflows. They map to the `ErrorCode` enum in the schema.
|
|
110
|
+
|
|
111
|
+
### Organization errors
|
|
112
|
+
|
|
113
|
+
| Code | Description |
|
|
114
|
+
|------|-------------|
|
|
115
|
+
| `ORGANIZATION_NOT_FOUND` | The specified organization does not exist or the caller does not have access to it. |
|
|
116
|
+
| `ORGANIZATION_ALREADY_EXISTS` | An organization with this name already exists. |
|
|
117
|
+
| `SLUG_ALREADY_EXISTS` | The requested slug is already in use by another organization. |
|
|
118
|
+
| `INVALID_SLUG` | The slug contains invalid characters. Slugs must be lowercase alphanumeric with hyphens. |
|
|
119
|
+
|
|
120
|
+
### Member errors
|
|
121
|
+
|
|
122
|
+
| Code | Description |
|
|
123
|
+
|------|-------------|
|
|
124
|
+
| `MEMBER_NOT_FOUND` | The specified user is not a member of the organization. |
|
|
125
|
+
| `MEMBER_ALREADY_EXISTS` | The user is already a member of the organization. |
|
|
126
|
+
| `CANNOT_REMOVE_PRIMARY_OWNER` | The primary owner of an organization cannot be removed. |
|
|
127
|
+
| `CANNOT_CHANGE_OWN_ROLE` | A member cannot change their own role. Another owner must perform this action. |
|
|
128
|
+
|
|
129
|
+
### Invitation errors
|
|
130
|
+
|
|
131
|
+
| Code | Description |
|
|
132
|
+
|------|-------------|
|
|
133
|
+
| `INVITATION_NOT_FOUND` | The specified invitation does not exist. |
|
|
134
|
+
| `INVITATION_EXPIRED` | The invitation has expired and can no longer be accepted. |
|
|
135
|
+
| `INVITATION_ALREADY_ACCEPTED` | The invitation has already been accepted. |
|
|
136
|
+
|
|
137
|
+
### Project errors
|
|
138
|
+
|
|
139
|
+
| Code | Description |
|
|
140
|
+
|------|-------------|
|
|
141
|
+
| `PROJECT_NOT_FOUND` | The specified project does not exist or the caller does not have access. |
|
|
142
|
+
|
|
143
|
+
### General errors
|
|
144
|
+
|
|
145
|
+
| Code | Description |
|
|
146
|
+
|------|-------------|
|
|
147
|
+
| `VALIDATION_ERROR` | The input failed validation. Check the `message` field for details about which field is invalid. |
|
|
148
|
+
| `INTERNAL_ERROR` | An unexpected server error occurred. Retry the request or contact support if the issue persists. |
|
|
149
|
+
|
|
150
|
+
## Handling errors in practice
|
|
151
|
+
|
|
152
|
+
### Check for errors first
|
|
153
|
+
|
|
154
|
+
Every response should be checked for the presence of an `errors` array before processing `data`:
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
const response = await fetch('/graphql', {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: {
|
|
160
|
+
'Content-Type': 'application/json',
|
|
161
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify({ query, variables }),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const result = await response.json();
|
|
167
|
+
|
|
168
|
+
if (result.errors) {
|
|
169
|
+
for (const error of result.errors) {
|
|
170
|
+
const code = error.extensions?.code;
|
|
171
|
+
|
|
172
|
+
switch (code) {
|
|
173
|
+
case 'UNAUTHORIZED':
|
|
174
|
+
// Redirect to login
|
|
175
|
+
break;
|
|
176
|
+
case 'FORBIDDEN':
|
|
177
|
+
// Show a permissions error
|
|
178
|
+
break;
|
|
179
|
+
case 'RATE_LIMITED':
|
|
180
|
+
// Wait and retry
|
|
181
|
+
break;
|
|
182
|
+
default:
|
|
183
|
+
// Log or display the error message
|
|
184
|
+
console.error(error.message);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Refresh expired tokens
|
|
191
|
+
|
|
192
|
+
If a request returns `UNAUTHORIZED` and the caller has a refresh token, attempt a [session refresh](/api/authentication#session-refresh) before treating it as a login failure:
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
if (code === 'UNAUTHORIZED' && refreshToken) {
|
|
196
|
+
const refreshResult = await sessionRefresh(refreshToken);
|
|
197
|
+
if (refreshResult.session) {
|
|
198
|
+
// Retry the original request with the new access token
|
|
199
|
+
} else {
|
|
200
|
+
// Redirect to login
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Handle partial data
|
|
206
|
+
|
|
207
|
+
GraphQL can return partial data alongside errors. For example, a query that fetches an organization and its members might succeed for the organization but fail for the members if the caller lacks `organization_members/read`. In this case, the response contains both `data` (with the organization) and `errors` (for the members field). Always check both.
|
|
208
|
+
|
|
209
|
+
### Retry transient failures
|
|
210
|
+
|
|
211
|
+
`INTERNAL_ERROR` and `RATE_LIMITED` are transient. For `RATE_LIMITED`, implement exponential backoff before retrying. For `INTERNAL_ERROR`, a single retry is usually sufficient.
|
package/dist/docs/api/index.md
CHANGED
|
@@ -2,31 +2,192 @@
|
|
|
2
2
|
layout: docs
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
#
|
|
5
|
+
# Getting started
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
This guide walks you through your first interactions with the Picked GraphQL API, from sending a simple ping to making authenticated queries and mutations.
|
|
8
8
|
|
|
9
9
|
## Playground
|
|
10
10
|
|
|
11
|
-
Explore the API and run queries interactively
|
|
11
|
+
Explore the API and run queries interactively in the [GraphQL Playground](https://api.picked.app/api/graphql).
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## API endpoint
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
All requests go to a single endpoint:
|
|
16
16
|
|
|
17
17
|
```
|
|
18
|
-
|
|
18
|
+
POST https://api.picked.app/api/graphql
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Every request must use the `POST` method with a JSON body containing a `query` field and an optional `variables` field:
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"query": "query { ... }",
|
|
26
|
+
"variables": { }
|
|
27
|
+
}
|
|
28
|
+
```
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
Set the `Content-Type` header to `application/json` on every request.
|
|
26
31
|
|
|
27
32
|
```bash
|
|
28
33
|
curl -X POST https://api.picked.app/api/graphql \
|
|
29
34
|
-H "Content-Type: application/json" \
|
|
30
|
-
-
|
|
31
|
-
|
|
35
|
+
-d '{"query": "{ ping }"}'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Ping the API
|
|
39
|
+
|
|
40
|
+
Verify that the API is reachable with the `ping` query. This query does not require authentication.
|
|
41
|
+
|
|
42
|
+
```graphql
|
|
43
|
+
query {
|
|
44
|
+
ping
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Response:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"data": {
|
|
53
|
+
"ping": "pong"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If you receive this response, the API is up and ready.
|
|
59
|
+
|
|
60
|
+
## Authenticate
|
|
61
|
+
|
|
62
|
+
Most operations require an access token. You obtain one by creating a session with your email and password.
|
|
63
|
+
|
|
64
|
+
```graphql
|
|
65
|
+
mutation {
|
|
66
|
+
sessionCreate(input: {
|
|
67
|
+
email: "you@example.com"
|
|
68
|
+
password: "your-password"
|
|
69
|
+
}) {
|
|
70
|
+
session {
|
|
71
|
+
accessToken
|
|
72
|
+
refreshToken
|
|
73
|
+
expiresAt
|
|
74
|
+
}
|
|
75
|
+
requiresMfa
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A successful response returns a `Session` containing an `accessToken` and a `refreshToken`:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"data": {
|
|
85
|
+
"sessionCreate": {
|
|
86
|
+
"session": {
|
|
87
|
+
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
|
|
88
|
+
"refreshToken": "v1.refresh-token...",
|
|
89
|
+
"expiresAt": "2025-01-15T12:00:00Z"
|
|
90
|
+
},
|
|
91
|
+
"requiresMfa": false
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
If `requiresMfa` is `true`, you need to complete the MFA challenge before receiving a session. See the [Authentication](./authentication.md) guide for details.
|
|
98
|
+
|
|
99
|
+
For server-to-server integrations, prefer [API key authentication](/api/authentication#api-key-authentication) over session tokens.
|
|
100
|
+
|
|
101
|
+
## Make an authenticated query
|
|
102
|
+
|
|
103
|
+
Pass the access token in the `Authorization` header as a Bearer token:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Now you can query resources that require authentication. For example, fetch your user profile:
|
|
110
|
+
|
|
111
|
+
```graphql
|
|
112
|
+
query {
|
|
113
|
+
user {
|
|
114
|
+
id
|
|
115
|
+
email
|
|
116
|
+
createdAt
|
|
117
|
+
organizations(first: 5) {
|
|
118
|
+
edges {
|
|
119
|
+
node {
|
|
120
|
+
id
|
|
121
|
+
name
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
32
127
|
```
|
|
128
|
+
|
|
129
|
+
## Make a mutation
|
|
130
|
+
|
|
131
|
+
Mutations follow the same pattern: send a `POST` request with the `Authorization` header. For example, create an organization:
|
|
132
|
+
|
|
133
|
+
```graphql
|
|
134
|
+
mutation {
|
|
135
|
+
organizationCreate(input: {
|
|
136
|
+
name: "My Company"
|
|
137
|
+
slug: "my-company"
|
|
138
|
+
}) {
|
|
139
|
+
organization {
|
|
140
|
+
id
|
|
141
|
+
name
|
|
142
|
+
slug
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Response:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"data": {
|
|
153
|
+
"organizationCreate": {
|
|
154
|
+
"organization": {
|
|
155
|
+
"id": "org_abc123",
|
|
156
|
+
"name": "My Company",
|
|
157
|
+
"slug": "my-company"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Every mutation accepts an `input` argument and returns a payload object. The payload contains the created or updated resource on success, or errors in the top-level `errors` array on failure.
|
|
165
|
+
|
|
166
|
+
## Error handling basics
|
|
167
|
+
|
|
168
|
+
When something goes wrong, the API returns errors in the standard GraphQL `errors` array:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"data": null,
|
|
173
|
+
"errors": [
|
|
174
|
+
{
|
|
175
|
+
"message": "Invalid credentials",
|
|
176
|
+
"extensions": {
|
|
177
|
+
"code": "INVALID_CREDENTIALS"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Check the `extensions.code` field for a machine-readable error code you can use to handle specific failures programmatically. See the [Error Handling](./error-handling.md) guide for the full list of error codes.
|
|
185
|
+
|
|
186
|
+
## Next steps
|
|
187
|
+
|
|
188
|
+
Now that you can authenticate and make requests, explore these guides to go deeper:
|
|
189
|
+
|
|
190
|
+
- [Authentication](./authentication.md) — email/password, OAuth, magic links, MFA, API keys, and session management
|
|
191
|
+
- [Authorization and Scopes](./authorization-and-scopes.md) — understand what each role can access and how scopes control permissions
|
|
192
|
+
- [Pagination](./pagination.md) — navigate large result sets with cursor-based pagination
|
|
193
|
+
- [Error Handling](./error-handling.md) — handle every error code the API can return
|