@idelio/contracts 0.1.0-beta.0
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/LICENSE +21 -0
- package/README.md +27 -0
- package/dist/api-keys.d.ts +44 -0
- package/dist/api-keys.js +19 -0
- package/dist/assets.d.ts +188 -0
- package/dist/assets.js +107 -0
- package/dist/billing.d.ts +94 -0
- package/dist/billing.js +67 -0
- package/dist/brands.d.ts +641 -0
- package/dist/brands.js +364 -0
- package/dist/catalog.d.ts +26 -0
- package/dist/catalog.js +53 -0
- package/dist/consent.d.ts +60 -0
- package/dist/consent.js +65 -0
- package/dist/creative-direction.d.ts +333 -0
- package/dist/creative-direction.js +681 -0
- package/dist/credits.d.ts +38 -0
- package/dist/credits.js +31 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +22 -0
- package/dist/events.d.ts +20 -0
- package/dist/events.js +29 -0
- package/dist/ids.d.ts +28 -0
- package/dist/ids.js +29 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/jobs.d.ts +100 -0
- package/dist/jobs.js +81 -0
- package/dist/me.d.ts +77 -0
- package/dist/me.js +45 -0
- package/dist/notifications.d.ts +17 -0
- package/dist/notifications.js +26 -0
- package/dist/pricing.d.ts +38 -0
- package/dist/pricing.js +105 -0
- package/dist/rate-limits.d.ts +8 -0
- package/dist/rate-limits.js +18 -0
- package/dist/usage.d.ts +22 -0
- package/dist/usage.js +11 -0
- package/dist/waitlist.d.ts +34 -0
- package/dist/waitlist.js +27 -0
- package/dist/webhooks.d.ts +74 -0
- package/dist/webhooks.js +26 -0
- package/openapi/openapi.yaml +3026 -0
- package/package.json +54 -0
|
@@ -0,0 +1,3026 @@
|
|
|
1
|
+
# Idelio Public API - OpenAPI 3.1 (source of truth)
|
|
2
|
+
# Full surface: 63 routes across 15 resource groups (see docs/API Specification v1.1).
|
|
3
|
+
# This file starts with the core generation slice; extend additively only. One
|
|
4
|
+
# route has ever been removed: POST /webhooks/paddle, when Paddle was retired
|
|
5
|
+
# (#334). It was a signature-gated inbound callback that only Paddle itself
|
|
6
|
+
# called, so no API client lost anything that worked - the additive-only rule
|
|
7
|
+
# protects clients of this API, not a retired provider's callbacks.
|
|
8
|
+
openapi: 3.1.0
|
|
9
|
+
info:
|
|
10
|
+
title: Idelio API
|
|
11
|
+
version: 1.0.0
|
|
12
|
+
description: >
|
|
13
|
+
AI-native branding platform. Bearer auth (Clerk JWT or workspace API key
|
|
14
|
+
sk_live_/sk_test_). Idempotency-Key mandatory on paid generation routes.
|
|
15
|
+
Uniform error envelope on every 4xx/5xx. Cursor pagination on all lists.
|
|
16
|
+
servers:
|
|
17
|
+
- url: https://api.idelio.pro/v1
|
|
18
|
+
security:
|
|
19
|
+
- bearerAuth: []
|
|
20
|
+
- apiKeyAuth: []
|
|
21
|
+
paths:
|
|
22
|
+
/me:
|
|
23
|
+
get:
|
|
24
|
+
operationId: getMe
|
|
25
|
+
summary: Return the authenticated user, their workspace and effective role.
|
|
26
|
+
responses:
|
|
27
|
+
"200":
|
|
28
|
+
description: User + workspace context
|
|
29
|
+
content:
|
|
30
|
+
application/json:
|
|
31
|
+
schema: { $ref: "#/components/schemas/MeResponse" }
|
|
32
|
+
/me/onboarding:
|
|
33
|
+
post:
|
|
34
|
+
operationId: completeOnboarding
|
|
35
|
+
summary: Record that the authenticated membership finished onboarding.
|
|
36
|
+
description: >-
|
|
37
|
+
Idempotent. A repeat returns the stored timestamp rather than moving it,
|
|
38
|
+
so a retry or a second tab cannot rewrite when a user was onboarded.
|
|
39
|
+
Takes no request body - the only fact is that it happened, and the
|
|
40
|
+
timestamp is the server's. Refused for an API key, which acts for the
|
|
41
|
+
workspace rather than for a person and has no membership to settle.
|
|
42
|
+
responses:
|
|
43
|
+
"200":
|
|
44
|
+
description: The onboarding completion timestamp in force.
|
|
45
|
+
content:
|
|
46
|
+
application/json:
|
|
47
|
+
schema:
|
|
48
|
+
type: object
|
|
49
|
+
required: [onboardingCompletedAt]
|
|
50
|
+
properties:
|
|
51
|
+
onboardingCompletedAt: { type: string, format: date-time }
|
|
52
|
+
"422":
|
|
53
|
+
description: Authenticated with an API key, which has no membership.
|
|
54
|
+
content:
|
|
55
|
+
application/json:
|
|
56
|
+
schema: { $ref: "#/components/schemas/Error" }
|
|
57
|
+
/workspace:
|
|
58
|
+
get:
|
|
59
|
+
operationId: getWorkspace
|
|
60
|
+
summary: Retrieve the current workspace.
|
|
61
|
+
responses:
|
|
62
|
+
"200":
|
|
63
|
+
description: Workspace object
|
|
64
|
+
headers:
|
|
65
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
66
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
67
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
68
|
+
content:
|
|
69
|
+
application/json:
|
|
70
|
+
schema:
|
|
71
|
+
type: object
|
|
72
|
+
properties:
|
|
73
|
+
workspace: { $ref: "#/components/schemas/Workspace" }
|
|
74
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
75
|
+
patch:
|
|
76
|
+
operationId: updateWorkspace
|
|
77
|
+
description: Available to a signed-in app session with at least the admin role. An API key receives 403 `session_required`.
|
|
78
|
+
security: [{ bearerAuth: [] }]
|
|
79
|
+
summary: Update workspace name and settings.
|
|
80
|
+
parameters:
|
|
81
|
+
- name: Idempotency-Key
|
|
82
|
+
in: header
|
|
83
|
+
required: true
|
|
84
|
+
schema: { type: string }
|
|
85
|
+
requestBody:
|
|
86
|
+
required: true
|
|
87
|
+
content:
|
|
88
|
+
application/json:
|
|
89
|
+
schema: { $ref: "#/components/schemas/UpdateWorkspaceRequest" }
|
|
90
|
+
responses:
|
|
91
|
+
"200":
|
|
92
|
+
description: Updated workspace
|
|
93
|
+
headers:
|
|
94
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
95
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
96
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
97
|
+
content:
|
|
98
|
+
application/json:
|
|
99
|
+
schema:
|
|
100
|
+
type: object
|
|
101
|
+
properties:
|
|
102
|
+
workspace: { $ref: "#/components/schemas/Workspace" }
|
|
103
|
+
"422": { description: Invalid patch }
|
|
104
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
105
|
+
/workspace/members:
|
|
106
|
+
get:
|
|
107
|
+
operationId: listMembers
|
|
108
|
+
summary: List members and their roles.
|
|
109
|
+
parameters:
|
|
110
|
+
- { name: cursor, in: query, schema: { type: string } }
|
|
111
|
+
- { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
|
|
112
|
+
responses:
|
|
113
|
+
"200":
|
|
114
|
+
description: Paginated members
|
|
115
|
+
headers:
|
|
116
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
117
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
118
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
119
|
+
content:
|
|
120
|
+
application/json:
|
|
121
|
+
schema:
|
|
122
|
+
type: object
|
|
123
|
+
properties:
|
|
124
|
+
data:
|
|
125
|
+
type: array
|
|
126
|
+
items: { $ref: "#/components/schemas/Member" }
|
|
127
|
+
next_cursor: { type: string, nullable: true }
|
|
128
|
+
has_more: { type: boolean }
|
|
129
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
130
|
+
/workspace/invitations:
|
|
131
|
+
post:
|
|
132
|
+
operationId: inviteMember
|
|
133
|
+
description: Available to a signed-in app session with at least the admin role. An API key receives 403 `session_required`.
|
|
134
|
+
security: [{ bearerAuth: [] }]
|
|
135
|
+
summary: Invite a user by email with a role.
|
|
136
|
+
parameters:
|
|
137
|
+
- name: Idempotency-Key
|
|
138
|
+
in: header
|
|
139
|
+
required: true
|
|
140
|
+
schema: { type: string }
|
|
141
|
+
requestBody:
|
|
142
|
+
required: true
|
|
143
|
+
content:
|
|
144
|
+
application/json:
|
|
145
|
+
schema: { $ref: "#/components/schemas/InviteMemberRequest" }
|
|
146
|
+
responses:
|
|
147
|
+
"201":
|
|
148
|
+
description: Invitation created
|
|
149
|
+
headers:
|
|
150
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
151
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
152
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
153
|
+
content:
|
|
154
|
+
application/json:
|
|
155
|
+
schema:
|
|
156
|
+
type: object
|
|
157
|
+
properties:
|
|
158
|
+
invitation:
|
|
159
|
+
type: object
|
|
160
|
+
properties:
|
|
161
|
+
email: { type: string }
|
|
162
|
+
role: { type: string, enum: [admin, member] }
|
|
163
|
+
status: { type: string }
|
|
164
|
+
"409": { description: Already a member }
|
|
165
|
+
"422": { description: Invalid request }
|
|
166
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
167
|
+
/workspace/members/{userId}:
|
|
168
|
+
patch:
|
|
169
|
+
operationId: changeMemberRole
|
|
170
|
+
description: Available to a signed-in app session with at least the admin role. An API key receives 403 `session_required`.
|
|
171
|
+
security: [{ bearerAuth: [] }]
|
|
172
|
+
summary: Change a member's role.
|
|
173
|
+
parameters:
|
|
174
|
+
- { name: userId, in: path, required: true, schema: { type: string } }
|
|
175
|
+
requestBody:
|
|
176
|
+
required: true
|
|
177
|
+
content:
|
|
178
|
+
application/json:
|
|
179
|
+
schema: { $ref: "#/components/schemas/ChangeMemberRoleRequest" }
|
|
180
|
+
responses:
|
|
181
|
+
"200":
|
|
182
|
+
description: Updated member
|
|
183
|
+
headers:
|
|
184
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
185
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
186
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
187
|
+
content:
|
|
188
|
+
application/json:
|
|
189
|
+
schema:
|
|
190
|
+
type: object
|
|
191
|
+
properties:
|
|
192
|
+
member: { $ref: "#/components/schemas/Member" }
|
|
193
|
+
"404": { description: Member not found }
|
|
194
|
+
"409": { description: Would leave the workspace without an owner }
|
|
195
|
+
"422": { description: Invalid request }
|
|
196
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
197
|
+
delete:
|
|
198
|
+
operationId: removeMember
|
|
199
|
+
description: Available to a signed-in app session with at least the admin role. An API key receives 403 `session_required`.
|
|
200
|
+
security: [{ bearerAuth: [] }]
|
|
201
|
+
summary: Remove a member from the workspace.
|
|
202
|
+
parameters:
|
|
203
|
+
- { name: userId, in: path, required: true, schema: { type: string } }
|
|
204
|
+
responses:
|
|
205
|
+
"204": { description: Removed }
|
|
206
|
+
"404": { description: Member not found }
|
|
207
|
+
"409": { description: Would leave the workspace without an owner }
|
|
208
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
209
|
+
/brands:
|
|
210
|
+
get:
|
|
211
|
+
operationId: listBrands
|
|
212
|
+
summary: List brands in the workspace.
|
|
213
|
+
parameters:
|
|
214
|
+
- { name: status, in: query, schema: { type: string, enum: [draft, generating, ready, failed] } }
|
|
215
|
+
- { name: q, in: query, schema: { type: string } }
|
|
216
|
+
- { name: cursor, in: query, schema: { type: string } }
|
|
217
|
+
- { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
|
|
218
|
+
responses:
|
|
219
|
+
"200":
|
|
220
|
+
description: Paginated brands
|
|
221
|
+
headers:
|
|
222
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
223
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
224
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
225
|
+
content:
|
|
226
|
+
application/json:
|
|
227
|
+
schema:
|
|
228
|
+
type: object
|
|
229
|
+
properties:
|
|
230
|
+
data:
|
|
231
|
+
type: array
|
|
232
|
+
items: { $ref: "#/components/schemas/Brand" }
|
|
233
|
+
next_cursor: { type: string, nullable: true }
|
|
234
|
+
has_more: { type: boolean }
|
|
235
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
236
|
+
post:
|
|
237
|
+
operationId: createBrand
|
|
238
|
+
summary: Generate a new brand from a prompt or template (reserves credits, starts a durable workflow).
|
|
239
|
+
parameters:
|
|
240
|
+
- name: Idempotency-Key
|
|
241
|
+
in: header
|
|
242
|
+
required: true
|
|
243
|
+
schema: { type: string }
|
|
244
|
+
requestBody:
|
|
245
|
+
required: true
|
|
246
|
+
content:
|
|
247
|
+
application/json:
|
|
248
|
+
schema: { $ref: "#/components/schemas/CreateBrandRequest" }
|
|
249
|
+
responses:
|
|
250
|
+
"202":
|
|
251
|
+
description: Job accepted
|
|
252
|
+
headers:
|
|
253
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
254
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
255
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
256
|
+
content:
|
|
257
|
+
application/json:
|
|
258
|
+
schema: { $ref: "#/components/schemas/JobEnvelope" }
|
|
259
|
+
"402": { description: Insufficient credits }
|
|
260
|
+
"409": { description: This workspace already has its plan's worth of generations running - `code` is `too_many_running_jobs` }
|
|
261
|
+
"422":
|
|
262
|
+
description: >-
|
|
263
|
+
Invalid request. `code` is `validation_failed`, or
|
|
264
|
+
`required_assets_missing` when an explicit `assets` list omits a
|
|
265
|
+
required kind (the message names which); `param` names the
|
|
266
|
+
failing field.
|
|
267
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
268
|
+
/brands/surprise:
|
|
269
|
+
post:
|
|
270
|
+
operationId: surpriseBrief
|
|
271
|
+
summary: Generate a random brand idea to seed the creator's Describe step ("Surprise me") - free, not persisted.
|
|
272
|
+
responses:
|
|
273
|
+
"200":
|
|
274
|
+
description: A freshly generated random brief
|
|
275
|
+
content:
|
|
276
|
+
application/json:
|
|
277
|
+
schema: { $ref: "#/components/schemas/SurpriseBriefResponse" }
|
|
278
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
279
|
+
/brands/directions:
|
|
280
|
+
post:
|
|
281
|
+
operationId: brandCreativeDirections
|
|
282
|
+
summary: Shortlist creative directions against a brief - free, not persisted.
|
|
283
|
+
description: >-
|
|
284
|
+
Ranks the palette, typography and logo catalogs against the founder's
|
|
285
|
+
own description so the Customize step suggests directions that suit
|
|
286
|
+
their brand instead of a fixed order. Returns catalog ids only, never
|
|
287
|
+
colours or family names, and the API validates every id against its own
|
|
288
|
+
catalog before answering. Free and not persisted, so no
|
|
289
|
+
Idempotency-Key; rate limited like /brands/names. A failure here is not
|
|
290
|
+
an error the user needs to see - the client falls back to the catalog
|
|
291
|
+
order.
|
|
292
|
+
requestBody:
|
|
293
|
+
required: true
|
|
294
|
+
content:
|
|
295
|
+
application/json:
|
|
296
|
+
schema: { $ref: "#/components/schemas/CreativeDirectionsRequest" }
|
|
297
|
+
responses:
|
|
298
|
+
"200":
|
|
299
|
+
description: A ranked shortlist per axis, padded from the catalog
|
|
300
|
+
content:
|
|
301
|
+
application/json:
|
|
302
|
+
schema: { $ref: "#/components/schemas/CreativeDirectionsShortlist" }
|
|
303
|
+
"422": { description: Invalid request body - the failing field is named in `param` }
|
|
304
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
305
|
+
"503": { description: The shortlist could not be produced - retryable }
|
|
306
|
+
/brands/names:
|
|
307
|
+
post:
|
|
308
|
+
operationId: brandNameIdeas
|
|
309
|
+
summary: Generate brand name ideas for the creator's name generator - free, not persisted.
|
|
310
|
+
description: >-
|
|
311
|
+
Returns six brand name candidates in the requested style, each with a
|
|
312
|
+
one-line hint explaining why it works. Pass the founder's own keyword
|
|
313
|
+
and description to keep the ideas about their actual business. Free
|
|
314
|
+
and not persisted, so no Idempotency-Key; rate limited like
|
|
315
|
+
/brands/surprise.
|
|
316
|
+
requestBody:
|
|
317
|
+
required: true
|
|
318
|
+
content:
|
|
319
|
+
application/json:
|
|
320
|
+
schema: { $ref: "#/components/schemas/NameIdeasRequest" }
|
|
321
|
+
responses:
|
|
322
|
+
"200":
|
|
323
|
+
description: A freshly generated set of name ideas
|
|
324
|
+
content:
|
|
325
|
+
application/json:
|
|
326
|
+
schema: { $ref: "#/components/schemas/NameIdeasResponse" }
|
|
327
|
+
"422": { description: Invalid request body - the failing field is named in `param` }
|
|
328
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
329
|
+
"503": { description: The generator could not produce names - retryable }
|
|
330
|
+
/brands/{id}:
|
|
331
|
+
get:
|
|
332
|
+
operationId: getBrand
|
|
333
|
+
summary: Retrieve a brand with its head version summary.
|
|
334
|
+
parameters:
|
|
335
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
336
|
+
responses:
|
|
337
|
+
"200":
|
|
338
|
+
description: Brand object
|
|
339
|
+
headers:
|
|
340
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
341
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
342
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
343
|
+
content:
|
|
344
|
+
application/json:
|
|
345
|
+
schema:
|
|
346
|
+
type: object
|
|
347
|
+
properties:
|
|
348
|
+
brand: { $ref: "#/components/schemas/Brand" }
|
|
349
|
+
"404": { description: Not found }
|
|
350
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
351
|
+
patch:
|
|
352
|
+
operationId: updateBrand
|
|
353
|
+
summary: Rename a brand, change its slug, or update generation locks.
|
|
354
|
+
parameters:
|
|
355
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
356
|
+
requestBody:
|
|
357
|
+
required: true
|
|
358
|
+
content:
|
|
359
|
+
application/json:
|
|
360
|
+
schema: { $ref: "#/components/schemas/UpdateBrandRequest" }
|
|
361
|
+
responses:
|
|
362
|
+
"200":
|
|
363
|
+
description: Updated brand
|
|
364
|
+
headers:
|
|
365
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
366
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
367
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
368
|
+
content:
|
|
369
|
+
application/json:
|
|
370
|
+
schema:
|
|
371
|
+
type: object
|
|
372
|
+
properties:
|
|
373
|
+
brand: { $ref: "#/components/schemas/Brand" }
|
|
374
|
+
"404": { description: Not found }
|
|
375
|
+
"409": { description: Slug already in use in this workspace }
|
|
376
|
+
"422": { description: Invalid patch }
|
|
377
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
378
|
+
delete:
|
|
379
|
+
operationId: deleteBrand
|
|
380
|
+
summary: Soft-delete a brand (never hard-deleted).
|
|
381
|
+
parameters:
|
|
382
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
383
|
+
responses:
|
|
384
|
+
"204": { description: Deleted }
|
|
385
|
+
"404": { description: Not found }
|
|
386
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
387
|
+
/brands/{id}/assets:
|
|
388
|
+
get:
|
|
389
|
+
operationId: listBrandAssets
|
|
390
|
+
summary: List assets belonging to a brand.
|
|
391
|
+
parameters:
|
|
392
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
393
|
+
- { name: kind, in: query, schema: { type: string } }
|
|
394
|
+
- { name: cursor, in: query, schema: { type: string } }
|
|
395
|
+
- name: limit
|
|
396
|
+
in: query
|
|
397
|
+
description: >-
|
|
398
|
+
Page size, 1-100. A brand with the whole catalogue holds more than
|
|
399
|
+
one default page, so follow `next_cursor` or raise the limit.
|
|
400
|
+
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
|
|
401
|
+
responses:
|
|
402
|
+
"200":
|
|
403
|
+
description: Paginated assets
|
|
404
|
+
headers:
|
|
405
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
406
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
407
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
408
|
+
content:
|
|
409
|
+
application/json:
|
|
410
|
+
schema:
|
|
411
|
+
type: object
|
|
412
|
+
properties:
|
|
413
|
+
data:
|
|
414
|
+
type: array
|
|
415
|
+
items: { $ref: "#/components/schemas/Asset" }
|
|
416
|
+
next_cursor: { type: string, nullable: true }
|
|
417
|
+
has_more: { type: boolean }
|
|
418
|
+
"404": { description: Not found }
|
|
419
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
420
|
+
post:
|
|
421
|
+
operationId: expandBrand
|
|
422
|
+
summary: Expand a brand - generate additional assets from its frozen head DNA (every asset kind except logo, which has its own generate/approve path). custom_image requires custom_image_prompt and is never combined with other kinds in the same request; color_palette/font_pairing are likewise never combined with any other kind. mockup and hero_image are studio/elite only - flash has no model route for them.
|
|
423
|
+
parameters:
|
|
424
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
425
|
+
- { name: Idempotency-Key, in: header, required: true, schema: { type: string } }
|
|
426
|
+
requestBody:
|
|
427
|
+
required: true
|
|
428
|
+
content:
|
|
429
|
+
application/json:
|
|
430
|
+
schema: { $ref: "#/components/schemas/ExpandBrandRequest" }
|
|
431
|
+
responses:
|
|
432
|
+
"202":
|
|
433
|
+
description: GenerationJob object (queued)
|
|
434
|
+
headers:
|
|
435
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
436
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
437
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
438
|
+
content:
|
|
439
|
+
application/json:
|
|
440
|
+
schema: { $ref: "#/components/schemas/JobEnvelope" }
|
|
441
|
+
"402": { description: Insufficient credits }
|
|
442
|
+
"404": { description: Not found }
|
|
443
|
+
"409": { description: "Brand not ready, one of the requested asset kinds already exists, or this workspace already has its plan's worth of generations running (`code` is `too_many_running_jobs`)" }
|
|
444
|
+
"422": { description: An asset kind cannot be expanded yet, or a templated/mockup asset was requested on the flash tier }
|
|
445
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
446
|
+
/brands/{id}/refine:
|
|
447
|
+
post:
|
|
448
|
+
operationId: refineBrandDna
|
|
449
|
+
summary: Free-text Creative Director chat edit to the brand's DNA (strategy/palette/typography); forks a new BrandVersion.
|
|
450
|
+
parameters:
|
|
451
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
452
|
+
- { name: Idempotency-Key, in: header, required: true, schema: { type: string } }
|
|
453
|
+
requestBody:
|
|
454
|
+
required: true
|
|
455
|
+
content:
|
|
456
|
+
application/json:
|
|
457
|
+
schema: { $ref: "#/components/schemas/BrandRefineRequest" }
|
|
458
|
+
responses:
|
|
459
|
+
"202":
|
|
460
|
+
description: GenerationJob object (queued)
|
|
461
|
+
headers:
|
|
462
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
463
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
464
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
465
|
+
content:
|
|
466
|
+
application/json:
|
|
467
|
+
schema: { $ref: "#/components/schemas/JobEnvelope" }
|
|
468
|
+
"402": { description: Insufficient credits }
|
|
469
|
+
"404": { description: Not found }
|
|
470
|
+
"409": { description: "Brand not ready, or this workspace already has its plan's worth of generations running (`code` is `too_many_running_jobs`)" }
|
|
471
|
+
"422": { description: Validation failed }
|
|
472
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
473
|
+
/brands/{id}/logo:
|
|
474
|
+
get:
|
|
475
|
+
operationId: getBrandLogo
|
|
476
|
+
summary: Redirect to the brand's logo file, as PNG or SVG.
|
|
477
|
+
parameters:
|
|
478
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
479
|
+
- { name: format, in: query, schema: { type: string, enum: [png, svg], default: png } }
|
|
480
|
+
- name: size
|
|
481
|
+
in: query
|
|
482
|
+
description: >-
|
|
483
|
+
Kept for older brands only. The mark, wordmark and lockup are each
|
|
484
|
+
served as the file generation wrote, at its native resolution. A
|
|
485
|
+
brand from before the raster mark holds an SVG-only logo, and for
|
|
486
|
+
it a PNG request is answered with the favicon (`size` <= 64) or
|
|
487
|
+
the app icon. Fixed-size icons are their own assets: use
|
|
488
|
+
GET /assets/{id}/download with `size`.
|
|
489
|
+
schema: { type: integer, default: 512 }
|
|
490
|
+
- name: variant
|
|
491
|
+
in: query
|
|
492
|
+
description: >-
|
|
493
|
+
`mark` is the symbol alone (the `logo` asset), `wordmark` the
|
|
494
|
+
name alone (the `logo_wordmark` asset, or the retired `wordmark`
|
|
495
|
+
kind older brands hold, which is PNG-only), and `lockup` the two
|
|
496
|
+
arranged together (`logo_alternative`) in whichever arrangement
|
|
497
|
+
the brand pinned in the creator - horizontal when it pinned none.
|
|
498
|
+
Brands generated before lockups were produced answer 422 for
|
|
499
|
+
`lockup`.
|
|
500
|
+
schema: { type: string, enum: [mark, wordmark, lockup], default: mark }
|
|
501
|
+
responses:
|
|
502
|
+
"302": { description: Redirect to signed CDN URL }
|
|
503
|
+
"404": { description: Not found }
|
|
504
|
+
"409": { description: Brand not ready }
|
|
505
|
+
"422": { description: Unsupported format for this asset, or a lockup this brand does not have }
|
|
506
|
+
/brands/{id}/palette:
|
|
507
|
+
get:
|
|
508
|
+
operationId: getBrandPalette
|
|
509
|
+
summary: Export the palette as design tokens (inline).
|
|
510
|
+
parameters:
|
|
511
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
512
|
+
- { name: format, in: query, schema: { type: string, enum: [json, css], default: json } }
|
|
513
|
+
responses:
|
|
514
|
+
"200":
|
|
515
|
+
description: Tokens or CSS variables
|
|
516
|
+
content:
|
|
517
|
+
application/json:
|
|
518
|
+
schema:
|
|
519
|
+
type: object
|
|
520
|
+
properties:
|
|
521
|
+
brand_id: { type: string }
|
|
522
|
+
colors: { type: object, additionalProperties: { type: string } }
|
|
523
|
+
css: { type: string }
|
|
524
|
+
"404": { description: Not found }
|
|
525
|
+
"409": { description: Brand not ready }
|
|
526
|
+
/brands/{id}/tokens:
|
|
527
|
+
get:
|
|
528
|
+
operationId: getBrandTokens
|
|
529
|
+
summary: Full W3C design-token document (inline).
|
|
530
|
+
parameters:
|
|
531
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
532
|
+
responses:
|
|
533
|
+
"200":
|
|
534
|
+
description: tokens.json
|
|
535
|
+
content:
|
|
536
|
+
application/json:
|
|
537
|
+
schema:
|
|
538
|
+
type: object
|
|
539
|
+
properties:
|
|
540
|
+
tokens: { type: object }
|
|
541
|
+
"404": { description: Not found }
|
|
542
|
+
"409": { description: Brand not ready }
|
|
543
|
+
/brands/{id}/dna:
|
|
544
|
+
get:
|
|
545
|
+
operationId: getBrandDna
|
|
546
|
+
summary: The frozen creative genome (brief + strategy) the brand reads from.
|
|
547
|
+
parameters:
|
|
548
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
549
|
+
responses:
|
|
550
|
+
"200":
|
|
551
|
+
description: Brand DNA
|
|
552
|
+
content:
|
|
553
|
+
application/json:
|
|
554
|
+
schema:
|
|
555
|
+
type: object
|
|
556
|
+
properties:
|
|
557
|
+
dna: { $ref: "#/components/schemas/BrandDna" }
|
|
558
|
+
"404": { description: Not found }
|
|
559
|
+
"409": { description: Brand not ready }
|
|
560
|
+
/brands/{id}/versions:
|
|
561
|
+
get:
|
|
562
|
+
operationId: listBrandVersions
|
|
563
|
+
summary: Version history (git-commit semantics) for the brand, newest first.
|
|
564
|
+
parameters:
|
|
565
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
566
|
+
responses:
|
|
567
|
+
"200":
|
|
568
|
+
description: Brand versions
|
|
569
|
+
content:
|
|
570
|
+
application/json:
|
|
571
|
+
schema:
|
|
572
|
+
type: object
|
|
573
|
+
properties:
|
|
574
|
+
data:
|
|
575
|
+
type: array
|
|
576
|
+
items: { $ref: "#/components/schemas/BrandVersionSummary" }
|
|
577
|
+
"404": { description: Not found }
|
|
578
|
+
/brands/{id}/suggestions:
|
|
579
|
+
get:
|
|
580
|
+
operationId: getBrandSuggestions
|
|
581
|
+
summary: This week's Creative-Director suggestions for the dashboard - computed weekly per brand, cached.
|
|
582
|
+
parameters:
|
|
583
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
584
|
+
responses:
|
|
585
|
+
"200":
|
|
586
|
+
description: Suggestions
|
|
587
|
+
content:
|
|
588
|
+
application/json:
|
|
589
|
+
schema:
|
|
590
|
+
type: object
|
|
591
|
+
properties:
|
|
592
|
+
items:
|
|
593
|
+
type: array
|
|
594
|
+
items: { $ref: "#/components/schemas/BrandSuggestionItem" }
|
|
595
|
+
"404": { description: Not found }
|
|
596
|
+
"409": { description: Brand not ready }
|
|
597
|
+
/brands/{id}/suggestions/{kind}/generate:
|
|
598
|
+
post:
|
|
599
|
+
operationId: generateBrandSuggestion
|
|
600
|
+
summary: Start generating one of this week's suggestions - delegates entirely to POST /brands/{id}/assets.
|
|
601
|
+
parameters:
|
|
602
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
603
|
+
- { name: kind, in: path, required: true, schema: { type: string } }
|
|
604
|
+
- { name: Idempotency-Key, in: header, required: true, schema: { type: string } }
|
|
605
|
+
responses:
|
|
606
|
+
"202":
|
|
607
|
+
description: GenerationJob object (queued) - poll GET /jobs/{id} or GET /brands/{id}/suggestions for status
|
|
608
|
+
content:
|
|
609
|
+
application/json:
|
|
610
|
+
schema:
|
|
611
|
+
type: object
|
|
612
|
+
properties:
|
|
613
|
+
job_id: { type: string }
|
|
614
|
+
"404": { description: Suggestion not found }
|
|
615
|
+
"409": { description: "Already generating, or brand not ready, or this workspace already has its plan's worth of generations running (`code` is `too_many_running_jobs`), or the suggestion is a custom asset idea (`code` is `suggestion_needs_review`) - generate those through POST /brands/{id}/assets once the user has seen the prompt" }
|
|
616
|
+
/suggestions:
|
|
617
|
+
get:
|
|
618
|
+
operationId: listSuggestions
|
|
619
|
+
summary: The dashboard's cross-brand suggestions - up to 3 items from the 3 most recently active ready brands, mixed round-robin. Never 404/409; a workspace with nothing to suggest gets an empty list. A brand that owns every catalogue kind it can be offered contributes custom asset ideas (kind custom_image) instead.
|
|
620
|
+
responses:
|
|
621
|
+
"200":
|
|
622
|
+
description: Suggestions
|
|
623
|
+
content:
|
|
624
|
+
application/json:
|
|
625
|
+
schema:
|
|
626
|
+
type: object
|
|
627
|
+
required: [items]
|
|
628
|
+
properties:
|
|
629
|
+
items:
|
|
630
|
+
type: array
|
|
631
|
+
maxItems: 3
|
|
632
|
+
items: { $ref: "#/components/schemas/WorkspaceSuggestionItem" }
|
|
633
|
+
/brands/{id}/versions/restore:
|
|
634
|
+
post:
|
|
635
|
+
operationId: restoreBrandVersion
|
|
636
|
+
summary: Restore a prior brand DNA version as the current version (creates a new version, like git revert - nothing is edited in place). Asset versions are untouched.
|
|
637
|
+
parameters:
|
|
638
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
639
|
+
- { name: Idempotency-Key, in: header, required: true, schema: { type: string } }
|
|
640
|
+
requestBody:
|
|
641
|
+
required: true
|
|
642
|
+
content:
|
|
643
|
+
application/json:
|
|
644
|
+
schema: { $ref: "#/components/schemas/RestoreRequest" }
|
|
645
|
+
responses:
|
|
646
|
+
"201":
|
|
647
|
+
description: Restored
|
|
648
|
+
content:
|
|
649
|
+
application/json:
|
|
650
|
+
schema:
|
|
651
|
+
type: object
|
|
652
|
+
properties:
|
|
653
|
+
brand: { $ref: "#/components/schemas/Brand" }
|
|
654
|
+
"404": { description: Not found }
|
|
655
|
+
/brands/{id}/export:
|
|
656
|
+
get:
|
|
657
|
+
operationId: exportBrandKit
|
|
658
|
+
summary: Package the full brand kit as a ZIP (async, always repackaged fresh).
|
|
659
|
+
parameters:
|
|
660
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
661
|
+
- { name: include, in: query, schema: { type: string }, description: "Comma-separated asset kinds, e.g. logo,color_palette. Omit for all." }
|
|
662
|
+
responses:
|
|
663
|
+
"202":
|
|
664
|
+
description: GenerationJob object (queued) - poll GET /jobs/{id} for download_url
|
|
665
|
+
content:
|
|
666
|
+
application/json:
|
|
667
|
+
schema: { $ref: "#/components/schemas/JobEnvelope" }
|
|
668
|
+
"404": { description: Not found }
|
|
669
|
+
"409": { description: Brand not ready }
|
|
670
|
+
/brands/{id}/bundle:
|
|
671
|
+
get:
|
|
672
|
+
operationId: getBrandBundle
|
|
673
|
+
summary: Repo-ready package (tokens, CSS/framework config, logo/favicon) for a target toolchain - deterministic per brand+target; 302 if already built, 202 otherwise.
|
|
674
|
+
parameters:
|
|
675
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
676
|
+
- { name: target, in: query, schema: { type: string, enum: [tokens, next, vite, expo], default: tokens } }
|
|
677
|
+
- { name: version, in: query, schema: { type: string }, description: "Not yet supported - any value returns 422." }
|
|
678
|
+
- { name: redirect, in: query, schema: { type: boolean, default: true } }
|
|
679
|
+
responses:
|
|
680
|
+
"200":
|
|
681
|
+
description: Signed CDN URL as JSON (only when redirect=false and the archive already exists)
|
|
682
|
+
content:
|
|
683
|
+
application/json:
|
|
684
|
+
schema:
|
|
685
|
+
type: object
|
|
686
|
+
required: [url]
|
|
687
|
+
properties: { url: { type: string } }
|
|
688
|
+
"302": { description: Redirect to a signed CDN URL for the already-built archive (default) }
|
|
689
|
+
"202":
|
|
690
|
+
description: Packaging job started - poll GET /jobs/{id} for download_url
|
|
691
|
+
content:
|
|
692
|
+
application/json:
|
|
693
|
+
schema: { $ref: "#/components/schemas/JobEnvelope" }
|
|
694
|
+
"404": { description: Not found }
|
|
695
|
+
"409": { description: Brand not ready }
|
|
696
|
+
"422": { description: Invalid target, or version pinning requested (not supported) }
|
|
697
|
+
/brands/{id}/guidelines:
|
|
698
|
+
get:
|
|
699
|
+
operationId: getBrandGuidelines
|
|
700
|
+
summary: Download the brand guidelines PDF.
|
|
701
|
+
parameters:
|
|
702
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
703
|
+
responses:
|
|
704
|
+
"302": { description: Redirect to signed CDN URL }
|
|
705
|
+
"404": { description: Not found - the brand hasn't expanded this asset yet }
|
|
706
|
+
"409": { description: Brand not ready }
|
|
707
|
+
/jobs:
|
|
708
|
+
get:
|
|
709
|
+
operationId: listJobs
|
|
710
|
+
summary: List generation jobs in the workspace.
|
|
711
|
+
parameters:
|
|
712
|
+
- { name: status, in: query, schema: { type: string, enum: [queued, running, completed, failed, canceled] } }
|
|
713
|
+
- { name: brand_id, in: query, schema: { type: string } }
|
|
714
|
+
- { name: cursor, in: query, schema: { type: string } }
|
|
715
|
+
responses:
|
|
716
|
+
"200":
|
|
717
|
+
description: Paginated jobs
|
|
718
|
+
headers:
|
|
719
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
720
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
721
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
722
|
+
content:
|
|
723
|
+
application/json:
|
|
724
|
+
schema:
|
|
725
|
+
type: object
|
|
726
|
+
properties:
|
|
727
|
+
data:
|
|
728
|
+
type: array
|
|
729
|
+
items: { $ref: "#/components/schemas/GenerationJob" }
|
|
730
|
+
next_cursor: { type: string, nullable: true }
|
|
731
|
+
has_more: { type: boolean }
|
|
732
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
733
|
+
/jobs/{id}:
|
|
734
|
+
get:
|
|
735
|
+
operationId: getJob
|
|
736
|
+
summary: Get job status and per-step progress.
|
|
737
|
+
parameters:
|
|
738
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
739
|
+
responses:
|
|
740
|
+
"200":
|
|
741
|
+
description: GenerationJob object
|
|
742
|
+
headers:
|
|
743
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
744
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
745
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
746
|
+
content:
|
|
747
|
+
application/json:
|
|
748
|
+
schema: { $ref: "#/components/schemas/JobEnvelope" }
|
|
749
|
+
"404": { description: Not found }
|
|
750
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
751
|
+
/jobs/{id}/events:
|
|
752
|
+
get:
|
|
753
|
+
operationId: streamJobEvents
|
|
754
|
+
summary: Subscribe to the job's SSE event stream (text/event-stream).
|
|
755
|
+
description: >-
|
|
756
|
+
Emits `stream.connected`, then `job.step.started`/`job.step.completed`
|
|
757
|
+
(skipped steps carry `"skipped": true`), interleaved with `asset.ready`
|
|
758
|
+
(payload `{asset_id, kind}`) whenever a generated asset becomes
|
|
759
|
+
available mid-pipeline - e.g. the logo concepts a picker step needs to
|
|
760
|
+
render, before the job as a whole finishes. A single asset can
|
|
761
|
+
independently emit `asset.failed` (`{kind, error}`) or
|
|
762
|
+
`asset.insufficient_credits` (`{kind, required, available}`) without
|
|
763
|
+
ending the stream, and `asset.retrying` (`{kind}`) when a retry signal
|
|
764
|
+
is accepted. `assets.pending_retry` (`{brand_id, kinds}`) is a one-off
|
|
765
|
+
nudge, a couple of hours in, that those kinds are still waiting to be
|
|
766
|
+
retried before the free in-job retry window closes. The stream finally
|
|
767
|
+
ends with `job.completed`,
|
|
768
|
+
`job.completed_with_errors` (`{failed_kinds}`, when one or more assets
|
|
769
|
+
were never resolved), or `job.failed`, after which the stream closes.
|
|
770
|
+
Event ids are Redis stream entry ids; reconnect with Last-Event-ID to
|
|
771
|
+
resume without gaps. Comment lines (`: ping`) are heartbeats. Clients
|
|
772
|
+
must close the EventSource after a terminal event - the server ends
|
|
773
|
+
the stream and a reconnect would only replay it.
|
|
774
|
+
parameters:
|
|
775
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
776
|
+
- { name: Last-Event-ID, in: header, schema: { type: string } }
|
|
777
|
+
responses:
|
|
778
|
+
"200": { description: text/event-stream }
|
|
779
|
+
"404": { description: Not found }
|
|
780
|
+
/jobs/{id}/cancel:
|
|
781
|
+
post:
|
|
782
|
+
operationId: cancelJob
|
|
783
|
+
summary: Cancel a running job; the reservation is refunded on the ledger.
|
|
784
|
+
parameters:
|
|
785
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
786
|
+
responses:
|
|
787
|
+
"200":
|
|
788
|
+
description: GenerationJob object (canceling/canceled)
|
|
789
|
+
headers:
|
|
790
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
791
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
792
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
793
|
+
content:
|
|
794
|
+
application/json:
|
|
795
|
+
schema: { $ref: "#/components/schemas/JobEnvelope" }
|
|
796
|
+
"404": { description: Not found }
|
|
797
|
+
"409": { description: Job already finished }
|
|
798
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
799
|
+
/jobs/{id}/logo-selection:
|
|
800
|
+
post:
|
|
801
|
+
operationId: selectLogoConcept
|
|
802
|
+
summary: Approve one of the generated logo concepts; resumes the paused workflow.
|
|
803
|
+
parameters:
|
|
804
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
805
|
+
requestBody:
|
|
806
|
+
required: true
|
|
807
|
+
content:
|
|
808
|
+
application/json:
|
|
809
|
+
schema: { $ref: "#/components/schemas/LogoSelectionRequest" }
|
|
810
|
+
responses:
|
|
811
|
+
"200":
|
|
812
|
+
description: >-
|
|
813
|
+
GenerationJob object (signal sent; the logo step transitions to
|
|
814
|
+
completed asynchronously once the workflow processes the
|
|
815
|
+
selection)
|
|
816
|
+
headers:
|
|
817
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
818
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
819
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
820
|
+
content:
|
|
821
|
+
application/json:
|
|
822
|
+
schema: { $ref: "#/components/schemas/JobEnvelope" }
|
|
823
|
+
"404": { description: Not found }
|
|
824
|
+
"409": { description: Job is not currently awaiting a logo selection }
|
|
825
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
826
|
+
/jobs/{id}/logo-concepts/{assetVersionId}/preview:
|
|
827
|
+
get:
|
|
828
|
+
operationId: previewLogoConceptLockup
|
|
829
|
+
summary: >-
|
|
830
|
+
A free, instant redirect to a preview render of one logo concept in a
|
|
831
|
+
different arrangement and/or font - never persisted, never charged,
|
|
832
|
+
no Idempotency-Key. Query params are both optional; omitting one
|
|
833
|
+
previews with that concept's own current value.
|
|
834
|
+
parameters:
|
|
835
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
836
|
+
- { name: assetVersionId, in: path, required: true, schema: { type: string } }
|
|
837
|
+
- { name: lockup, in: query, schema: { type: string, enum: [horizontal, stacked] } }
|
|
838
|
+
- { name: family, in: query, schema: { type: string } }
|
|
839
|
+
- { name: ink, in: query, schema: { type: string } }
|
|
840
|
+
- { name: accent, in: query, schema: { type: string } }
|
|
841
|
+
- { name: style_seed, in: query, schema: { type: string }, description: "Free-form nonce for the 'shuffle wordmark style' control - a fresh value re-rolls which of a category's 5 treatments this render uses." }
|
|
842
|
+
- { name: redirect, in: query, schema: { type: boolean, default: true } }
|
|
843
|
+
responses:
|
|
844
|
+
"200":
|
|
845
|
+
description: Signed CDN URL as JSON (only when redirect=false)
|
|
846
|
+
content:
|
|
847
|
+
application/json:
|
|
848
|
+
schema:
|
|
849
|
+
type: object
|
|
850
|
+
required: [url]
|
|
851
|
+
properties: { url: { type: string } }
|
|
852
|
+
"302": { description: Redirect to a signed CDN URL for the rendered preview (default) }
|
|
853
|
+
"404": { description: Not found }
|
|
854
|
+
"422": { description: Invalid lockup value }
|
|
855
|
+
"503": { description: The preview render could not be started - try again }
|
|
856
|
+
/jobs/{id}/logo-concepts/{assetVersionId}/palette:
|
|
857
|
+
get:
|
|
858
|
+
operationId: getLogoConceptPalette
|
|
859
|
+
summary: >-
|
|
860
|
+
The brand's generated DNA palette tokens, for the picker's colour
|
|
861
|
+
override swatches - free, read-only.
|
|
862
|
+
parameters:
|
|
863
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
864
|
+
- { name: assetVersionId, in: path, required: true, schema: { type: string } }
|
|
865
|
+
responses:
|
|
866
|
+
"200":
|
|
867
|
+
description: LogoConceptPalette object
|
|
868
|
+
content:
|
|
869
|
+
application/json:
|
|
870
|
+
schema: { $ref: "#/components/schemas/LogoConceptPalette" }
|
|
871
|
+
"404": { description: Not found }
|
|
872
|
+
/jobs/{id}/assets/{kind}/retry:
|
|
873
|
+
post:
|
|
874
|
+
operationId: retryJobAsset
|
|
875
|
+
summary: Retry one failed or credit-blocked asset within an in-progress generation job, for free.
|
|
876
|
+
parameters:
|
|
877
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
878
|
+
- { name: kind, in: path, required: true, schema: { type: string } }
|
|
879
|
+
- { name: Idempotency-Key, in: header, required: true, schema: { type: string } }
|
|
880
|
+
responses:
|
|
881
|
+
"202":
|
|
882
|
+
description: Retry signal accepted.
|
|
883
|
+
headers:
|
|
884
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
885
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
886
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
887
|
+
content:
|
|
888
|
+
application/json:
|
|
889
|
+
schema: { $ref: "#/components/schemas/JobEnvelope" }
|
|
890
|
+
"404": { description: Not found, or the asset kind is not part of this job }
|
|
891
|
+
"409": { description: The asset is not in a retryable state (already generating/ready), or the job has already finalized - `code` distinguishes these }
|
|
892
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
893
|
+
/assets/{id}:
|
|
894
|
+
get:
|
|
895
|
+
operationId: getAsset
|
|
896
|
+
summary: Retrieve a single asset with its current version.
|
|
897
|
+
parameters:
|
|
898
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
899
|
+
responses:
|
|
900
|
+
"200":
|
|
901
|
+
description: Asset object
|
|
902
|
+
headers:
|
|
903
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
904
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
905
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
906
|
+
content:
|
|
907
|
+
application/json:
|
|
908
|
+
schema:
|
|
909
|
+
type: object
|
|
910
|
+
properties:
|
|
911
|
+
asset: { $ref: "#/components/schemas/Asset" }
|
|
912
|
+
"404": { description: Not found }
|
|
913
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
914
|
+
delete:
|
|
915
|
+
operationId: deleteAsset
|
|
916
|
+
summary: Remove an asset from the brand library.
|
|
917
|
+
parameters:
|
|
918
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
919
|
+
responses:
|
|
920
|
+
"204": { description: Deleted }
|
|
921
|
+
"404": { description: Not found }
|
|
922
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
923
|
+
/assets/{id}/qa:
|
|
924
|
+
get:
|
|
925
|
+
operationId: getAssetQaReport
|
|
926
|
+
summary: Get the Pixel Check QA report (per-dimension scores).
|
|
927
|
+
description: >-
|
|
928
|
+
Supported for API consumers regardless of what the Idelio app UI
|
|
929
|
+
currently renders. No in-app screen shows this report today - see
|
|
930
|
+
QaReport's own description for why - but the endpoint and the data
|
|
931
|
+
it returns are a stable, intentional part of the public surface, the
|
|
932
|
+
same status GET /ai-teams holds while its own in-app picker is
|
|
933
|
+
parked.
|
|
934
|
+
parameters:
|
|
935
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
936
|
+
responses:
|
|
937
|
+
"200":
|
|
938
|
+
description: QA report
|
|
939
|
+
headers:
|
|
940
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
941
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
942
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
943
|
+
content:
|
|
944
|
+
application/json:
|
|
945
|
+
schema:
|
|
946
|
+
type: object
|
|
947
|
+
properties:
|
|
948
|
+
report:
|
|
949
|
+
allOf: [{ $ref: "#/components/schemas/QaReport" }]
|
|
950
|
+
nullable: true
|
|
951
|
+
"404": { description: Not found }
|
|
952
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
953
|
+
/assets/{id}/download:
|
|
954
|
+
get:
|
|
955
|
+
operationId: downloadAsset
|
|
956
|
+
summary: Redirect to a time-limited signed CDN URL, or return it as JSON
|
|
957
|
+
with `redirect=false`.
|
|
958
|
+
description: |
|
|
959
|
+
`redirect=false` returns `{ url }` as JSON instead of a 302. A
|
|
960
|
+
browser `fetch()` that then requests that URL directly - rather than
|
|
961
|
+
following our own 302 to it - keeps its real Origin header on the
|
|
962
|
+
request to the CDN: on a same-origin-to-cross-origin 302, browsers
|
|
963
|
+
replace Origin with "null" for the follow-up request, which the
|
|
964
|
+
CDN's CORS policy can never allow-list, so the redirect path fails
|
|
965
|
+
CORS regardless of how the bucket is configured (confirmed live
|
|
966
|
+
2026-09-17). A plain link or `<img>` navigation is not subject to
|
|
967
|
+
CORS at all and should keep using the default redirect.
|
|
968
|
+
parameters:
|
|
969
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
970
|
+
- name: format
|
|
971
|
+
in: query
|
|
972
|
+
description: >-
|
|
973
|
+
An extension (`png`, `svg`) or a file's full label from the
|
|
974
|
+
version's `files` (`png-favicon-32`). With neither `format` nor
|
|
975
|
+
`size`, the SVG is served when the asset has one.
|
|
976
|
+
schema: { type: string }
|
|
977
|
+
- name: size
|
|
978
|
+
in: query
|
|
979
|
+
description: >-
|
|
980
|
+
Picks a pre-rendered size - favicon 16/32/48/512, app icon
|
|
981
|
+
64/128/192/512/1024 - and implies `format=png` when `format` is
|
|
982
|
+
omitted. Nothing is resized on request: a size the asset was not
|
|
983
|
+
rendered at answers 415 `unsupported_size`, naming the sizes it has.
|
|
984
|
+
schema: { type: integer, minimum: 1 }
|
|
985
|
+
- { name: redirect, in: query, schema: { type: boolean, default: true } }
|
|
986
|
+
responses:
|
|
987
|
+
"200":
|
|
988
|
+
description: Signed CDN URL as JSON (only when redirect=false)
|
|
989
|
+
content:
|
|
990
|
+
application/json:
|
|
991
|
+
schema:
|
|
992
|
+
type: object
|
|
993
|
+
required: [url]
|
|
994
|
+
properties: { url: { type: string } }
|
|
995
|
+
"302": { description: Redirect to signed CDN URL (default) }
|
|
996
|
+
"404": { description: Not found }
|
|
997
|
+
"415": { description: "The asset has no file in that format (`unsupported_format`) or was not rendered at that size (`unsupported_size`)" }
|
|
998
|
+
"422": { description: "`size` is not a positive whole number" }
|
|
999
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1000
|
+
/assets/{id}/versions/{versionId}/download:
|
|
1001
|
+
get:
|
|
1002
|
+
operationId: downloadAssetVersion
|
|
1003
|
+
summary: Redirect to a time-limited signed CDN URL for a SPECIFIC version - unlike
|
|
1004
|
+
/assets/{id}/download (always the current version), this previews any version of
|
|
1005
|
+
this asset, selected or not (e.g. a logo concept awaiting the human pick).
|
|
1006
|
+
`redirect=false` returns it as JSON instead - see /assets/{id}/download.
|
|
1007
|
+
parameters:
|
|
1008
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
1009
|
+
- { name: versionId, in: path, required: true, schema: { type: string } }
|
|
1010
|
+
- name: format
|
|
1011
|
+
in: query
|
|
1012
|
+
description: >-
|
|
1013
|
+
An extension (`png`, `svg`) or a file's full label from the
|
|
1014
|
+
version's `files` (`png-favicon-32`). With neither `format` nor
|
|
1015
|
+
`size`, the SVG is served when the asset has one.
|
|
1016
|
+
schema: { type: string }
|
|
1017
|
+
- name: size
|
|
1018
|
+
in: query
|
|
1019
|
+
description: >-
|
|
1020
|
+
Picks a pre-rendered size - favicon 16/32/48/512, app icon
|
|
1021
|
+
64/128/192/512/1024 - and implies `format=png` when `format` is
|
|
1022
|
+
omitted. Nothing is resized on request: a size the asset was not
|
|
1023
|
+
rendered at answers 415 `unsupported_size`, naming the sizes it has.
|
|
1024
|
+
schema: { type: integer, minimum: 1 }
|
|
1025
|
+
- { name: redirect, in: query, schema: { type: boolean, default: true } }
|
|
1026
|
+
responses:
|
|
1027
|
+
"200":
|
|
1028
|
+
description: Signed CDN URL as JSON (only when redirect=false)
|
|
1029
|
+
content:
|
|
1030
|
+
application/json:
|
|
1031
|
+
schema:
|
|
1032
|
+
type: object
|
|
1033
|
+
required: [url]
|
|
1034
|
+
properties: { url: { type: string } }
|
|
1035
|
+
"302": { description: Redirect to signed CDN URL (default) }
|
|
1036
|
+
"404": { description: Not found }
|
|
1037
|
+
"415": { description: "The asset has no file in that format (`unsupported_format`) or was not rendered at that size (`unsupported_size`)" }
|
|
1038
|
+
"422": { description: "`size` is not a positive whole number" }
|
|
1039
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1040
|
+
/assets/{id}/download-all:
|
|
1041
|
+
get:
|
|
1042
|
+
operationId: downloadAllAssetFiles
|
|
1043
|
+
summary: Zip every file of the current version together and redirect to a
|
|
1044
|
+
time-limited signed CDN URL for the ZIP - unlike /assets/{id}/download
|
|
1045
|
+
(one file, chosen by `format`), this bundles all of them, one entry
|
|
1046
|
+
per file, so "Download all files" stops silently picking one.
|
|
1047
|
+
`redirect=false` returns it as JSON instead - see /assets/{id}/download.
|
|
1048
|
+
parameters:
|
|
1049
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
1050
|
+
- { name: redirect, in: query, schema: { type: boolean, default: true } }
|
|
1051
|
+
responses:
|
|
1052
|
+
"200":
|
|
1053
|
+
description: Signed CDN URL as JSON (only when redirect=false)
|
|
1054
|
+
content:
|
|
1055
|
+
application/json:
|
|
1056
|
+
schema:
|
|
1057
|
+
type: object
|
|
1058
|
+
required: [url]
|
|
1059
|
+
properties: { url: { type: string } }
|
|
1060
|
+
"302": { description: Redirect to a signed CDN URL for the ZIP (default) }
|
|
1061
|
+
"404": { description: Not found }
|
|
1062
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1063
|
+
"503": { description: Packaging failed or timed out }
|
|
1064
|
+
/assets/{id}/versions:
|
|
1065
|
+
get:
|
|
1066
|
+
operationId: listAssetVersions
|
|
1067
|
+
summary: List an asset's versions (e.g. logo concepts awaiting selection, or version history).
|
|
1068
|
+
parameters:
|
|
1069
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
1070
|
+
responses:
|
|
1071
|
+
"200":
|
|
1072
|
+
description: Paginated asset versions
|
|
1073
|
+
headers:
|
|
1074
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1075
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1076
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1077
|
+
content:
|
|
1078
|
+
application/json:
|
|
1079
|
+
schema:
|
|
1080
|
+
type: object
|
|
1081
|
+
properties:
|
|
1082
|
+
data:
|
|
1083
|
+
type: array
|
|
1084
|
+
items: { $ref: "#/components/schemas/AssetVersion" }
|
|
1085
|
+
next_cursor: { type: string, nullable: true }
|
|
1086
|
+
has_more: { type: boolean }
|
|
1087
|
+
"404": { description: Not found }
|
|
1088
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1089
|
+
/assets/{id}/regenerate:
|
|
1090
|
+
post:
|
|
1091
|
+
operationId: regenerateAsset
|
|
1092
|
+
summary: Regenerate an asset's concepts (2 free per asset, then 25% of its base price).
|
|
1093
|
+
parameters:
|
|
1094
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
1095
|
+
- { name: Idempotency-Key, in: header, required: true, schema: { type: string } }
|
|
1096
|
+
responses:
|
|
1097
|
+
"202":
|
|
1098
|
+
description: GenerationJob object (queued)
|
|
1099
|
+
headers:
|
|
1100
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1101
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1102
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1103
|
+
content:
|
|
1104
|
+
application/json:
|
|
1105
|
+
schema: { $ref: "#/components/schemas/JobEnvelope" }
|
|
1106
|
+
"402": { description: Insufficient credits }
|
|
1107
|
+
"404": { description: Not found }
|
|
1108
|
+
"409": { description: This workspace already has its plan's worth of generations running - `code` is `too_many_running_jobs` }
|
|
1109
|
+
"422": { description: "This build cannot price this asset's kind - `code` is `asset_kind_not_priceable`" }
|
|
1110
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1111
|
+
"501": { description: This asset kind is not regenerable yet }
|
|
1112
|
+
/assets/{id}/refine:
|
|
1113
|
+
post:
|
|
1114
|
+
operationId: refineAsset
|
|
1115
|
+
summary: Refine an asset - a free-text instruction, or font/accent/style overrides for `logo_wordmark`.
|
|
1116
|
+
description: >-
|
|
1117
|
+
For `logo`, sends a free-text instruction. For `logo_wordmark`, sends
|
|
1118
|
+
either a free-text instruction or structured treatment overrides (font
|
|
1119
|
+
family, accent color, style seed) - an instruction is resolved to
|
|
1120
|
+
exactly those three fields, constrained to the font catalog the
|
|
1121
|
+
platform can render and to the brand's own palette, so it never
|
|
1122
|
+
becomes an image prompt. `logo_wordmark` follows the same pricing rule
|
|
1123
|
+
as other assets: first 2 refinements are free, then 25% of the asset's
|
|
1124
|
+
base price per refinement. A `logo_wordmark` instruction that turns
|
|
1125
|
+
out to change the font on a brand whose typography is locked, or that
|
|
1126
|
+
resolves to no change at all, is refused by the job rather than
|
|
1127
|
+
applied in part: the job ends `failed`, its failure event carries the
|
|
1128
|
+
reason, and the reservation is refunded in full.
|
|
1129
|
+
parameters:
|
|
1130
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
1131
|
+
- { name: Idempotency-Key, in: header, required: true, schema: { type: string } }
|
|
1132
|
+
requestBody:
|
|
1133
|
+
required: true
|
|
1134
|
+
content:
|
|
1135
|
+
application/json:
|
|
1136
|
+
schema: { $ref: "#/components/schemas/RefineRequest" }
|
|
1137
|
+
responses:
|
|
1138
|
+
"202":
|
|
1139
|
+
description: GenerationJob object (queued)
|
|
1140
|
+
headers:
|
|
1141
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1142
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1143
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1144
|
+
content:
|
|
1145
|
+
application/json:
|
|
1146
|
+
schema: { $ref: "#/components/schemas/JobEnvelope" }
|
|
1147
|
+
"404": { description: Not found }
|
|
1148
|
+
"409": { description: "This workspace already has its plan's worth of generations running (`code` is `too_many_running_jobs`), or a structured `family` override was sent for a brand whose typography is locked (`code` is `typography_locked`)" }
|
|
1149
|
+
"422": { description: "Invalid instruction, or this build cannot price this asset's kind - `code` is `asset_kind_not_priceable`" }
|
|
1150
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1151
|
+
"501": { description: This asset kind is not regenerable yet }
|
|
1152
|
+
/assets/{id}/logo-treatment:
|
|
1153
|
+
get:
|
|
1154
|
+
operationId: getLogoTreatment
|
|
1155
|
+
summary: Read a wordmark/logo's current treatment (font, accent, style).
|
|
1156
|
+
tags: [Assets]
|
|
1157
|
+
security: [{ bearerAuth: [] }, { apiKeyAuth: [] }]
|
|
1158
|
+
parameters:
|
|
1159
|
+
- { name: id, in: path, required: true, schema: { type: string }, description: A `logo_wordmark` asset's id. }
|
|
1160
|
+
responses:
|
|
1161
|
+
"200":
|
|
1162
|
+
description: The brand's current logo treatment.
|
|
1163
|
+
content:
|
|
1164
|
+
application/json:
|
|
1165
|
+
schema: { $ref: "#/components/schemas/LogoTreatment" }
|
|
1166
|
+
"404":
|
|
1167
|
+
$ref: "#/components/responses/NotFound"
|
|
1168
|
+
/assets/{id}/logo-treatment/preview:
|
|
1169
|
+
get:
|
|
1170
|
+
operationId: previewLogoTreatment
|
|
1171
|
+
summary: >-
|
|
1172
|
+
A free, instant redirect to a preview render of a wordmark with
|
|
1173
|
+
different font/accent/style - never persisted, never charged, no
|
|
1174
|
+
Idempotency-Key. Query params are optional; omitting one previews with
|
|
1175
|
+
that treatment's current value.
|
|
1176
|
+
tags: [Assets]
|
|
1177
|
+
security: [{ bearerAuth: [] }, { apiKeyAuth: [] }]
|
|
1178
|
+
parameters:
|
|
1179
|
+
- { name: id, in: path, required: true, schema: { type: string }, description: A `logo_wordmark` asset's id. }
|
|
1180
|
+
- { name: family, in: query, schema: { type: string }, description: Font family name. }
|
|
1181
|
+
- { name: ink, in: query, schema: { type: string, pattern: "^#[0-9a-fA-F]{6}$" }, description: Hex name (ink) color. }
|
|
1182
|
+
- { name: accent, in: query, schema: { type: string, pattern: "^#[0-9a-fA-F]{6}$" }, description: Hex accent color. }
|
|
1183
|
+
- { name: style_seed, in: query, schema: { type: string }, description: Free-form nonce for shuffling wordmark style - a fresh value re-rolls which of a category's treatments this render uses. }
|
|
1184
|
+
- { name: style_index, in: query, schema: { type: integer, minimum: 0, maximum: 10 }, description: "Names one of the category's 11 wordmark treatments outright, instead of re-rolling for one. Preferred over style_seed." }
|
|
1185
|
+
- { name: redirect, in: query, schema: { type: boolean, default: true }, description: When true (default), redirect to the preview URL; when false, return the URL as JSON. }
|
|
1186
|
+
responses:
|
|
1187
|
+
"200":
|
|
1188
|
+
description: Signed CDN URL as JSON (only when redirect=false).
|
|
1189
|
+
content:
|
|
1190
|
+
application/json:
|
|
1191
|
+
schema:
|
|
1192
|
+
type: object
|
|
1193
|
+
required: [url]
|
|
1194
|
+
properties:
|
|
1195
|
+
url: { type: string }
|
|
1196
|
+
"302":
|
|
1197
|
+
description: Redirect to a signed CDN URL for the rendered preview (default).
|
|
1198
|
+
"404":
|
|
1199
|
+
$ref: "#/components/responses/NotFound"
|
|
1200
|
+
"422":
|
|
1201
|
+
description: Invalid query parameters.
|
|
1202
|
+
"503":
|
|
1203
|
+
description: The preview render could not be started - try again.
|
|
1204
|
+
/assets/{id}/variations:
|
|
1205
|
+
post:
|
|
1206
|
+
operationId: variationsAsset
|
|
1207
|
+
summary: Generate a curated preset variation of a logo (always logo-kind, counts against the 2-free-regens budget).
|
|
1208
|
+
parameters:
|
|
1209
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
1210
|
+
- { name: Idempotency-Key, in: header, required: true, schema: { type: string } }
|
|
1211
|
+
requestBody:
|
|
1212
|
+
required: true
|
|
1213
|
+
content:
|
|
1214
|
+
application/json:
|
|
1215
|
+
schema: { $ref: "#/components/schemas/VariationRequest" }
|
|
1216
|
+
responses:
|
|
1217
|
+
"202":
|
|
1218
|
+
description: GenerationJob object (queued)
|
|
1219
|
+
headers:
|
|
1220
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1221
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1222
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1223
|
+
content:
|
|
1224
|
+
application/json:
|
|
1225
|
+
schema: { $ref: "#/components/schemas/JobEnvelope" }
|
|
1226
|
+
"402": { description: Insufficient credits }
|
|
1227
|
+
"404": { description: Not found }
|
|
1228
|
+
"409": { description: "This workspace already has its plan's worth of generations running - `code` is `too_many_running_jobs`" }
|
|
1229
|
+
"422": { description: "Unknown preset, the logo has no approved concept yet, or this build cannot price this asset's kind - `code` is `asset_kind_not_priceable`" }
|
|
1230
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1231
|
+
"501": { description: This asset kind is not regenerable yet }
|
|
1232
|
+
/assets/{id}/restore:
|
|
1233
|
+
post:
|
|
1234
|
+
operationId: restoreAsset
|
|
1235
|
+
summary: Restore a prior asset version as the current version (creates a new version, like git revert - nothing is edited in place).
|
|
1236
|
+
parameters:
|
|
1237
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
1238
|
+
- { name: Idempotency-Key, in: header, required: true, schema: { type: string } }
|
|
1239
|
+
requestBody:
|
|
1240
|
+
required: true
|
|
1241
|
+
content:
|
|
1242
|
+
application/json:
|
|
1243
|
+
schema: { $ref: "#/components/schemas/RestoreRequest" }
|
|
1244
|
+
responses:
|
|
1245
|
+
"201":
|
|
1246
|
+
description: Restored
|
|
1247
|
+
content:
|
|
1248
|
+
application/json:
|
|
1249
|
+
schema:
|
|
1250
|
+
type: object
|
|
1251
|
+
properties:
|
|
1252
|
+
asset: { $ref: "#/components/schemas/Asset" }
|
|
1253
|
+
job:
|
|
1254
|
+
description: Present only when restoring a `logo` version started the free background cascade that re-derives favicon/app_icon/profile_avatar/logo_mono/logo_reversed/logo_alternative from the restored mark. `restore` itself is already complete by the time this response arrives - `job` is purely a handle for a client that wants to know when those derived tiles are caught up too.
|
|
1255
|
+
allOf: [{ $ref: "#/components/schemas/GenerationJob" }]
|
|
1256
|
+
"404": { description: Not found }
|
|
1257
|
+
/credits/balance:
|
|
1258
|
+
get:
|
|
1259
|
+
operationId: getCreditBalance
|
|
1260
|
+
summary: Get the current balance, plan and renewal date.
|
|
1261
|
+
responses:
|
|
1262
|
+
"200":
|
|
1263
|
+
description: Balance summary
|
|
1264
|
+
content:
|
|
1265
|
+
application/json:
|
|
1266
|
+
schema: { $ref: "#/components/schemas/CreditBalance" }
|
|
1267
|
+
/credits/ledger:
|
|
1268
|
+
get:
|
|
1269
|
+
operationId: listCreditLedger
|
|
1270
|
+
summary: List the workspace's append-only credit ledger entries.
|
|
1271
|
+
parameters:
|
|
1272
|
+
- { name: reason, in: query, schema: { type: string, enum: [grant, purchase, reserve, settle, refund, clawback] } }
|
|
1273
|
+
- { name: cursor, in: query, schema: { type: string } }
|
|
1274
|
+
- { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
|
|
1275
|
+
responses:
|
|
1276
|
+
"200":
|
|
1277
|
+
description: Ledger page, newest first
|
|
1278
|
+
content:
|
|
1279
|
+
application/json:
|
|
1280
|
+
schema: { $ref: "#/components/schemas/LedgerPage" }
|
|
1281
|
+
/ai-teams:
|
|
1282
|
+
get:
|
|
1283
|
+
operationId: listAiTeams
|
|
1284
|
+
summary: List the AI team tiers and their credit multipliers.
|
|
1285
|
+
responses:
|
|
1286
|
+
"200":
|
|
1287
|
+
description: AI team tiers
|
|
1288
|
+
content:
|
|
1289
|
+
application/json:
|
|
1290
|
+
schema:
|
|
1291
|
+
type: object
|
|
1292
|
+
properties:
|
|
1293
|
+
data:
|
|
1294
|
+
type: array
|
|
1295
|
+
items: { $ref: "#/components/schemas/AiTeam" }
|
|
1296
|
+
/healthz:
|
|
1297
|
+
get:
|
|
1298
|
+
operationId: healthz
|
|
1299
|
+
summary: Liveness and dependency health probe (unversioned in prod routing).
|
|
1300
|
+
security: []
|
|
1301
|
+
responses:
|
|
1302
|
+
"200": { description: Healthy }
|
|
1303
|
+
"503": { description: Degraded }
|
|
1304
|
+
/waitlist:
|
|
1305
|
+
post:
|
|
1306
|
+
operationId: joinWaitlist
|
|
1307
|
+
summary: Join the public waitlist. No authentication required.
|
|
1308
|
+
security: []
|
|
1309
|
+
requestBody:
|
|
1310
|
+
required: true
|
|
1311
|
+
content:
|
|
1312
|
+
application/json:
|
|
1313
|
+
schema: { $ref: "#/components/schemas/WaitlistSignupRequest" }
|
|
1314
|
+
responses:
|
|
1315
|
+
"200":
|
|
1316
|
+
description: >-
|
|
1317
|
+
Joined (or already on the list - the response is identical
|
|
1318
|
+
either way).
|
|
1319
|
+
content:
|
|
1320
|
+
application/json:
|
|
1321
|
+
schema: { $ref: "#/components/schemas/WaitlistSignupResponse" }
|
|
1322
|
+
"422":
|
|
1323
|
+
description: >-
|
|
1324
|
+
Validation failed - including when marketing_consent is true but
|
|
1325
|
+
privacy_policy_version is missing.
|
|
1326
|
+
"429": { description: Rate limit exceeded }
|
|
1327
|
+
/consent:
|
|
1328
|
+
post:
|
|
1329
|
+
operationId: recordConsent
|
|
1330
|
+
summary: >-
|
|
1331
|
+
Record proof of cookie/privacy/Terms consent, or of what a checkout
|
|
1332
|
+
confirmation step disclosed. Cookie and privacy records need no
|
|
1333
|
+
authentication (client_id is the caller's anonymous/session
|
|
1334
|
+
identifier), and a purchase disclosure is sent with a session so it
|
|
1335
|
+
attributes to the workspace. A terms_acceptance REQUIRES a session and
|
|
1336
|
+
is refused with 401 without one: a record scoped to no workspace could
|
|
1337
|
+
never be read back, so an anonymous one would evidence nothing.
|
|
1338
|
+
security: []
|
|
1339
|
+
requestBody:
|
|
1340
|
+
required: true
|
|
1341
|
+
content:
|
|
1342
|
+
application/json:
|
|
1343
|
+
schema: { $ref: "#/components/schemas/ConsentRequest" }
|
|
1344
|
+
responses:
|
|
1345
|
+
"201":
|
|
1346
|
+
description: Consent recorded.
|
|
1347
|
+
content:
|
|
1348
|
+
application/json:
|
|
1349
|
+
schema: { $ref: "#/components/schemas/ConsentResponse" }
|
|
1350
|
+
"400":
|
|
1351
|
+
description: Validation failed.
|
|
1352
|
+
content:
|
|
1353
|
+
application/json:
|
|
1354
|
+
schema: { $ref: "#/components/schemas/ErrorEnvelope" }
|
|
1355
|
+
"401":
|
|
1356
|
+
description: >-
|
|
1357
|
+
A terms_acceptance was sent without a verifiable session. That type
|
|
1358
|
+
alone is never recorded anonymously - an anonymous row is scoped to
|
|
1359
|
+
no workspace, so nothing can ever read it back.
|
|
1360
|
+
content:
|
|
1361
|
+
application/json:
|
|
1362
|
+
schema: { $ref: "#/components/schemas/ErrorEnvelope" }
|
|
1363
|
+
"422":
|
|
1364
|
+
description: >-
|
|
1365
|
+
A terms_acceptance whose checkbox_text or accepted_versions are not
|
|
1366
|
+
the canonical values the sign-up screen shows.
|
|
1367
|
+
content:
|
|
1368
|
+
application/json:
|
|
1369
|
+
schema: { $ref: "#/components/schemas/ErrorEnvelope" }
|
|
1370
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1371
|
+
/api-keys:
|
|
1372
|
+
get:
|
|
1373
|
+
operationId: listApiKeys
|
|
1374
|
+
summary: List the workspace's API keys (non-revoked only).
|
|
1375
|
+
responses:
|
|
1376
|
+
"200":
|
|
1377
|
+
description: API keys
|
|
1378
|
+
headers:
|
|
1379
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1380
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1381
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1382
|
+
content:
|
|
1383
|
+
application/json:
|
|
1384
|
+
schema:
|
|
1385
|
+
type: object
|
|
1386
|
+
properties:
|
|
1387
|
+
data:
|
|
1388
|
+
type: array
|
|
1389
|
+
items: { $ref: "#/components/schemas/ApiKey" }
|
|
1390
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1391
|
+
post:
|
|
1392
|
+
operationId: createApiKey
|
|
1393
|
+
description: Available to a signed-in app session with at least the admin role. An API key receives 403 `session_required`.
|
|
1394
|
+
security: [{ bearerAuth: [] }]
|
|
1395
|
+
summary: Mint a new API key. The secret is shown only in this response.
|
|
1396
|
+
requestBody:
|
|
1397
|
+
required: true
|
|
1398
|
+
content:
|
|
1399
|
+
application/json:
|
|
1400
|
+
schema: { $ref: "#/components/schemas/CreateApiKeyRequest" }
|
|
1401
|
+
responses:
|
|
1402
|
+
"201":
|
|
1403
|
+
description: The new key, including its one-time secret
|
|
1404
|
+
headers:
|
|
1405
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1406
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1407
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1408
|
+
content:
|
|
1409
|
+
application/json:
|
|
1410
|
+
schema: { $ref: "#/components/schemas/ApiKeyWithSecret" }
|
|
1411
|
+
"403": { description: Caller's role is below admin }
|
|
1412
|
+
"422": { description: Invalid request body }
|
|
1413
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1414
|
+
/api-keys/{id}:
|
|
1415
|
+
delete:
|
|
1416
|
+
operationId: revokeApiKey
|
|
1417
|
+
description: Available to a signed-in app session with at least the admin role. An API key receives 403 `session_required`.
|
|
1418
|
+
security: [{ bearerAuth: [] }]
|
|
1419
|
+
summary: Revoke an API key immediately.
|
|
1420
|
+
parameters:
|
|
1421
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
1422
|
+
responses:
|
|
1423
|
+
"204": { description: Revoked }
|
|
1424
|
+
"403": { description: Caller's role is below admin }
|
|
1425
|
+
"404": { description: No active key with that id }
|
|
1426
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1427
|
+
/usage:
|
|
1428
|
+
get:
|
|
1429
|
+
operationId: getUsage
|
|
1430
|
+
summary: Aggregate API-key usage for the workspace, bucketed by time.
|
|
1431
|
+
parameters:
|
|
1432
|
+
- { name: from, in: query, schema: { type: string, format: date-time } }
|
|
1433
|
+
- { name: to, in: query, schema: { type: string, format: date-time } }
|
|
1434
|
+
- { name: granularity, in: query, schema: { type: string, enum: [day, week, month], default: day } }
|
|
1435
|
+
responses:
|
|
1436
|
+
"200":
|
|
1437
|
+
description: Usage buckets
|
|
1438
|
+
content:
|
|
1439
|
+
application/json:
|
|
1440
|
+
schema: { $ref: "#/components/schemas/UsagePage" }
|
|
1441
|
+
/webhook-endpoints:
|
|
1442
|
+
get:
|
|
1443
|
+
operationId: listWebhookEndpoints
|
|
1444
|
+
summary: List the workspace's outbound webhook endpoints (secrets never included).
|
|
1445
|
+
responses:
|
|
1446
|
+
"200":
|
|
1447
|
+
description: Webhook endpoints
|
|
1448
|
+
headers:
|
|
1449
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1450
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1451
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1452
|
+
content:
|
|
1453
|
+
application/json:
|
|
1454
|
+
schema:
|
|
1455
|
+
type: object
|
|
1456
|
+
properties:
|
|
1457
|
+
data:
|
|
1458
|
+
type: array
|
|
1459
|
+
items: { $ref: "#/components/schemas/WebhookEndpoint" }
|
|
1460
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1461
|
+
post:
|
|
1462
|
+
operationId: createWebhookEndpoint
|
|
1463
|
+
description: A session needs at least the admin role; an API key needs the `webhooks:write` scope.
|
|
1464
|
+
summary: Register a new outbound webhook endpoint. The signing secret is shown only in this response.
|
|
1465
|
+
requestBody:
|
|
1466
|
+
required: true
|
|
1467
|
+
content:
|
|
1468
|
+
application/json:
|
|
1469
|
+
schema: { $ref: "#/components/schemas/CreateWebhookEndpointRequest" }
|
|
1470
|
+
responses:
|
|
1471
|
+
"201":
|
|
1472
|
+
description: The new endpoint, including its one-time signing secret
|
|
1473
|
+
headers:
|
|
1474
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1475
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1476
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1477
|
+
content:
|
|
1478
|
+
application/json:
|
|
1479
|
+
schema: { $ref: "#/components/schemas/WebhookEndpointWithSecret" }
|
|
1480
|
+
"403": { description: Caller's role is below admin }
|
|
1481
|
+
"422": { description: Invalid request body }
|
|
1482
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1483
|
+
/webhook-endpoints/{id}:
|
|
1484
|
+
patch:
|
|
1485
|
+
operationId: updateWebhookEndpoint
|
|
1486
|
+
description: A session needs at least the admin role; an API key needs the `webhooks:write` scope.
|
|
1487
|
+
summary: Update an endpoint's subscribed events, or enable/disable it.
|
|
1488
|
+
parameters:
|
|
1489
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
1490
|
+
requestBody:
|
|
1491
|
+
required: true
|
|
1492
|
+
content:
|
|
1493
|
+
application/json:
|
|
1494
|
+
schema: { $ref: "#/components/schemas/UpdateWebhookEndpointRequest" }
|
|
1495
|
+
responses:
|
|
1496
|
+
"200":
|
|
1497
|
+
description: The updated endpoint
|
|
1498
|
+
headers:
|
|
1499
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1500
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1501
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1502
|
+
content:
|
|
1503
|
+
application/json:
|
|
1504
|
+
schema:
|
|
1505
|
+
type: object
|
|
1506
|
+
properties:
|
|
1507
|
+
endpoint: { $ref: "#/components/schemas/WebhookEndpoint" }
|
|
1508
|
+
"403": { description: Caller's role is below admin }
|
|
1509
|
+
"404": { description: No webhook endpoint with that id }
|
|
1510
|
+
"422": { description: Invalid request body }
|
|
1511
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1512
|
+
delete:
|
|
1513
|
+
operationId: deleteWebhookEndpoint
|
|
1514
|
+
description: >-
|
|
1515
|
+
A session needs at least the admin role; an API key needs the `webhooks:write` scope.
|
|
1516
|
+
The endpoint stops receiving events at once, including retries of a delivery already in
|
|
1517
|
+
flight, and every later call naming it answers 404.
|
|
1518
|
+
summary: Remove a webhook endpoint.
|
|
1519
|
+
parameters:
|
|
1520
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
1521
|
+
responses:
|
|
1522
|
+
"204": { description: Removed }
|
|
1523
|
+
"403": { description: Caller's role is below admin }
|
|
1524
|
+
"404": { description: No webhook endpoint with that id }
|
|
1525
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1526
|
+
/webhook-endpoints/{id}/test:
|
|
1527
|
+
post:
|
|
1528
|
+
operationId: testWebhookEndpoint
|
|
1529
|
+
description: A session needs at least the admin role; an API key needs the `webhooks:write` scope.
|
|
1530
|
+
summary: Dispatch a synthetic test.ping event to the endpoint.
|
|
1531
|
+
parameters:
|
|
1532
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
1533
|
+
responses:
|
|
1534
|
+
"202":
|
|
1535
|
+
description: Delivery dispatched
|
|
1536
|
+
headers:
|
|
1537
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1538
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1539
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1540
|
+
content:
|
|
1541
|
+
application/json:
|
|
1542
|
+
schema:
|
|
1543
|
+
type: object
|
|
1544
|
+
properties:
|
|
1545
|
+
dispatched: { type: boolean }
|
|
1546
|
+
"403": { description: Caller's role is below admin }
|
|
1547
|
+
"404": { description: No webhook endpoint with that id }
|
|
1548
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1549
|
+
/webhooks/stripe:
|
|
1550
|
+
post:
|
|
1551
|
+
operationId: receiveStripeWebhook
|
|
1552
|
+
summary: >-
|
|
1553
|
+
Inbound Stripe event (checkout/invoice/subscription). HMAC-SHA256
|
|
1554
|
+
signed via the Stripe-Signature header - not the same auth as the rest
|
|
1555
|
+
of the API. Drives the credit ledger and workspace plan/entitlements.
|
|
1556
|
+
Stripe is not Merchant of Record: SFER LABS LLC is the seller, so tax
|
|
1557
|
+
and invoicing responsibility sits with us.
|
|
1558
|
+
security: []
|
|
1559
|
+
requestBody:
|
|
1560
|
+
required: true
|
|
1561
|
+
content:
|
|
1562
|
+
application/json:
|
|
1563
|
+
schema: { $ref: "#/components/schemas/StripeWebhookEvent" }
|
|
1564
|
+
responses:
|
|
1565
|
+
"200":
|
|
1566
|
+
description: Received (and processed, or a no-op replay of an already-seen event id)
|
|
1567
|
+
content:
|
|
1568
|
+
application/json:
|
|
1569
|
+
schema:
|
|
1570
|
+
type: object
|
|
1571
|
+
properties:
|
|
1572
|
+
received: { type: boolean }
|
|
1573
|
+
"401":
|
|
1574
|
+
description: Signature verification failed
|
|
1575
|
+
content:
|
|
1576
|
+
application/json:
|
|
1577
|
+
schema: { $ref: "#/components/schemas/ErrorEnvelope" }
|
|
1578
|
+
"422": { description: Payload failed validation (after signature verification) }
|
|
1579
|
+
/webhooks/clerk:
|
|
1580
|
+
post:
|
|
1581
|
+
operationId: receiveClerkWebhook
|
|
1582
|
+
summary: >-
|
|
1583
|
+
Inbound Clerk lifecycle event (organization.deleted,
|
|
1584
|
+
organizationMembership.deleted, ...). Verified via svix
|
|
1585
|
+
(svix-id/svix-timestamp/svix-signature headers) - not the same auth
|
|
1586
|
+
as the rest of the API.
|
|
1587
|
+
security: []
|
|
1588
|
+
requestBody:
|
|
1589
|
+
required: true
|
|
1590
|
+
content:
|
|
1591
|
+
application/json:
|
|
1592
|
+
schema: { type: object, description: "Clerk's own event envelope (opaque - not tightly typed here)." }
|
|
1593
|
+
responses:
|
|
1594
|
+
"200":
|
|
1595
|
+
description: Received (and processed, or a no-op replay of an already-seen event id)
|
|
1596
|
+
content:
|
|
1597
|
+
application/json:
|
|
1598
|
+
schema:
|
|
1599
|
+
type: object
|
|
1600
|
+
properties:
|
|
1601
|
+
received: { type: boolean }
|
|
1602
|
+
"401":
|
|
1603
|
+
description: Signature verification failed
|
|
1604
|
+
content:
|
|
1605
|
+
application/json:
|
|
1606
|
+
schema: { $ref: "#/components/schemas/ErrorEnvelope" }
|
|
1607
|
+
/billing/subscription:
|
|
1608
|
+
get:
|
|
1609
|
+
operationId: getSubscription
|
|
1610
|
+
description: Available to a signed-in app session with at least the admin role. An API key receives 403 `session_required`.
|
|
1611
|
+
security: [{ bearerAuth: [] }]
|
|
1612
|
+
summary: Get the active subscription and entitlements.
|
|
1613
|
+
responses:
|
|
1614
|
+
"200":
|
|
1615
|
+
description: Subscription object
|
|
1616
|
+
headers:
|
|
1617
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1618
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1619
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1620
|
+
content:
|
|
1621
|
+
application/json:
|
|
1622
|
+
schema:
|
|
1623
|
+
type: object
|
|
1624
|
+
properties:
|
|
1625
|
+
subscription:
|
|
1626
|
+
oneOf:
|
|
1627
|
+
- { $ref: "#/components/schemas/Subscription" }
|
|
1628
|
+
- { type: "null" }
|
|
1629
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1630
|
+
/billing/checkout:
|
|
1631
|
+
post:
|
|
1632
|
+
operationId: createCheckout
|
|
1633
|
+
description: Available to a signed-in app session with at least the admin role. An API key receives 403 `session_required`.
|
|
1634
|
+
security: [{ bearerAuth: [] }]
|
|
1635
|
+
summary: Create a checkout session for a plan or credit pack.
|
|
1636
|
+
parameters:
|
|
1637
|
+
- name: Idempotency-Key
|
|
1638
|
+
in: header
|
|
1639
|
+
required: true
|
|
1640
|
+
schema: { type: string }
|
|
1641
|
+
requestBody:
|
|
1642
|
+
required: true
|
|
1643
|
+
content:
|
|
1644
|
+
application/json:
|
|
1645
|
+
schema: { $ref: "#/components/schemas/CheckoutRequest" }
|
|
1646
|
+
responses:
|
|
1647
|
+
"201":
|
|
1648
|
+
description: Checkout session
|
|
1649
|
+
headers:
|
|
1650
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1651
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1652
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1653
|
+
content:
|
|
1654
|
+
application/json:
|
|
1655
|
+
schema:
|
|
1656
|
+
type: object
|
|
1657
|
+
properties:
|
|
1658
|
+
checkout:
|
|
1659
|
+
type: object
|
|
1660
|
+
properties:
|
|
1661
|
+
url: { type: string }
|
|
1662
|
+
"422":
|
|
1663
|
+
description: >
|
|
1664
|
+
Unrecognized price_id, or a success_url whose origin is not the
|
|
1665
|
+
app's own (APP_BASE_URL).
|
|
1666
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1667
|
+
/billing/checkout/{id}:
|
|
1668
|
+
get:
|
|
1669
|
+
operationId: getCheckoutSession
|
|
1670
|
+
security: [{ bearerAuth: [] }]
|
|
1671
|
+
summary: Read whether a checkout has been applied to this workspace yet.
|
|
1672
|
+
description: >
|
|
1673
|
+
Available to a signed-in app session with at least the admin role; an
|
|
1674
|
+
API key receives 403 `session_required`.
|
|
1675
|
+
Reports settlement in Idelio, not at the payment provider. A plan
|
|
1676
|
+
purchase is only `paid` once both the credit grant and the plan change
|
|
1677
|
+
have landed, because they arrive on separate provider webhooks in an
|
|
1678
|
+
order the provider does not guarantee. Answers 404 - never 403 - for
|
|
1679
|
+
an id belonging to another workspace, so the endpoint cannot be used
|
|
1680
|
+
to confirm that an id exists.
|
|
1681
|
+
parameters:
|
|
1682
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
1683
|
+
responses:
|
|
1684
|
+
"200":
|
|
1685
|
+
description: Checkout session
|
|
1686
|
+
headers:
|
|
1687
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1688
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1689
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1690
|
+
content:
|
|
1691
|
+
application/json:
|
|
1692
|
+
schema:
|
|
1693
|
+
type: object
|
|
1694
|
+
properties:
|
|
1695
|
+
checkout: { $ref: "#/components/schemas/CheckoutSession" }
|
|
1696
|
+
"404": { description: No such checkout session for this workspace }
|
|
1697
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1698
|
+
/billing/subscription/change-plan:
|
|
1699
|
+
post:
|
|
1700
|
+
operationId: changeSubscriptionPlan
|
|
1701
|
+
description: Available to a signed-in app session with at least the admin role. An API key receives 403 `session_required`.
|
|
1702
|
+
security: [{ bearerAuth: [] }]
|
|
1703
|
+
summary: >-
|
|
1704
|
+
Switch an existing subscription to a different plan. Upgrades apply
|
|
1705
|
+
immediately (prorated credits granted right away); downgrades are
|
|
1706
|
+
queued for the end of the current billing period and reported back
|
|
1707
|
+
via pending_plan/plan_change_effective_at.
|
|
1708
|
+
parameters:
|
|
1709
|
+
- name: Idempotency-Key
|
|
1710
|
+
in: header
|
|
1711
|
+
required: true
|
|
1712
|
+
schema: { type: string }
|
|
1713
|
+
requestBody:
|
|
1714
|
+
required: true
|
|
1715
|
+
content:
|
|
1716
|
+
application/json:
|
|
1717
|
+
schema: { $ref: "#/components/schemas/ChangePlanRequest" }
|
|
1718
|
+
responses:
|
|
1719
|
+
"200":
|
|
1720
|
+
description: The subscription after applying (or queuing) the change
|
|
1721
|
+
headers:
|
|
1722
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1723
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1724
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1725
|
+
content:
|
|
1726
|
+
application/json:
|
|
1727
|
+
schema:
|
|
1728
|
+
type: object
|
|
1729
|
+
properties:
|
|
1730
|
+
subscription: { $ref: "#/components/schemas/Subscription" }
|
|
1731
|
+
"409": { description: No active subscription to change - use POST /billing/checkout instead }
|
|
1732
|
+
"422": { description: Unrecognized price_id, a top-up price, or the workspace's current plan }
|
|
1733
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1734
|
+
/billing/catalog:
|
|
1735
|
+
get:
|
|
1736
|
+
operationId: getBillingCatalog
|
|
1737
|
+
description: Available to a signed-in app session with at least the admin role. An API key receives 403 `session_required`.
|
|
1738
|
+
security: [{ bearerAuth: [] }]
|
|
1739
|
+
summary: Resolve the active provider's price_id for every plan and top-up pack.
|
|
1740
|
+
responses:
|
|
1741
|
+
"200":
|
|
1742
|
+
description: Provider-agnostic price catalog
|
|
1743
|
+
headers:
|
|
1744
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1745
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1746
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1747
|
+
content:
|
|
1748
|
+
application/json:
|
|
1749
|
+
schema: { $ref: "#/components/schemas/BillingCatalog" }
|
|
1750
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1751
|
+
/billing/portal:
|
|
1752
|
+
post:
|
|
1753
|
+
operationId: createPortalSession
|
|
1754
|
+
description: Available to a signed-in app session with at least the admin role. An API key receives 403 `session_required`.
|
|
1755
|
+
security: [{ bearerAuth: [] }]
|
|
1756
|
+
summary: Create a Stripe customer-portal session.
|
|
1757
|
+
parameters:
|
|
1758
|
+
- name: Idempotency-Key
|
|
1759
|
+
in: header
|
|
1760
|
+
required: true
|
|
1761
|
+
schema: { type: string }
|
|
1762
|
+
responses:
|
|
1763
|
+
"201":
|
|
1764
|
+
description: Portal session URL
|
|
1765
|
+
headers:
|
|
1766
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1767
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1768
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1769
|
+
content:
|
|
1770
|
+
application/json:
|
|
1771
|
+
schema:
|
|
1772
|
+
type: object
|
|
1773
|
+
properties:
|
|
1774
|
+
portal:
|
|
1775
|
+
type: object
|
|
1776
|
+
properties:
|
|
1777
|
+
url: { type: string }
|
|
1778
|
+
"409": { description: No billing customer for this workspace yet }
|
|
1779
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1780
|
+
/billing/payment-method:
|
|
1781
|
+
get:
|
|
1782
|
+
operationId: getPaymentMethod
|
|
1783
|
+
security: [{ bearerAuth: [] }]
|
|
1784
|
+
summary: The card this workspace's charges land on.
|
|
1785
|
+
description: >
|
|
1786
|
+
Available to a signed-in app session with at least the admin role. An API key receives 403 `session_required`.
|
|
1787
|
+
Returns null when there is no card on file - which is the normal state for a workspace
|
|
1788
|
+
that has never paid, and for a saved method that is not a card (PayPal, a bank debit).
|
|
1789
|
+
Null is an answer rather than a failure: render the absence. Never the full number and
|
|
1790
|
+
never a token; this exists so a user can tell WHICH of their cards is on file. Changing
|
|
1791
|
+
it happens in the provider's own portal.
|
|
1792
|
+
responses:
|
|
1793
|
+
"200":
|
|
1794
|
+
description: The card on file, or null
|
|
1795
|
+
headers:
|
|
1796
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1797
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1798
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1799
|
+
content:
|
|
1800
|
+
application/json:
|
|
1801
|
+
schema:
|
|
1802
|
+
type: object
|
|
1803
|
+
properties:
|
|
1804
|
+
payment_method:
|
|
1805
|
+
nullable: true
|
|
1806
|
+
allOf: [{ $ref: "#/components/schemas/PaymentMethod" }]
|
|
1807
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1808
|
+
/billing/invoices:
|
|
1809
|
+
get:
|
|
1810
|
+
operationId: listInvoices
|
|
1811
|
+
security: [{ bearerAuth: [] }]
|
|
1812
|
+
summary: List the workspace's invoices from Stripe.
|
|
1813
|
+
description: >
|
|
1814
|
+
Available to a signed-in app session with at least the admin role. An API key receives 403 `session_required`.
|
|
1815
|
+
Results are newest first. That order is inherited rather than pinned: Stripe's invoice
|
|
1816
|
+
list exposes no ordering parameter and orders by creation date, which is already newest
|
|
1817
|
+
first. Note that `billed_at` is the date the invoice was issued, which is not the key
|
|
1818
|
+
the list is ordered by: an invoice finalized long after it was created carries a
|
|
1819
|
+
`billed_at` later than its position in the list implies. Order by `billed_at`
|
|
1820
|
+
client-side if you need the two to agree.
|
|
1821
|
+
parameters:
|
|
1822
|
+
- { name: cursor, in: query, schema: { type: string } }
|
|
1823
|
+
- { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
|
|
1824
|
+
responses:
|
|
1825
|
+
"200":
|
|
1826
|
+
description: Paginated invoices
|
|
1827
|
+
headers:
|
|
1828
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1829
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1830
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1831
|
+
content:
|
|
1832
|
+
application/json:
|
|
1833
|
+
schema:
|
|
1834
|
+
type: object
|
|
1835
|
+
properties:
|
|
1836
|
+
data:
|
|
1837
|
+
type: array
|
|
1838
|
+
items: { $ref: "#/components/schemas/Invoice" }
|
|
1839
|
+
next_cursor: { type: string, nullable: true }
|
|
1840
|
+
has_more: { type: boolean }
|
|
1841
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1842
|
+
/billing/auto-topup:
|
|
1843
|
+
get:
|
|
1844
|
+
operationId: getAutoTopup
|
|
1845
|
+
security: [{ bearerAuth: [] }]
|
|
1846
|
+
summary: Get the workspace's auto top-up preference.
|
|
1847
|
+
description: >-
|
|
1848
|
+
Available to a signed-in app session with at least the admin role. An API key receives 403 `session_required`.
|
|
1849
|
+
The stored preference only. Nothing acts on it yet: no automatic
|
|
1850
|
+
purchase is made when the balance runs low, for any workspace (#426).
|
|
1851
|
+
responses:
|
|
1852
|
+
"200":
|
|
1853
|
+
description: Auto top-up preference
|
|
1854
|
+
headers:
|
|
1855
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1856
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1857
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1858
|
+
content:
|
|
1859
|
+
application/json:
|
|
1860
|
+
schema:
|
|
1861
|
+
type: object
|
|
1862
|
+
properties:
|
|
1863
|
+
auto_topup: { $ref: "#/components/schemas/AutoTopup" }
|
|
1864
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1865
|
+
patch:
|
|
1866
|
+
operationId: updateAutoTopup
|
|
1867
|
+
security: [{ bearerAuth: [] }]
|
|
1868
|
+
summary: Store the workspace's auto top-up preference.
|
|
1869
|
+
description: >-
|
|
1870
|
+
Available to a signed-in app session with at least the admin role. An API key receives 403 `session_required`.
|
|
1871
|
+
Records the preference and the pack it names. It does NOT arrange a
|
|
1872
|
+
purchase: nothing reads these values, and no credits are ever bought
|
|
1873
|
+
automatically (#426). Do not build against an automatic top-up until
|
|
1874
|
+
that issue closes.
|
|
1875
|
+
requestBody:
|
|
1876
|
+
required: true
|
|
1877
|
+
content:
|
|
1878
|
+
application/json:
|
|
1879
|
+
schema: { $ref: "#/components/schemas/UpdateAutoTopupRequest" }
|
|
1880
|
+
responses:
|
|
1881
|
+
"200":
|
|
1882
|
+
description: Updated auto top-up preference
|
|
1883
|
+
headers:
|
|
1884
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1885
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1886
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1887
|
+
content:
|
|
1888
|
+
application/json:
|
|
1889
|
+
schema:
|
|
1890
|
+
type: object
|
|
1891
|
+
properties:
|
|
1892
|
+
auto_topup: { $ref: "#/components/schemas/AutoTopup" }
|
|
1893
|
+
"422": { description: "pack_credits missing while enabling, or not one of 100/300/1000" }
|
|
1894
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1895
|
+
/templates:
|
|
1896
|
+
get:
|
|
1897
|
+
operationId: listTemplates
|
|
1898
|
+
summary: List available brand templates.
|
|
1899
|
+
parameters:
|
|
1900
|
+
- { name: category, in: query, schema: { type: string } }
|
|
1901
|
+
- { name: cursor, in: query, schema: { type: string } }
|
|
1902
|
+
responses:
|
|
1903
|
+
"200":
|
|
1904
|
+
description: Paginated templates
|
|
1905
|
+
headers:
|
|
1906
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1907
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1908
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1909
|
+
content:
|
|
1910
|
+
application/json:
|
|
1911
|
+
schema:
|
|
1912
|
+
type: object
|
|
1913
|
+
properties:
|
|
1914
|
+
data:
|
|
1915
|
+
type: array
|
|
1916
|
+
items: { $ref: "#/components/schemas/Template" }
|
|
1917
|
+
next_cursor: { type: string, nullable: true }
|
|
1918
|
+
has_more: { type: boolean }
|
|
1919
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1920
|
+
/templates/{id}:
|
|
1921
|
+
get:
|
|
1922
|
+
operationId: getTemplate
|
|
1923
|
+
summary: Retrieve a template definition.
|
|
1924
|
+
parameters:
|
|
1925
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
1926
|
+
responses:
|
|
1927
|
+
"200":
|
|
1928
|
+
description: Template object
|
|
1929
|
+
headers:
|
|
1930
|
+
X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
|
|
1931
|
+
X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
|
|
1932
|
+
X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
|
|
1933
|
+
content:
|
|
1934
|
+
application/json:
|
|
1935
|
+
schema:
|
|
1936
|
+
type: object
|
|
1937
|
+
properties:
|
|
1938
|
+
template: { $ref: "#/components/schemas/Template" }
|
|
1939
|
+
"404": { description: Not found }
|
|
1940
|
+
"429": { $ref: "#/components/responses/RateLimited" }
|
|
1941
|
+
components:
|
|
1942
|
+
securitySchemes:
|
|
1943
|
+
bearerAuth:
|
|
1944
|
+
type: http
|
|
1945
|
+
scheme: bearer
|
|
1946
|
+
apiKeyAuth:
|
|
1947
|
+
type: http
|
|
1948
|
+
scheme: bearer
|
|
1949
|
+
description: >
|
|
1950
|
+
Machine credential `sk_live_...` / `sk_test_...` sent as the Bearer
|
|
1951
|
+
token; scoped per key. A key acts for the workspace, not a person, so it
|
|
1952
|
+
holds no workspace role: operations that need one (API keys, billing,
|
|
1953
|
+
members, workspace settings) are session-only and answer a key with 403
|
|
1954
|
+
`session_required`. Webhook endpoints are the exception - a key manages
|
|
1955
|
+
them with the `webhooks:write` scope.
|
|
1956
|
+
# Per-API-key token-bucket rate limiting. Every route reachable via an API
|
|
1957
|
+
# key returns these three headers on success and a 429 (see the
|
|
1958
|
+
# RateLimited response below) once the caller's bucket is empty.
|
|
1959
|
+
# OpenAPI has no single $ref for a group of headers - reference all three
|
|
1960
|
+
# individually wherever RateLimitLimit is referenced. Applied here to the
|
|
1961
|
+
# api-keys, webhook-endpoints, brands, assets and jobs resource groups (the
|
|
1962
|
+
# ones a machine client actually calls) as a representative, meaningfully
|
|
1963
|
+
# -scoped set; apply to the remaining paths in a follow-up once the
|
|
1964
|
+
# limiter's uniform coverage across all authenticated routes is confirmed
|
|
1965
|
+
# in production.
|
|
1966
|
+
headers:
|
|
1967
|
+
RateLimitLimit:
|
|
1968
|
+
description: Sustained requests-per-minute allowed for the caller's plan.
|
|
1969
|
+
schema: { type: integer }
|
|
1970
|
+
RateLimitRemaining:
|
|
1971
|
+
description: Tokens left in the caller's bucket right now.
|
|
1972
|
+
schema: { type: integer }
|
|
1973
|
+
RateLimitReset:
|
|
1974
|
+
description: Seconds until the bucket is fully refilled back to its burst ceiling.
|
|
1975
|
+
schema: { type: integer }
|
|
1976
|
+
responses:
|
|
1977
|
+
RateLimited:
|
|
1978
|
+
description: Rate limit exceeded - the caller's token bucket is empty.
|
|
1979
|
+
content:
|
|
1980
|
+
application/json:
|
|
1981
|
+
schema: { $ref: "#/components/schemas/ErrorEnvelope" }
|
|
1982
|
+
example:
|
|
1983
|
+
error:
|
|
1984
|
+
type: rate_limit_error
|
|
1985
|
+
code: rate_limited
|
|
1986
|
+
message: Rate limit exceeded. Retry after the interval in Retry-After.
|
|
1987
|
+
request_id: req_01hz3x9k2q
|
|
1988
|
+
docs_url: https://docs.idelio.pro/errors/rate_limited
|
|
1989
|
+
schemas:
|
|
1990
|
+
CreditBalance:
|
|
1991
|
+
type: object
|
|
1992
|
+
properties:
|
|
1993
|
+
balance: { type: integer }
|
|
1994
|
+
plan: { type: string, enum: [free, pro, studio, agency] }
|
|
1995
|
+
monthly_allowance: { type: integer }
|
|
1996
|
+
renews_at: { type: string, format: date-time, nullable: true }
|
|
1997
|
+
LedgerEntry:
|
|
1998
|
+
type: object
|
|
1999
|
+
properties:
|
|
2000
|
+
id: { type: string }
|
|
2001
|
+
delta: { type: integer }
|
|
2002
|
+
reason: { type: string, enum: [grant, purchase, reserve, settle, refund, clawback] }
|
|
2003
|
+
job_id: { type: string, nullable: true }
|
|
2004
|
+
balance_after: { type: integer }
|
|
2005
|
+
created_at: { type: string, format: date-time }
|
|
2006
|
+
LedgerPage:
|
|
2007
|
+
type: object
|
|
2008
|
+
properties:
|
|
2009
|
+
data:
|
|
2010
|
+
type: array
|
|
2011
|
+
items: { $ref: "#/components/schemas/LedgerEntry" }
|
|
2012
|
+
next_cursor: { type: string, nullable: true }
|
|
2013
|
+
has_more: { type: boolean }
|
|
2014
|
+
CreateBrandRequest:
|
|
2015
|
+
type: object
|
|
2016
|
+
properties:
|
|
2017
|
+
prompt: { type: string, minLength: 8 }
|
|
2018
|
+
template_id: { type: string }
|
|
2019
|
+
name: { type: string, minLength: 1, maxLength: 80 }
|
|
2020
|
+
tagline: { type: string, minLength: 1, maxLength: 60, description: What the user typed in the creator - pinned in code rather than suggested to the model }
|
|
2021
|
+
assets:
|
|
2022
|
+
type: array
|
|
2023
|
+
description: >-
|
|
2024
|
+
The asset kinds to generate. Every brand includes five kinds -
|
|
2025
|
+
"logo" (the symbol), "logo_alternative" (the logo lockup),
|
|
2026
|
+
"logo_wordmark", "color_palette" and "font_pairing" - 7 credits
|
|
2027
|
+
at Studio together. Omit the list, or send an empty array, to
|
|
2028
|
+
generate exactly those five. A non-empty list MUST contain all
|
|
2029
|
+
five, and anything else is added on top; a list missing any of
|
|
2030
|
+
them answers 422 `required_assets_missing`. More kinds can be
|
|
2031
|
+
added to an existing brand later with POST /brands/{id}/assets.
|
|
2032
|
+
items: { type: string }
|
|
2033
|
+
team: { type: string, enum: [flash, studio, elite], default: studio }
|
|
2034
|
+
customize:
|
|
2035
|
+
type: object
|
|
2036
|
+
deprecated: true
|
|
2037
|
+
description: >-
|
|
2038
|
+
Superseded by creative_direction, which is typed against the
|
|
2039
|
+
published catalogs instead of accepting free-form records. Still
|
|
2040
|
+
accepted for released SDK versions. Neither block reaches the
|
|
2041
|
+
generation pipeline yet - both are accepted and validated only -
|
|
2042
|
+
so new clients should send creative_direction and stop sending
|
|
2043
|
+
this one.
|
|
2044
|
+
creative_direction: { $ref: "#/components/schemas/CreativeDirection" }
|
|
2045
|
+
auto_select_logo:
|
|
2046
|
+
type: boolean
|
|
2047
|
+
default: false
|
|
2048
|
+
description: >
|
|
2049
|
+
Every generation pauses once its logo concepts exist and waits for
|
|
2050
|
+
one to be picked through `POST /jobs/{id}/logo-selection` - for up
|
|
2051
|
+
to 24 hours, after which the job fails and its credits are
|
|
2052
|
+
refunded. `true` picks the first concept on the server instead, so
|
|
2053
|
+
a caller with nobody to choose can poll the job straight to
|
|
2054
|
+
`completed`.
|
|
2055
|
+
CreativeDirection:
|
|
2056
|
+
type: object
|
|
2057
|
+
additionalProperties: false
|
|
2058
|
+
description: >-
|
|
2059
|
+
The creative direction the user chose. Every axis accepts the literal
|
|
2060
|
+
"ai", which means "let the pipeline decide" - an explicit value, not
|
|
2061
|
+
an omitted field. Ids come from the published catalogs in
|
|
2062
|
+
@idelio/contracts; an id outside them is rejected.
|
|
2063
|
+
properties:
|
|
2064
|
+
styles:
|
|
2065
|
+
type: array
|
|
2066
|
+
maxItems: 5
|
|
2067
|
+
items: { type: string, enum: [modern, minimal, bold, playful, elegant, corporate, luxury, organic, geometric, handcrafted, brutalist, editorial, retro, vintage, futuristic, timeless, art_deco, y2k, calm, energetic, warm, technical, rebellious, premium] }
|
|
2068
|
+
color:
|
|
2069
|
+
oneOf:
|
|
2070
|
+
- type: object
|
|
2071
|
+
additionalProperties: false
|
|
2072
|
+
required: [mode]
|
|
2073
|
+
properties:
|
|
2074
|
+
mode: { type: string, enum: [ai] }
|
|
2075
|
+
- type: object
|
|
2076
|
+
additionalProperties: false
|
|
2077
|
+
required: [mode, preset_id]
|
|
2078
|
+
properties:
|
|
2079
|
+
mode: { type: string, enum: [preset] }
|
|
2080
|
+
preset_id: { type: string, enum: [indigo, teal, amber, forest, rose, slate, midnight, coral, sand, plum, ocean, citrus, mono_warm, berry] }
|
|
2081
|
+
- type: object
|
|
2082
|
+
additionalProperties: false
|
|
2083
|
+
required: [mode, colors]
|
|
2084
|
+
properties:
|
|
2085
|
+
mode: { type: string, enum: [custom] }
|
|
2086
|
+
colors:
|
|
2087
|
+
type: array
|
|
2088
|
+
minItems: 3
|
|
2089
|
+
maxItems: 6
|
|
2090
|
+
items: { type: string, pattern: "^#[0-9a-fA-F]{6}$" }
|
|
2091
|
+
typography:
|
|
2092
|
+
oneOf:
|
|
2093
|
+
- type: object
|
|
2094
|
+
additionalProperties: false
|
|
2095
|
+
required: [mode]
|
|
2096
|
+
properties:
|
|
2097
|
+
mode: { type: string, enum: [ai] }
|
|
2098
|
+
- type: object
|
|
2099
|
+
additionalProperties: false
|
|
2100
|
+
required: [mode, preset_id]
|
|
2101
|
+
properties:
|
|
2102
|
+
mode: { type: string, enum: [preset] }
|
|
2103
|
+
preset_id: { type: string, enum: [sora_inter, fraunces_inter, space_inter, playfair_source, dmserif_dmsans, outfit_inter, manrope_manrope, bricolage_inter, instrument_inter, archivo_archivo, lora_lato, syne_inter, epilogue_inter, ibmplex_ibmplex, jost_inter, newsreader_inter, chivo_chivo, unbounded_inter, figtree_figtree, spectral_inter] }
|
|
2104
|
+
logo:
|
|
2105
|
+
type: object
|
|
2106
|
+
additionalProperties: false
|
|
2107
|
+
description: >-
|
|
2108
|
+
Two independent axes. A mark style and a lockup are not
|
|
2109
|
+
alternatives - a brand has one of each. The single impossible
|
|
2110
|
+
pair is rejected: style "wordmark_only" with lockup "icon_only"
|
|
2111
|
+
describes a logo with neither icon nor name.
|
|
2112
|
+
properties:
|
|
2113
|
+
style: { type: string, enum: [ai, geometric, rounded, circular, sharp, monogram, lettermark, abstract, emblem, organic, line, gradient, wordmark_only], default: ai }
|
|
2114
|
+
lockup: { type: string, enum: [ai, horizontal, stacked, icon_only], default: ai }
|
|
2115
|
+
UpdateBrandRequest:
|
|
2116
|
+
type: object
|
|
2117
|
+
properties:
|
|
2118
|
+
name: { type: string, minLength: 1 }
|
|
2119
|
+
slug: { type: string, minLength: 1 }
|
|
2120
|
+
locks: { type: object }
|
|
2121
|
+
ExpandBrandRequest:
|
|
2122
|
+
type: object
|
|
2123
|
+
required: [assets]
|
|
2124
|
+
properties:
|
|
2125
|
+
assets:
|
|
2126
|
+
type: array
|
|
2127
|
+
minItems: 1
|
|
2128
|
+
items: { type: string }
|
|
2129
|
+
team:
|
|
2130
|
+
type: string
|
|
2131
|
+
enum: [flash, studio, elite]
|
|
2132
|
+
custom_image_prompt:
|
|
2133
|
+
type: string
|
|
2134
|
+
minLength: 1
|
|
2135
|
+
maxLength: 500
|
|
2136
|
+
description: Required when assets is exactly ["custom_image"].
|
|
2137
|
+
custom_image_format:
|
|
2138
|
+
type: string
|
|
2139
|
+
enum: [square_1_1, portrait_4_5, story_9_16, landscape_16_9, wide_21_9, photo_3_2, poster_2_3, standard_4_3, auto, instagram_post, story, linkedin_post, ad_banner, custom]
|
|
2140
|
+
custom_image_name:
|
|
2141
|
+
type: string
|
|
2142
|
+
minLength: 1
|
|
2143
|
+
maxLength: 60
|
|
2144
|
+
description: >-
|
|
2145
|
+
Library name for the generated custom asset. Optional - when absent
|
|
2146
|
+
the name is derived from custom_image_prompt. Only valid when assets
|
|
2147
|
+
is exactly ["custom_image"]. Leading and trailing whitespace is
|
|
2148
|
+
trimmed BEFORE minLength/maxLength are applied, so a 64-character
|
|
2149
|
+
value whose padding trims to 58 is accepted, and a value of only
|
|
2150
|
+
spaces is rejected as empty. Control characters (C0 and DEL) are
|
|
2151
|
+
refused.
|
|
2152
|
+
BrandRefineRequest:
|
|
2153
|
+
type: object
|
|
2154
|
+
required: [message]
|
|
2155
|
+
properties:
|
|
2156
|
+
message: { type: string, minLength: 1, maxLength: 500 }
|
|
2157
|
+
QaCheck:
|
|
2158
|
+
type: object
|
|
2159
|
+
description: >-
|
|
2160
|
+
One criterion the Brand Guardian evaluated. `label` travels with the
|
|
2161
|
+
check because the set of criteria is the Guardian's to decide - a client
|
|
2162
|
+
owning the labels would print nothing for a check it had not heard of.
|
|
2163
|
+
required: [id, label, status]
|
|
2164
|
+
properties:
|
|
2165
|
+
id: { type: string }
|
|
2166
|
+
label: { type: string }
|
|
2167
|
+
status: { type: string, enum: [pass, warn, fail] }
|
|
2168
|
+
detail: { type: string, nullable: true, description: "Why, in the user's language." }
|
|
2169
|
+
source: { type: string, enum: [deterministic, ai], description: Which producer wrote this check. }
|
|
2170
|
+
QaReport:
|
|
2171
|
+
type: object
|
|
2172
|
+
description: >-
|
|
2173
|
+
Pixel Check report for one asset version. `overall_score` is required
|
|
2174
|
+
because it already has a consumer: the API derives `Asset.pixel_check`
|
|
2175
|
+
from it. The object is open on purpose - a Guardian that learns to
|
|
2176
|
+
report more must not fail validation in clients that predate it.
|
|
2177
|
+
The Brand Guardian writes this on generation, logo regeneration and
|
|
2178
|
+
DNA refine, so a real report exists on many asset versions - no
|
|
2179
|
+
current app UI surfaces it, but that is a client-side choice, not a
|
|
2180
|
+
sign the field is unused.
|
|
2181
|
+
required: [overall_score]
|
|
2182
|
+
additionalProperties: true
|
|
2183
|
+
properties:
|
|
2184
|
+
overall_score: { type: integer, minimum: 0, maximum: 100 }
|
|
2185
|
+
checks:
|
|
2186
|
+
type: array
|
|
2187
|
+
items: { $ref: "#/components/schemas/QaCheck" }
|
|
2188
|
+
UsageBucket:
|
|
2189
|
+
type: object
|
|
2190
|
+
description: >-
|
|
2191
|
+
One time bucket of API-key usage. `requests` and `credits` are measures
|
|
2192
|
+
on different scales - a bucket can hold many cheap reads or one expensive
|
|
2193
|
+
generation - so a consumer must not plot them against a shared axis.
|
|
2194
|
+
required: [bucket, requests, credits]
|
|
2195
|
+
properties:
|
|
2196
|
+
bucket: { type: string, format: date-time, description: Start of the bucket, RFC 3339 UTC. }
|
|
2197
|
+
requests: { type: integer, minimum: 0 }
|
|
2198
|
+
credits: { type: integer, minimum: 0 }
|
|
2199
|
+
UsagePage:
|
|
2200
|
+
type: object
|
|
2201
|
+
description: >-
|
|
2202
|
+
Not cursor-paginated: the window is bounded by the from/to query rather
|
|
2203
|
+
than by a cursor, so `data` is the whole slice.
|
|
2204
|
+
required: [data]
|
|
2205
|
+
properties:
|
|
2206
|
+
data:
|
|
2207
|
+
type: array
|
|
2208
|
+
items: { $ref: "#/components/schemas/UsageBucket" }
|
|
2209
|
+
Subscription:
|
|
2210
|
+
type: object
|
|
2211
|
+
properties:
|
|
2212
|
+
plan: { type: string, enum: [free, pro, studio, agency] }
|
|
2213
|
+
status: { type: string, nullable: true }
|
|
2214
|
+
next_billed_at: { type: string, format: date-time, nullable: true }
|
|
2215
|
+
cancel_at_period_end: { type: boolean }
|
|
2216
|
+
pending_plan: { type: string, enum: [free, pro, studio, agency], nullable: true }
|
|
2217
|
+
plan_change_effective_at: { type: string, format: date-time, nullable: true }
|
|
2218
|
+
CheckoutSession:
|
|
2219
|
+
type: object
|
|
2220
|
+
required: [id, status, kind, plan, credits, amount, currency, created_at, settled_at]
|
|
2221
|
+
properties:
|
|
2222
|
+
id: { type: string }
|
|
2223
|
+
status: { type: string, enum: [pending, paid, failed, cancelled] }
|
|
2224
|
+
kind: { type: string, enum: [plan, topup] }
|
|
2225
|
+
plan: { type: string, enum: [free, pro, studio, agency], nullable: true }
|
|
2226
|
+
credits: { type: integer, nullable: true }
|
|
2227
|
+
amount:
|
|
2228
|
+
type: string
|
|
2229
|
+
nullable: true
|
|
2230
|
+
description: >-
|
|
2231
|
+
The catalogue list price frozen when this checkout was created -
|
|
2232
|
+
NOT the amount billed. Prices are tax-exclusive, so a customer in
|
|
2233
|
+
a taxed jurisdiction pays this plus tax (#425), and adaptive
|
|
2234
|
+
pricing may convert the currency. Read the invoice's total for the
|
|
2235
|
+
money that actually moved.
|
|
2236
|
+
currency:
|
|
2237
|
+
type: string
|
|
2238
|
+
nullable: true
|
|
2239
|
+
description: Currency of `amount`; USD for every catalogue price today.
|
|
2240
|
+
created_at: { type: string, format: date-time }
|
|
2241
|
+
settled_at: { type: string, format: date-time, nullable: true }
|
|
2242
|
+
CheckoutRequest:
|
|
2243
|
+
type: object
|
|
2244
|
+
required: [price_id]
|
|
2245
|
+
properties:
|
|
2246
|
+
price_id: { type: string, minLength: 1 }
|
|
2247
|
+
success_url:
|
|
2248
|
+
type: string
|
|
2249
|
+
format: uri
|
|
2250
|
+
description: >
|
|
2251
|
+
Absolute URL the provider returns the buyer to. Must be on the
|
|
2252
|
+
app's own origin (APP_BASE_URL); any other origin is rejected
|
|
2253
|
+
with 422 validation_failed rather than followed, so this field
|
|
2254
|
+
cannot be used as an open redirect. Idelio appends its own
|
|
2255
|
+
`checkout` outcome and `ref` query parameters to it.
|
|
2256
|
+
ChangePlanRequest:
|
|
2257
|
+
type: object
|
|
2258
|
+
required: [price_id]
|
|
2259
|
+
properties:
|
|
2260
|
+
price_id: { type: string, minLength: 1 }
|
|
2261
|
+
BillingCatalog:
|
|
2262
|
+
type: object
|
|
2263
|
+
properties:
|
|
2264
|
+
plans:
|
|
2265
|
+
type: object
|
|
2266
|
+
properties:
|
|
2267
|
+
pro: { type: string, nullable: true }
|
|
2268
|
+
studio: { type: string, nullable: true }
|
|
2269
|
+
agency: { type: string, nullable: true }
|
|
2270
|
+
topups:
|
|
2271
|
+
type: object
|
|
2272
|
+
properties:
|
|
2273
|
+
"100": { type: string, nullable: true }
|
|
2274
|
+
"300": { type: string, nullable: true }
|
|
2275
|
+
"1000": { type: string, nullable: true }
|
|
2276
|
+
AutoTopup:
|
|
2277
|
+
type: object
|
|
2278
|
+
properties:
|
|
2279
|
+
enabled: { type: boolean }
|
|
2280
|
+
pack_credits: { type: integer, enum: [100, 300, 1000], nullable: true }
|
|
2281
|
+
UpdateAutoTopupRequest:
|
|
2282
|
+
type: object
|
|
2283
|
+
required: [enabled]
|
|
2284
|
+
properties:
|
|
2285
|
+
enabled: { type: boolean }
|
|
2286
|
+
pack_credits:
|
|
2287
|
+
type: integer
|
|
2288
|
+
enum: [100, 300, 1000]
|
|
2289
|
+
description: Required when enabled is true.
|
|
2290
|
+
Template:
|
|
2291
|
+
type: object
|
|
2292
|
+
properties:
|
|
2293
|
+
id: { type: string }
|
|
2294
|
+
name: { type: string }
|
|
2295
|
+
category: { type: string }
|
|
2296
|
+
description: { type: string }
|
|
2297
|
+
prompt: { type: string }
|
|
2298
|
+
accent: { type: string }
|
|
2299
|
+
PaymentMethod:
|
|
2300
|
+
type: object
|
|
2301
|
+
properties:
|
|
2302
|
+
brand:
|
|
2303
|
+
type: string
|
|
2304
|
+
description: >
|
|
2305
|
+
The card network as the provider names it - "visa", "mastercard", "amex" and so on.
|
|
2306
|
+
Deliberately NOT an enum: providers add networks over time, so render a known mark
|
|
2307
|
+
when you recognise one and fall back to the raw value otherwise. It must stay
|
|
2308
|
+
readable as text, since an unrecognised brand with only an icon is a blank square.
|
|
2309
|
+
last4: { type: string }
|
|
2310
|
+
exp_month: { type: integer, minimum: 1, maximum: 12 }
|
|
2311
|
+
exp_year: { type: integer }
|
|
2312
|
+
Invoice:
|
|
2313
|
+
type: object
|
|
2314
|
+
properties:
|
|
2315
|
+
id: { type: string }
|
|
2316
|
+
status: { type: string }
|
|
2317
|
+
total:
|
|
2318
|
+
type: string
|
|
2319
|
+
nullable: true
|
|
2320
|
+
description: >
|
|
2321
|
+
The amount billed for this invoice after discounts, tax and any customer credit
|
|
2322
|
+
applied, as a string in the MINOR UNITS of `currency` - "2900" with a currency of
|
|
2323
|
+
"USD" is $29.00. Some currencies have no minor unit (JPY, KRW) and some have three
|
|
2324
|
+
(BHD, KWD), so derive the exponent from `currency` and never divide by 100. The
|
|
2325
|
+
integer-minor-unit encoding is established by Stripe's own SDK type, and the
|
|
2326
|
+
post-credit definition is what the provider maps (`amount_due`).
|
|
2327
|
+
currency:
|
|
2328
|
+
type: string
|
|
2329
|
+
nullable: true
|
|
2330
|
+
description: >
|
|
2331
|
+
ISO 4217 currency code. Its case is NOT normalised - Stripe returns it lowercase
|
|
2332
|
+
("usd") - so compare it case-insensitively.
|
|
2333
|
+
billed_at: { type: string, format: date-time, nullable: true }
|
|
2334
|
+
invoice_number: { type: string, nullable: true }
|
|
2335
|
+
url:
|
|
2336
|
+
type: string
|
|
2337
|
+
format: uri
|
|
2338
|
+
nullable: true
|
|
2339
|
+
description: >
|
|
2340
|
+
Provider-hosted page where the customer can read or download this invoice, or null
|
|
2341
|
+
when the provider has not produced one - a draft has no hosted document yet. Render
|
|
2342
|
+
the absence rather than a dead link. The URL carries its own provider session and
|
|
2343
|
+
expires on the provider's terms, so treat it as a redirect target rather than
|
|
2344
|
+
something to embed or store.
|
|
2345
|
+
Workspace:
|
|
2346
|
+
type: object
|
|
2347
|
+
properties:
|
|
2348
|
+
id: { type: string }
|
|
2349
|
+
name: { type: string }
|
|
2350
|
+
plan: { type: string, enum: [free, pro, studio, agency] }
|
|
2351
|
+
settings: { type: object }
|
|
2352
|
+
UpdateWorkspaceRequest:
|
|
2353
|
+
type: object
|
|
2354
|
+
properties:
|
|
2355
|
+
name: { type: string, minLength: 1 }
|
|
2356
|
+
settings: { type: object }
|
|
2357
|
+
Member:
|
|
2358
|
+
type: object
|
|
2359
|
+
properties:
|
|
2360
|
+
id: { type: string }
|
|
2361
|
+
email: { type: string }
|
|
2362
|
+
role: { type: string, enum: [owner, admin, member] }
|
|
2363
|
+
created_at: { type: string, format: date-time }
|
|
2364
|
+
InviteMemberRequest:
|
|
2365
|
+
type: object
|
|
2366
|
+
required: [email, role]
|
|
2367
|
+
properties:
|
|
2368
|
+
email: { type: string, format: email }
|
|
2369
|
+
role: { type: string, enum: [admin, member] }
|
|
2370
|
+
ChangeMemberRoleRequest:
|
|
2371
|
+
type: object
|
|
2372
|
+
required: [role]
|
|
2373
|
+
properties:
|
|
2374
|
+
role: { type: string, enum: [owner, admin, member] }
|
|
2375
|
+
StripeWebhookEvent:
|
|
2376
|
+
type: object
|
|
2377
|
+
required: [id, type]
|
|
2378
|
+
description: >-
|
|
2379
|
+
Stripe's own event envelope - opaque/generic, not tightly typed here
|
|
2380
|
+
since we do not own Stripe's schema. The resource itself is nested
|
|
2381
|
+
under data.object.
|
|
2382
|
+
properties:
|
|
2383
|
+
id: { type: string }
|
|
2384
|
+
type: { type: string }
|
|
2385
|
+
data:
|
|
2386
|
+
type: object
|
|
2387
|
+
properties:
|
|
2388
|
+
object: { type: object }
|
|
2389
|
+
ErrorEnvelope:
|
|
2390
|
+
type: object
|
|
2391
|
+
required: [error]
|
|
2392
|
+
properties:
|
|
2393
|
+
error:
|
|
2394
|
+
type: object
|
|
2395
|
+
required: [type, code, message, request_id]
|
|
2396
|
+
properties:
|
|
2397
|
+
type: { type: string }
|
|
2398
|
+
code: { type: string }
|
|
2399
|
+
message: { type: string }
|
|
2400
|
+
param: { type: string }
|
|
2401
|
+
request_id: { type: string }
|
|
2402
|
+
docs_url: { type: string }
|
|
2403
|
+
Brand:
|
|
2404
|
+
type: object
|
|
2405
|
+
properties:
|
|
2406
|
+
id: { type: string }
|
|
2407
|
+
workspace_id: { type: string }
|
|
2408
|
+
name: { type: string }
|
|
2409
|
+
slug: { type: string }
|
|
2410
|
+
status: { type: string, enum: [draft, generating, ready, failed] }
|
|
2411
|
+
current_version_id: { type: string, nullable: true }
|
|
2412
|
+
locks: { type: object }
|
|
2413
|
+
preview:
|
|
2414
|
+
$ref: "#/components/schemas/BrandPreview"
|
|
2415
|
+
created_at: { type: string, format: date-time }
|
|
2416
|
+
updated_at: { type: string, format: date-time }
|
|
2417
|
+
BrandPreview:
|
|
2418
|
+
type: object
|
|
2419
|
+
description: >
|
|
2420
|
+
Visual summary for list and card views. Absent on reads that do not
|
|
2421
|
+
need it.
|
|
2422
|
+
properties:
|
|
2423
|
+
palette:
|
|
2424
|
+
type: array
|
|
2425
|
+
items: { type: string }
|
|
2426
|
+
description: >
|
|
2427
|
+
Every colour of the brand's current version, in the order its Brand
|
|
2428
|
+
DNA lists them - role-grouped by construction (text, surface, then
|
|
2429
|
+
accents, with any founder-chosen colour appended). The same array
|
|
2430
|
+
GET /brands/{id}/dna serves, so no two surfaces order it
|
|
2431
|
+
differently.
|
|
2432
|
+
cover: { type: string, nullable: true }
|
|
2433
|
+
asset_count: { type: integer, minimum: 0 }
|
|
2434
|
+
logo_url: { type: string, nullable: true }
|
|
2435
|
+
gradient_stops:
|
|
2436
|
+
type: array
|
|
2437
|
+
items: { type: string }
|
|
2438
|
+
description: >
|
|
2439
|
+
The two colours every surface draws this brand's gradient from -
|
|
2440
|
+
the DNA's first two accent-role tokens, the same ones the logo is
|
|
2441
|
+
drawn in. Empty before generation.
|
|
2442
|
+
BrandDna:
|
|
2443
|
+
type: object
|
|
2444
|
+
properties:
|
|
2445
|
+
brief:
|
|
2446
|
+
type: object
|
|
2447
|
+
properties:
|
|
2448
|
+
industry: { type: string }
|
|
2449
|
+
audience: { type: string }
|
|
2450
|
+
tone: { type: string }
|
|
2451
|
+
keywords: { type: array, items: { type: string } }
|
|
2452
|
+
constraints: { type: array, items: { type: string } }
|
|
2453
|
+
strategy:
|
|
2454
|
+
type: object
|
|
2455
|
+
properties:
|
|
2456
|
+
positioning: { type: string }
|
|
2457
|
+
archetype: { type: string }
|
|
2458
|
+
values: { type: array, items: { type: string } }
|
|
2459
|
+
name: { type: string }
|
|
2460
|
+
tagline: { type: string }
|
|
2461
|
+
name_rationale: { type: string }
|
|
2462
|
+
visual_system:
|
|
2463
|
+
type: object
|
|
2464
|
+
properties:
|
|
2465
|
+
palette:
|
|
2466
|
+
type: object
|
|
2467
|
+
properties:
|
|
2468
|
+
tokens:
|
|
2469
|
+
type: array
|
|
2470
|
+
items:
|
|
2471
|
+
type: object
|
|
2472
|
+
properties:
|
|
2473
|
+
name: { type: string }
|
|
2474
|
+
hex: { type: string }
|
|
2475
|
+
typography:
|
|
2476
|
+
type: object
|
|
2477
|
+
properties:
|
|
2478
|
+
heading_font: { type: string }
|
|
2479
|
+
body_font: { type: string }
|
|
2480
|
+
mono_font: { type: string, nullable: true }
|
|
2481
|
+
BrandVersionSummary:
|
|
2482
|
+
type: object
|
|
2483
|
+
properties:
|
|
2484
|
+
id: { type: string }
|
|
2485
|
+
version_no: { type: integer }
|
|
2486
|
+
message: { type: string, nullable: true }
|
|
2487
|
+
created_by: { type: string }
|
|
2488
|
+
qa_score: { type: integer, nullable: true }
|
|
2489
|
+
created_at: { type: string, format: date-time }
|
|
2490
|
+
is_current: { type: boolean }
|
|
2491
|
+
BrandSuggestionItem:
|
|
2492
|
+
type: object
|
|
2493
|
+
properties:
|
|
2494
|
+
kind: { type: string, enum: [logo_alternative, logo_wordmark, logo_mono, logo_reversed, favicon, brand_guide, profile_avatar, social_x, social_linkedin, social_linkedin_personal, social_facebook, social_youtube, social_post, social_story, app_icon, og_image, email_signature, hero_image, play_feature_graphic, business_card, letterhead, mockup, custom_image] }
|
|
2495
|
+
tag: { type: string }
|
|
2496
|
+
title: { type: string }
|
|
2497
|
+
desc: { type: string }
|
|
2498
|
+
state: { type: string, enum: [idle, generating, ready] }
|
|
2499
|
+
asset_id: { type: string, nullable: true }
|
|
2500
|
+
custom:
|
|
2501
|
+
nullable: true
|
|
2502
|
+
description: "Set exactly when kind is custom_image - an idea offered only once the brand owns every catalogue kind it can be offered. It is never generated in one click: prefill POST /brands/{id}/assets (custom_image_name, custom_image_prompt, custom_image_format) and let the user review it."
|
|
2503
|
+
allOf:
|
|
2504
|
+
- $ref: "#/components/schemas/CustomSuggestion"
|
|
2505
|
+
CustomSuggestion:
|
|
2506
|
+
type: object
|
|
2507
|
+
required: [name, prompt, format]
|
|
2508
|
+
properties:
|
|
2509
|
+
name: { type: string, minLength: 1, maxLength: 60 }
|
|
2510
|
+
prompt: { type: string, minLength: 1, maxLength: 500 }
|
|
2511
|
+
format: { type: string, enum: [square_1_1, portrait_4_5, story_9_16, landscape_16_9, wide_21_9, photo_3_2, poster_2_3, standard_4_3, auto] }
|
|
2512
|
+
WorkspaceSuggestionItem:
|
|
2513
|
+
allOf:
|
|
2514
|
+
- $ref: "#/components/schemas/BrandSuggestionItem"
|
|
2515
|
+
- type: object
|
|
2516
|
+
required: [job_id, brand]
|
|
2517
|
+
properties:
|
|
2518
|
+
job_id: { type: string, nullable: true, description: "The job behind a generating item, null otherwise." }
|
|
2519
|
+
brand:
|
|
2520
|
+
type: object
|
|
2521
|
+
required: [id, name]
|
|
2522
|
+
properties:
|
|
2523
|
+
id: { type: string }
|
|
2524
|
+
name: { type: string }
|
|
2525
|
+
preview: { $ref: "#/components/schemas/BrandPreview" }
|
|
2526
|
+
CreativeDirectionsRequest:
|
|
2527
|
+
type: object
|
|
2528
|
+
required: [prompt]
|
|
2529
|
+
properties:
|
|
2530
|
+
prompt:
|
|
2531
|
+
type: string
|
|
2532
|
+
minLength: 8
|
|
2533
|
+
maxLength: 500
|
|
2534
|
+
description: The founder's own description of the brand - what the ranking is against.
|
|
2535
|
+
styles:
|
|
2536
|
+
type: array
|
|
2537
|
+
description: The style chips already picked, if any - catalog ids, not labels.
|
|
2538
|
+
items: { $ref: "#/components/schemas/CreativeDirection/properties/styles/items" }
|
|
2539
|
+
CreativeDirectionsShortlist:
|
|
2540
|
+
type: object
|
|
2541
|
+
required: [palettes, font_pairs, logo_styles]
|
|
2542
|
+
description: >-
|
|
2543
|
+
Catalog ids, best fit first, padded from the catalog's own order so a
|
|
2544
|
+
section is never half empty. Enums are generated from the TypeScript
|
|
2545
|
+
catalogs by scripts/sync-creative-direction-enums.mjs - do not edit by
|
|
2546
|
+
hand.
|
|
2547
|
+
properties:
|
|
2548
|
+
palettes:
|
|
2549
|
+
type: array
|
|
2550
|
+
items: { type: string, enum: [indigo, teal, amber, forest, rose, slate, midnight, coral, sand, plum, ocean, citrus, mono_warm, berry] }
|
|
2551
|
+
font_pairs:
|
|
2552
|
+
type: array
|
|
2553
|
+
items: { type: string, enum: [sora_inter, fraunces_inter, space_inter, playfair_source, dmserif_dmsans, outfit_inter, manrope_manrope, bricolage_inter, instrument_inter, archivo_archivo, lora_lato, syne_inter, epilogue_inter, ibmplex_ibmplex, jost_inter, newsreader_inter, chivo_chivo, unbounded_inter, figtree_figtree, spectral_inter] }
|
|
2554
|
+
logo_styles:
|
|
2555
|
+
type: array
|
|
2556
|
+
items: { type: string, enum: [geometric, rounded, circular, sharp, monogram, lettermark, abstract, emblem, organic, line, gradient, wordmark_only] }
|
|
2557
|
+
rationale:
|
|
2558
|
+
type: string
|
|
2559
|
+
maxLength: 200
|
|
2560
|
+
description: One line on why the top choices fit - shown as a caption, never parsed.
|
|
2561
|
+
NameIdeasRequest:
|
|
2562
|
+
type: object
|
|
2563
|
+
required: [style]
|
|
2564
|
+
properties:
|
|
2565
|
+
style:
|
|
2566
|
+
type: string
|
|
2567
|
+
enum: [compound, abstract, evocative, playful]
|
|
2568
|
+
keyword: { type: string, maxLength: 80 }
|
|
2569
|
+
description: { type: string, maxLength: 500 }
|
|
2570
|
+
NameIdea:
|
|
2571
|
+
type: object
|
|
2572
|
+
required: [name, hint]
|
|
2573
|
+
properties:
|
|
2574
|
+
name: { type: string }
|
|
2575
|
+
hint:
|
|
2576
|
+
type: string
|
|
2577
|
+
description: One line on why the name works - the only thing that distinguishes two invented words to a user.
|
|
2578
|
+
NameIdeasResponse:
|
|
2579
|
+
type: object
|
|
2580
|
+
required: [items]
|
|
2581
|
+
properties:
|
|
2582
|
+
items:
|
|
2583
|
+
type: array
|
|
2584
|
+
items: { $ref: "#/components/schemas/NameIdea" }
|
|
2585
|
+
SurpriseBriefResponse:
|
|
2586
|
+
type: object
|
|
2587
|
+
properties:
|
|
2588
|
+
name: { type: string }
|
|
2589
|
+
tagline: { type: string }
|
|
2590
|
+
description: { type: string }
|
|
2591
|
+
styles:
|
|
2592
|
+
type: array
|
|
2593
|
+
description: >-
|
|
2594
|
+
Catalog ids, not display labels - the creator seeds its style chips
|
|
2595
|
+
with these and matches them against the same ids.
|
|
2596
|
+
items: { type: string, enum: [modern, minimal, bold, playful, elegant, corporate, luxury, organic, geometric, handcrafted, brutalist, editorial, retro, vintage, futuristic, timeless, art_deco, y2k, calm, energetic, warm, technical, rebellious, premium] }
|
|
2597
|
+
JobStep:
|
|
2598
|
+
type: object
|
|
2599
|
+
properties:
|
|
2600
|
+
step:
|
|
2601
|
+
type: string
|
|
2602
|
+
enum: [intake, strategy, visual_system, logo, brand_guardian, applications, render_package, persist_deliver, color_palette, font_pairing, logo_alternative, logo_mono, logo_reversed, logo_wordmark, favicon, brand_guide, business_card, letterhead, mockup, app_icon, og_image, email_signature, hero_image, play_feature_graphic, profile_avatar, social_x, social_linkedin, social_linkedin_personal, social_facebook, social_youtube, social_post, social_story, custom_image, packaging, dna_refine]
|
|
2603
|
+
status: { type: string, enum: [pending, running, completed, failed, skipped] }
|
|
2604
|
+
started_at: { type: string, format: date-time, nullable: true }
|
|
2605
|
+
completed_at: { type: string, format: date-time, nullable: true }
|
|
2606
|
+
GenerationJob:
|
|
2607
|
+
type: object
|
|
2608
|
+
properties:
|
|
2609
|
+
id: { type: string }
|
|
2610
|
+
brand_id: { type: string }
|
|
2611
|
+
kind: { type: string, enum: [brand_generation, asset_regeneration, brand_expand, brand_export, brand_bundle, brand_dna_refine] }
|
|
2612
|
+
workflow_id: { type: string }
|
|
2613
|
+
team_tier: { type: string, enum: [flash, studio, elite] }
|
|
2614
|
+
credit_multiplier: { type: number }
|
|
2615
|
+
status: { type: string, enum: [queued, running, completed, completed_with_errors, failed, canceled] }
|
|
2616
|
+
steps:
|
|
2617
|
+
type: array
|
|
2618
|
+
items: { $ref: "#/components/schemas/JobStep" }
|
|
2619
|
+
credits_reserved: { type: integer }
|
|
2620
|
+
credits_used: { type: integer, nullable: true }
|
|
2621
|
+
eta_seconds: { type: integer, nullable: true }
|
|
2622
|
+
download_url: { type: string, nullable: true }
|
|
2623
|
+
created_at: { type: string, format: date-time }
|
|
2624
|
+
JobEnvelope:
|
|
2625
|
+
type: object
|
|
2626
|
+
properties:
|
|
2627
|
+
job: { $ref: "#/components/schemas/GenerationJob" }
|
|
2628
|
+
LogoSelectionRequest:
|
|
2629
|
+
type: object
|
|
2630
|
+
properties:
|
|
2631
|
+
asset_version_id: { type: string }
|
|
2632
|
+
lockup_override:
|
|
2633
|
+
type: string
|
|
2634
|
+
enum: [horizontal, stacked]
|
|
2635
|
+
description: >-
|
|
2636
|
+
Changes ONLY this approval's logo_alternative render - never the
|
|
2637
|
+
brand's own pinned Logo direction.
|
|
2638
|
+
family_override:
|
|
2639
|
+
type: string
|
|
2640
|
+
description: >-
|
|
2641
|
+
Changes ONLY this approval's logo_alternative wordmark font -
|
|
2642
|
+
never the brand's own typography.heading_font DNA value.
|
|
2643
|
+
ink_override:
|
|
2644
|
+
type: string
|
|
2645
|
+
description: >-
|
|
2646
|
+
Changes ONLY this approval's logo_alternative wordmark ink
|
|
2647
|
+
(main text) colour - never the brand's own DNA palette
|
|
2648
|
+
(color_palette asset). Must be #rrggbb and one of the brand's
|
|
2649
|
+
own generated palette tokens.
|
|
2650
|
+
accent_override:
|
|
2651
|
+
type: string
|
|
2652
|
+
description: >-
|
|
2653
|
+
Changes ONLY this approval's logo_alternative wordmark accent
|
|
2654
|
+
(highlighted run) colour - never the brand's own DNA palette
|
|
2655
|
+
(color_palette asset). Must be #rrggbb and one of the brand's
|
|
2656
|
+
own generated palette tokens.
|
|
2657
|
+
style_seed_override:
|
|
2658
|
+
type: string
|
|
2659
|
+
description: >-
|
|
2660
|
+
Carries forward the "shuffle wordmark style" control's last
|
|
2661
|
+
value so approving does not silently re-roll the treatment the
|
|
2662
|
+
user picked.
|
|
2663
|
+
LogoConceptPalette:
|
|
2664
|
+
type: object
|
|
2665
|
+
properties:
|
|
2666
|
+
tokens:
|
|
2667
|
+
type: array
|
|
2668
|
+
items:
|
|
2669
|
+
type: object
|
|
2670
|
+
properties:
|
|
2671
|
+
hex: { type: string }
|
|
2672
|
+
role: { type: string }
|
|
2673
|
+
# The brand's generated name, from the same concept recipe the tokens
|
|
2674
|
+
# come from. Null on a batch persisted before the strategy was stored
|
|
2675
|
+
# with it. The brand row itself carries only a working name derived
|
|
2676
|
+
# from the prompt until persist_brand_version runs, which is the last
|
|
2677
|
+
# activity before the job completes - so this is the only place the
|
|
2678
|
+
# real name is readable while the concept picker is on screen.
|
|
2679
|
+
brand_name: { type: string, nullable: true }
|
|
2680
|
+
Asset:
|
|
2681
|
+
type: object
|
|
2682
|
+
properties:
|
|
2683
|
+
id: { type: string }
|
|
2684
|
+
brand_id: { type: string }
|
|
2685
|
+
version_id: { type: string, nullable: true }
|
|
2686
|
+
kind: { type: string }
|
|
2687
|
+
name: { type: string }
|
|
2688
|
+
status: { type: string, enum: [queued, generating, ready, failed] }
|
|
2689
|
+
url: { type: string, nullable: true }
|
|
2690
|
+
thumbnail_url: { type: string, nullable: true }
|
|
2691
|
+
credits: { type: integer }
|
|
2692
|
+
pixel_check: { type: integer, nullable: true }
|
|
2693
|
+
qa_report:
|
|
2694
|
+
allOf: [{ $ref: "#/components/schemas/QaReport" }]
|
|
2695
|
+
nullable: true
|
|
2696
|
+
formats:
|
|
2697
|
+
description: >-
|
|
2698
|
+
Downloadable file formats of the effective version, normalized for
|
|
2699
|
+
display (png-1024 and png-256 collapse to PNG). The effective
|
|
2700
|
+
version is version_id's row, or - where an asset has versions but
|
|
2701
|
+
none is current yet (logo concepts awaiting selection) - the latest
|
|
2702
|
+
one, so this can be non-empty while version_id is still null. Empty
|
|
2703
|
+
when the asset has no version at all.
|
|
2704
|
+
type: array
|
|
2705
|
+
items: { type: string }
|
|
2706
|
+
created_at: { type: string, format: date-time }
|
|
2707
|
+
AssetVersion:
|
|
2708
|
+
type: object
|
|
2709
|
+
properties:
|
|
2710
|
+
id: { type: string }
|
|
2711
|
+
asset_id: { type: string }
|
|
2712
|
+
parent_version_id: { type: string, nullable: true }
|
|
2713
|
+
files:
|
|
2714
|
+
type: array
|
|
2715
|
+
items:
|
|
2716
|
+
type: object
|
|
2717
|
+
properties:
|
|
2718
|
+
format: { type: string }
|
|
2719
|
+
content_hash: { type: string }
|
|
2720
|
+
storage_key: { type: string }
|
|
2721
|
+
bytes: { type: integer }
|
|
2722
|
+
credit_cost: { type: integer }
|
|
2723
|
+
qa_report:
|
|
2724
|
+
allOf: [{ $ref: "#/components/schemas/QaReport" }]
|
|
2725
|
+
nullable: true
|
|
2726
|
+
created_at: { type: string, format: date-time }
|
|
2727
|
+
RefineRequest:
|
|
2728
|
+
description: >-
|
|
2729
|
+
A free-text instruction, accepted by `logo` and `logo_wordmark`, or
|
|
2730
|
+
structured treatment overrides, accepted by `logo_wordmark` only.
|
|
2731
|
+
oneOf:
|
|
2732
|
+
- type: object
|
|
2733
|
+
required: [instruction]
|
|
2734
|
+
properties:
|
|
2735
|
+
instruction: { type: string, minLength: 1, maxLength: 500 }
|
|
2736
|
+
- $ref: "#/components/schemas/LogoTreatmentOverrides"
|
|
2737
|
+
VariationRequest:
|
|
2738
|
+
type: object
|
|
2739
|
+
required: [preset]
|
|
2740
|
+
properties:
|
|
2741
|
+
preset:
|
|
2742
|
+
type: string
|
|
2743
|
+
enum:
|
|
2744
|
+
- more_minimal
|
|
2745
|
+
- more_startup
|
|
2746
|
+
- bolder
|
|
2747
|
+
- playful
|
|
2748
|
+
- elegant
|
|
2749
|
+
- geometric
|
|
2750
|
+
- hand_drawn
|
|
2751
|
+
- monochrome
|
|
2752
|
+
RestoreRequest:
|
|
2753
|
+
type: object
|
|
2754
|
+
required: [to_version]
|
|
2755
|
+
properties:
|
|
2756
|
+
to_version: { type: string }
|
|
2757
|
+
LogoTreatmentOverrides:
|
|
2758
|
+
type: object
|
|
2759
|
+
description: >-
|
|
2760
|
+
Structured overrides for wordmark treatment - font, ink (name) colour,
|
|
2761
|
+
accent colour and style variant. At least one field is required.
|
|
2762
|
+
properties:
|
|
2763
|
+
family: { type: string, minLength: 1, maxLength: 200 }
|
|
2764
|
+
ink: { type: string, pattern: "^#[0-9a-fA-F]{6}$" }
|
|
2765
|
+
accent: { type: string, pattern: "^#[0-9a-fA-F]{6}$" }
|
|
2766
|
+
styleSeed: { type: string, minLength: 1, maxLength: 64 }
|
|
2767
|
+
styleIndex:
|
|
2768
|
+
type: integer
|
|
2769
|
+
minimum: 0
|
|
2770
|
+
maximum: 10
|
|
2771
|
+
description: >-
|
|
2772
|
+
Names one of the category's 5 wordmark treatments outright.
|
|
2773
|
+
Preferred over styleSeed, which re-rolls: a nonce lands on the
|
|
2774
|
+
treatment already in force one time in five and names nothing
|
|
2775
|
+
a caller can return to. Persisted on the asset recipe, so
|
|
2776
|
+
every later render of the brand reproduces it.
|
|
2777
|
+
LogoTreatment:
|
|
2778
|
+
type: object
|
|
2779
|
+
description: >-
|
|
2780
|
+
The wordmark/logo's current treatment (font, ink, accent, style) -
|
|
2781
|
+
read from the brand's logo asset's own current-version recipe, the
|
|
2782
|
+
durable source of truth. Provides pre-fill values for the wordmark
|
|
2783
|
+
treatment panel.
|
|
2784
|
+
required: [family, ink, accent, style_seed, style_index]
|
|
2785
|
+
properties:
|
|
2786
|
+
family: { type: string }
|
|
2787
|
+
ink: { type: string, pattern: "^#[0-9a-fA-F]{6}$" }
|
|
2788
|
+
accent: { type: string, pattern: "^#[0-9a-fA-F]{6}$" }
|
|
2789
|
+
style_seed: { type: string }
|
|
2790
|
+
style_index:
|
|
2791
|
+
type: integer
|
|
2792
|
+
minimum: 0
|
|
2793
|
+
maximum: 10
|
|
2794
|
+
nullable: true
|
|
2795
|
+
description: >-
|
|
2796
|
+
Which of the category's 11 wordmark treatments is currently
|
|
2797
|
+
drawn, or null for a brand that has never named one (the
|
|
2798
|
+
renderer then picks by hash, as it always did).
|
|
2799
|
+
MeResponse:
|
|
2800
|
+
type: object
|
|
2801
|
+
properties:
|
|
2802
|
+
user:
|
|
2803
|
+
type: object
|
|
2804
|
+
properties:
|
|
2805
|
+
id: { type: string }
|
|
2806
|
+
clerkUserId: { type: string, nullable: true }
|
|
2807
|
+
email: { type: string, nullable: true }
|
|
2808
|
+
termsAcceptedAt:
|
|
2809
|
+
type: string
|
|
2810
|
+
format: date-time
|
|
2811
|
+
nullable: true
|
|
2812
|
+
description: >-
|
|
2813
|
+
When this membership first accepted the Terms of Service, or null
|
|
2814
|
+
if no acceptance is on record. Scoped to the workspace membership
|
|
2815
|
+
rather than the person. Always null for an API key, which has no
|
|
2816
|
+
membership to read. Reports the earliest acceptance, so it does
|
|
2817
|
+
not track later revisions of the Terms.
|
|
2818
|
+
workspace:
|
|
2819
|
+
type: object
|
|
2820
|
+
properties:
|
|
2821
|
+
id: { type: string }
|
|
2822
|
+
clerkOrgId: { type: string }
|
|
2823
|
+
name: { type: string }
|
|
2824
|
+
plan: { type: string }
|
|
2825
|
+
role: { type: string, enum: [owner, admin, member] }
|
|
2826
|
+
AiTeam:
|
|
2827
|
+
type: object
|
|
2828
|
+
properties:
|
|
2829
|
+
tier: { type: string, enum: [flash, studio, elite] }
|
|
2830
|
+
credit_multiplier: { type: number }
|
|
2831
|
+
WaitlistSignupRequest:
|
|
2832
|
+
type: object
|
|
2833
|
+
required: [email]
|
|
2834
|
+
properties:
|
|
2835
|
+
email: { type: string, format: email }
|
|
2836
|
+
marketing_consent: { type: boolean, default: false }
|
|
2837
|
+
privacy_policy_version: { type: string }
|
|
2838
|
+
attribution: { $ref: "#/components/schemas/WaitlistAttribution" }
|
|
2839
|
+
WaitlistAttribution:
|
|
2840
|
+
description: >-
|
|
2841
|
+
First-touch acquisition snapshot captured on the landing page:
|
|
2842
|
+
utm_* / ad click ids from the entry URL, the external referrer and
|
|
2843
|
+
the entry path. All fields optional; unknown fields are stripped and
|
|
2844
|
+
never persisted.
|
|
2845
|
+
type: object
|
|
2846
|
+
properties:
|
|
2847
|
+
utm_source: { type: string, minLength: 1, maxLength: 512 }
|
|
2848
|
+
utm_medium: { type: string, minLength: 1, maxLength: 512 }
|
|
2849
|
+
utm_campaign: { type: string, minLength: 1, maxLength: 512 }
|
|
2850
|
+
utm_term: { type: string, minLength: 1, maxLength: 512 }
|
|
2851
|
+
utm_content: { type: string, minLength: 1, maxLength: 512 }
|
|
2852
|
+
gclid: { type: string, minLength: 1, maxLength: 512 }
|
|
2853
|
+
fbclid: { type: string, minLength: 1, maxLength: 512 }
|
|
2854
|
+
referrer: { type: string, minLength: 1, maxLength: 512 }
|
|
2855
|
+
landing_page: { type: string, minLength: 1, maxLength: 512 }
|
|
2856
|
+
WaitlistSignupResponse:
|
|
2857
|
+
type: object
|
|
2858
|
+
properties:
|
|
2859
|
+
status: { type: string, enum: [joined] }
|
|
2860
|
+
ConsentCookieRequest:
|
|
2861
|
+
type: object
|
|
2862
|
+
required: [type, region, categories, banner_version, gpc, client_id]
|
|
2863
|
+
properties:
|
|
2864
|
+
type: { type: string, enum: [cookie_consent] }
|
|
2865
|
+
region: { type: string, minLength: 1, maxLength: 16 }
|
|
2866
|
+
categories:
|
|
2867
|
+
type: array
|
|
2868
|
+
maxItems: 16
|
|
2869
|
+
items: { type: string, minLength: 1, maxLength: 64 }
|
|
2870
|
+
banner_version: { type: integer, minimum: 0 }
|
|
2871
|
+
gpc: { type: boolean }
|
|
2872
|
+
client_id: { type: string, minLength: 1, maxLength: 128 }
|
|
2873
|
+
ConsentPrivacyChoiceRequest:
|
|
2874
|
+
type: object
|
|
2875
|
+
required: [type, region, choices, gpc, client_id]
|
|
2876
|
+
properties:
|
|
2877
|
+
type: { type: string, enum: [privacy_choice] }
|
|
2878
|
+
region: { type: string, minLength: 1, maxLength: 16 }
|
|
2879
|
+
choices:
|
|
2880
|
+
type: object
|
|
2881
|
+
additionalProperties: { type: boolean }
|
|
2882
|
+
gpc: { type: boolean }
|
|
2883
|
+
client_id: { type: string, minLength: 1, maxLength: 128 }
|
|
2884
|
+
ConsentTermsAcceptanceRequest:
|
|
2885
|
+
type: object
|
|
2886
|
+
required: [type, checkbox_text, accepted_versions, client_id]
|
|
2887
|
+
properties:
|
|
2888
|
+
type: { type: string, enum: [terms_acceptance] }
|
|
2889
|
+
checkbox_text: { type: string, minLength: 1, maxLength: 512 }
|
|
2890
|
+
accepted_versions:
|
|
2891
|
+
type: object
|
|
2892
|
+
additionalProperties: { type: string, maxLength: 32 }
|
|
2893
|
+
client_id: { type: string, minLength: 1, maxLength: 128 }
|
|
2894
|
+
ConsentPurchaseDisclosureRequest:
|
|
2895
|
+
type: object
|
|
2896
|
+
description: >-
|
|
2897
|
+
What the checkout confirmation step displayed before the buyer approved
|
|
2898
|
+
the purchase (#424). A record of a DISCLOSURE, not of a permission -
|
|
2899
|
+
pressing the pay button is the agreement, and no separate tick for the
|
|
2900
|
+
payment or the renewal is shown. Recorded before the redirect to the
|
|
2901
|
+
payment provider, so `recorded_at` is when the buyer approved, not when
|
|
2902
|
+
the money moved.
|
|
2903
|
+
required:
|
|
2904
|
+
[type, idempotency_key, price_id, kind, amount, currency, billing_period,
|
|
2905
|
+
disclosure_version, client_id]
|
|
2906
|
+
properties:
|
|
2907
|
+
type: { type: string, enum: [purchase_disclosure] }
|
|
2908
|
+
idempotency_key:
|
|
2909
|
+
type: string
|
|
2910
|
+
minLength: 1
|
|
2911
|
+
maxLength: 128
|
|
2912
|
+
description: >-
|
|
2913
|
+
The Idempotency-Key the checkout request carries - what ties this
|
|
2914
|
+
record to one purchase attempt. Not a checkout session id: the
|
|
2915
|
+
session is created by the request this disclosure precedes.
|
|
2916
|
+
price_id: { type: string, minLength: 1, maxLength: 255 }
|
|
2917
|
+
kind: { type: string, enum: [plan, topup] }
|
|
2918
|
+
plan: { type: string, enum: [pro, studio, agency] }
|
|
2919
|
+
credits: { type: integer, enum: [100, 300, 1000] }
|
|
2920
|
+
amount: { type: string, minLength: 1, maxLength: 32 }
|
|
2921
|
+
currency: { type: string, minLength: 1, maxLength: 8 }
|
|
2922
|
+
billing_period: { type: string, enum: [month, one_time] }
|
|
2923
|
+
renewal_text:
|
|
2924
|
+
type: string
|
|
2925
|
+
minLength: 1
|
|
2926
|
+
maxLength: 512
|
|
2927
|
+
description: >-
|
|
2928
|
+
The automatic-renewal sentence as shown. Stored rather than
|
|
2929
|
+
derived, so a later wording change cannot rewrite what a past buyer
|
|
2930
|
+
was told. Absent for a one-time pack, which renews nothing.
|
|
2931
|
+
disclosure_version: { type: integer, minimum: 0 }
|
|
2932
|
+
client_id: { type: string, minLength: 1, maxLength: 128 }
|
|
2933
|
+
ConsentRequest:
|
|
2934
|
+
oneOf:
|
|
2935
|
+
- { $ref: "#/components/schemas/ConsentCookieRequest" }
|
|
2936
|
+
- { $ref: "#/components/schemas/ConsentPrivacyChoiceRequest" }
|
|
2937
|
+
- { $ref: "#/components/schemas/ConsentTermsAcceptanceRequest" }
|
|
2938
|
+
- { $ref: "#/components/schemas/ConsentPurchaseDisclosureRequest" }
|
|
2939
|
+
discriminator:
|
|
2940
|
+
propertyName: type
|
|
2941
|
+
mapping:
|
|
2942
|
+
cookie_consent: "#/components/schemas/ConsentCookieRequest"
|
|
2943
|
+
privacy_choice: "#/components/schemas/ConsentPrivacyChoiceRequest"
|
|
2944
|
+
terms_acceptance: "#/components/schemas/ConsentTermsAcceptanceRequest"
|
|
2945
|
+
purchase_disclosure: "#/components/schemas/ConsentPurchaseDisclosureRequest"
|
|
2946
|
+
ConsentResponse:
|
|
2947
|
+
type: object
|
|
2948
|
+
required: [id, recorded_at]
|
|
2949
|
+
properties:
|
|
2950
|
+
id: { type: string }
|
|
2951
|
+
recorded_at: { type: string, format: date-time }
|
|
2952
|
+
ApiKey:
|
|
2953
|
+
type: object
|
|
2954
|
+
properties:
|
|
2955
|
+
id: { type: string }
|
|
2956
|
+
name: { type: string }
|
|
2957
|
+
environment: { type: string, enum: [live, test] }
|
|
2958
|
+
prefix: { type: string }
|
|
2959
|
+
last4: { type: string }
|
|
2960
|
+
requests: { type: integer }
|
|
2961
|
+
credits: { type: integer }
|
|
2962
|
+
created_at: { type: string, format: date-time }
|
|
2963
|
+
last_used_at: { type: string, format: date-time, nullable: true }
|
|
2964
|
+
created_by_email: { type: string, nullable: true, description: "Null for keys created before this field existed." }
|
|
2965
|
+
ApiKeyWithSecret:
|
|
2966
|
+
allOf:
|
|
2967
|
+
- { $ref: "#/components/schemas/ApiKey" }
|
|
2968
|
+
- type: object
|
|
2969
|
+
properties:
|
|
2970
|
+
secret: { type: string }
|
|
2971
|
+
CreateApiKeyRequest:
|
|
2972
|
+
type: object
|
|
2973
|
+
required: [name]
|
|
2974
|
+
properties:
|
|
2975
|
+
name: { type: string, minLength: 1, maxLength: 80 }
|
|
2976
|
+
environment:
|
|
2977
|
+
type: string
|
|
2978
|
+
enum: [live, test]
|
|
2979
|
+
default: live
|
|
2980
|
+
description: >
|
|
2981
|
+
A label only - a test key reaches the real workspace and spends
|
|
2982
|
+
real credits exactly like a live one.
|
|
2983
|
+
WebhookEndpoint:
|
|
2984
|
+
type: object
|
|
2985
|
+
properties:
|
|
2986
|
+
id: { type: string }
|
|
2987
|
+
url: { type: string, format: uri }
|
|
2988
|
+
events:
|
|
2989
|
+
type: array
|
|
2990
|
+
items: { $ref: "#/components/schemas/WebhookEventType" }
|
|
2991
|
+
disabled: { type: boolean }
|
|
2992
|
+
created_at: { type: string, format: date-time }
|
|
2993
|
+
WebhookEndpointWithSecret:
|
|
2994
|
+
allOf:
|
|
2995
|
+
- type: object
|
|
2996
|
+
properties:
|
|
2997
|
+
endpoint: { $ref: "#/components/schemas/WebhookEndpoint" }
|
|
2998
|
+
secret: { type: string }
|
|
2999
|
+
CreateWebhookEndpointRequest:
|
|
3000
|
+
type: object
|
|
3001
|
+
required: [url, events]
|
|
3002
|
+
properties:
|
|
3003
|
+
url: { type: string, format: uri }
|
|
3004
|
+
events:
|
|
3005
|
+
type: array
|
|
3006
|
+
minItems: 1
|
|
3007
|
+
items: { $ref: "#/components/schemas/WebhookEventType" }
|
|
3008
|
+
UpdateWebhookEndpointRequest:
|
|
3009
|
+
type: object
|
|
3010
|
+
properties:
|
|
3011
|
+
events:
|
|
3012
|
+
type: array
|
|
3013
|
+
minItems: 1
|
|
3014
|
+
items: { $ref: "#/components/schemas/WebhookEventType" }
|
|
3015
|
+
disabled: { type: boolean }
|
|
3016
|
+
WebhookEventType:
|
|
3017
|
+
type: string
|
|
3018
|
+
enum:
|
|
3019
|
+
- brand.completed
|
|
3020
|
+
- brand.updated
|
|
3021
|
+
- asset.ready
|
|
3022
|
+
- asset.regenerated
|
|
3023
|
+
- job.failed
|
|
3024
|
+
- credits.low
|
|
3025
|
+
- credits.settled
|
|
3026
|
+
- subscription.changed
|