# Blueprint Studio API: full reference

Complete documentation for all 43 API v1 endpoints.
Base URL: https://tools.blueprintstudio.ai

## Authentication

Include your API key in the Authorization header:

```
curl https://tools.blueprintstudio.ai/api/v1/assets \
  -H "Authorization: Bearer bp_live_your_api_key_here"
```

API keys start with `bp_live_`. Create keys in account settings (https://tools.blueprintstudio.ai/settings#api-keys) or via the API.

The REST API and API keys are on paid plans: Pro and Max. A Free account can also use them with Pro access from a client grant or credit pack of 600 or more credits, or with a key bound to a brand that includes Pro. Otherwise every request, read or write, answers 403 with code PLAN_REQUIRED, nextStep, plansUrl and checkoutUrl (a one-step Stripe checkout for Pro on the key's account, no sign-in); the key isn't revoked and works again once the account upgrades. AI apps connect with MCP sign-in on every plan, Free included.

Each API key is bound to one workspace: a single brand, or your personal library. A key only works in its own workspace, so a personal key can't act on brands and a brand key can't reach other brands or your personal library. Choose the workspace when you create a key in Settings, and create one key per brand you automate.

## Rate Limits

| Tier | API Requests | Generation credits |
|------|-------------|--------------------|
| Free | Paid plans only | 500 credits once, for 30 days (about 166 default images) |
| Pro | 5,000 / day | 900 credits / month (about 300 standard images) |
| Max | 20,000 / day | 10,000 credits / month (about 3,300 standard images) |

Credits are weighted by model, size and quality, so one image is not one credit: a standard image (GPT Image 2.5 Sunburst, 2K, medium quality) costs 3 credits, and each image sent with it (a reference, logo or the image being edited) adds 1. See every model's cost at https://tools.blueprintstudio.ai/help#credits-per-image.

Every /api/v1 response to an API key carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds, next UTC midnight). Polling a generation job (GET /api/v1/generate/jobs/:id) doesn't count toward the daily limit. Over the limit you get 429 with code API_REQUEST_LIMIT and a Retry-After header.

## Error Codes

| Status | Meaning |
|--------|---------|
| 400 | Bad request: check parameters. A refused reference image (code INVALID_REFERENCE_IMAGE, REFERENCE_URL_BLOCKED, REFERENCE_URL_UNREACHABLE, REFERENCE_IMAGE_TOO_LARGE or REFERENCE_UNAVAILABLE, with the matching status) says which one in reference, why in reason, and what works in nextStep; nothing is charged |
| 401 | Invalid or missing API key |
| 402 | Not enough credits for this image (code GENERATION_LIMIT), whether the balance is empty or only short of this image. Don't retry: show nextStep (cost against balance, and the closest setting that fits, also in cheaperOption, null when none) and plansUrl to the user, then wait for them or for the monthly reset (resetsAt; null when nothing refills, as on Free, so only a plan change or new credits help). On Free, checkoutUrl is a one-step Stripe checkout for Pro on this account (no sign-in, good for 7 days): show it, and from a terminal offer to open it in the user's browser only if they agree |
| 403 | Forbidden: the API is on paid plans and this key's account has no Pro access (code PLAN_REQUIRED, with nextStep, plansUrl and checkoutUrl, a one-step Stripe checkout for Pro on this account), or, rarely, the model or size isn't included where you're generating (code MODEL_NOT_ENTITLED, with plansUrl; every plan includes every current model and size) |
| 404 | Resource not found |
| 410 | An upload or upload link has expired (code UPLOAD_EXPIRED or UPLOAD_LINK_EXPIRED). Upload links take files for 30 minutes and uploads work for 24 hours: create a new upload link |
| 409 | Conflict, such as a requestId reused with different inputs (code IDEMPOTENCY_CONFLICT). A changed request, including a retry after a failed job, needs a new requestId |
| 413 | The uploaded file is over the 4 MB limit. Resize or compress it and retry. |
| 415 | The file isn't one of the accepted image types (PNG, JPEG, WebP, GIF, AVIF or SVG), whatever name or declared content type it was sent with. |
| 422 | The image model's safety filter refused the prompt or a reference image (code moderation_blocked, or a Google code such as image_safety). Not a server error and no credits were charged: reword it as nextStep says and retry once with a new requestId |
| 429 | A wait, not a failure: the daily request limit (code API_REQUEST_LIMIT, with resetAt), a short limit such as RATE_LIMITED, or the reference link limit (code REFERENCE_LINK_LIMIT: 120 new image links per 10 minutes, 4 fetched at a time; or reuse links already sent, saved image links or data URLs). UPLOAD_QUOTA means 200 uploads from the last 24 hours: use those, or wait for older ones to expire. Retry-After and retryAfterSeconds give the same seconds, error says how long in words, and nextStep says to wait and then retry once. No credits are charged |
| 500 | Server error |
| 503 | Usually a brief wait: a busy image or analysis service (code SERVICE_BUSY) or a short outage on our side (TEMPORARILY_UNAVAILABLE). Retry-After and retryAfterSeconds say how long; wait, then retry once. If the body has jobId (such as ADMISSION_OUTCOME_UNKNOWN, where credits may be held), poll that job instead; don't resubmit |

Error response format: `{ "error": "description", "code": "GENERATION_LIMIT" }`. Credit, plan and request-limit errors include code and plansUrl. GENERATION_LIMIT adds nextStep (one line for the user: what this image costs against the balance) and cheaperOption (the closest setting the balance still covers, or null). On a Free account, GENERATION_LIMIT and PLAN_REQUIRED add checkoutUrl: a one-step Stripe checkout for Pro on that account (no sign-in, good for 7 days). Show it to the user; from a terminal, offer to open it in their browser and open it only if they agree. Do not log or share it. A wait (429, or 503 SERVICE_BUSY) adds retryAfterSeconds, the same number as the Retry-After header, and nextStep: tell the user it is a wait, retry once after that many seconds, and do not switch tools, regenerate or create another account. A 503 whose body has jobId is not a wait: poll that job and do not resubmit. Web-app links in API responses carry ?ref=api.

## Generation

Generate brand assets, get prompt suggestions, and brainstorm ideas using AI.

### POST /api/v1/generate

**Generate a brand asset**

Generate an image through the shared app pipeline. Slower settings (GPT Image 2.5 Sunburst at every setting, GPT Image 2.6 Flare at every setting, GPT Image 2.6 Sunburst at every setting, GPT Image 2.5 Flare at every setting, GPT Image 2 at medium or high quality, Gemini Nano Banana 2.1 at 4K), async=true, Prefer: respond-async, or requestId use background jobs and return HTTP 202. Poll the returned pollUrl in the same account/workspace until completed or failed (add ?waitSeconds=20 so each poll waits for the result). With no styleId a new image gets no Style. Parent edits use the selected ancestor chain, never unrelated later turns.

  Request Body:
    - async: boolean — Request durable background execution (recommended for integrations).
    - requestId: string — This generation's key, a unique id you make up: 8 to 128 letters, digits, - and _ (a UUID works). Reuse only for identical retries; automatically enables background execution.
    - prompt: string (required) — What to generate (e.g., "a coffee cup icon")
    - styleId: string — Style ID to apply, or "freestyle". A new image with no styleId gets no Style, the same as "freestyle"; an edit (parentAssetId) with no styleId keeps its parent's Style. The brand's default Style isn't applied for you: GET /api/v1/styles marks it isBrandDefault, so pass its id when it fits the request.
    - aspectRatio: string (default: 1:1, enum: 1:1 | 16:9 | 9:16 | 4:3 | 3:4 | 3:2 | 2:3 | 4:5 | 5:4 | 21:9) — Output aspect ratio.
    - highQuality: boolean (default: false) — Legacy Gemini quality switch. Prefer modelId and quality for new integrations.
    - modelId: string (enum: gpt-image-2.5-sunburst | gpt-image-2.6-flare | gpt-image-2.6-sunburst | gpt-image-2.5-flare | gpt-image-2 | gemini-3-pro-image | gemini-3.1-flash-image) — Image model. Left out: the user's image preset (GET /api/v1/settings imagePreset; "balanced", GPT Image 2.6 Sunburst at medium quality, unless they chose another), with its quality unless quality is given.
    - parentAssetId: string — Selected image to edit. Style and only that image's ancestry are inherited. Native provider continuation is used when supported and verified; otherwise the selected parent image is sent explicitly.
    - markup: object — With parentAssetId: the image to edit with marks on it (circles, arrows, scribbles, written notes), as one input in the forms inputs takes, e.g. {kind:"upload",uploadId}. A transparent PNG of only the marks, the edited image's size, works too. The prompt says what the marks ask for; the result has no marks. It counts as one image sent. Cannot combine with referenceImages/referenceImage.
    - inputs: array — Ordered references: {kind:"brand_asset",assetId,versionId}, {kind:"upload",uploadId} (a file from POST /api/v1/uploads or an upload link), {kind:"receipt",sourceAssetId,inputPosition}, or {kind:"url",url} (a public https link to a PNG, JPEG or WebP image, GIF converted, or a saved image's imageUrl, webUrl or share link). Use generationInput from GET /api/v1/brands/:id/assets (MCP list_brand_assets) for official logos. Cannot combine with referenceImages/referenceImage. Always returns a durable job.
    - referenceImages: array — Array of public https links to PNG, JPEG or WebP images (GIF is converted), saved image links (an asset's imageUrl, webUrl or share link), or PNG/JPEG/WebP base64 data URLs (up to 20 MiB each; declared type must match the image). For a file that isn't at a public link, upload it first (POST /api/v1/uploads, or POST /api/v1/upload-links for files over 4 MB) and pass it in inputs as {kind: "upload", uploadId}. Count limit depends on model.
    - referenceImage: string — Single image link (any form referenceImages takes) or PNG/JPEG/WebP base64 data URL. Legacy; prefer referenceImages[].
    - temperature: number (default: 0.4) — Generation temperature. Lower = more consistent.
    - imageSize: string (enum: 1K | 2K | 4K) — Output image resolution. Omitted: 2K, on every plan, or 4K for a user on the best preset when modelId is left out too; an edit (parentAssetId) keeps its parent's size. Use 4K for hero images, print and large banners, and 1K or quality low for quick drafts. Larger sizes cost more credits (a medium-quality image with the default model: 5 at 1K, 9 at 2K, 15 at 4K).
    - quality: string (enum: low | medium | high) — Model-specific output quality.
    - outputFormat: string (enum: png | jpeg | webp | gif | svg) — Model-specific output format. Also "gif", an animated GIF with frames 512 px on the long side (it can animate a reference or the image being edited), and "svg", a vector SVG drawn from the prompt alone (no references or edits). Only GPT Image 2.6 (gpt-image-2.6-flare, gpt-image-2.6-sunburst) makes these: with another modelId, or none, Flare 2.6 makes them. An SVG image's downloadUrl is the SVG; its imageUrl is a PNG drawn from it. Each has its own price (the models catalog).
    - outputCompression: integer — JPEG/WebP output compression from 0-100.
    - frameCount: integer (default: 9) — With outputFormat gif: how many frames (2 to 16).
    - complexity: string (enum: low | medium | high) — GPT Image 2.6 only (others ignore it): how much the model plans before drawing. "low" is quicker for a small, focused edit; omitted, it is high, for dense text or a busy layout. It costs nothing.
    - frameDurationMs: integer (default: 200) — With outputFormat gif: how long each frame shows, in ms (20 to 2000).
    - background: string (default: opaque, enum: transparent | opaque) — "transparent" makes a PNG with a see-through background (sprites, icons, logos, product cutouts). GPT Image 2.5 Sunburst and GPT Image 2.6 Flare and GPT Image 2.6 Sunburst and GPT Image 2.5 Flare make it natively at no extra cost; other models remove the background after generation for 2 credits more, refunded if removal fails. Always a queued job. The job result's background field says how it was done; applied false means the image kept its background, its note says why, and no removal credit is kept. Not with outputFormat jpeg on a native model.

  Note: Send one requestId per intended generation (a unique id you make up: 8 to 128 letters, digits, - and _ (a UUID works)); use the same ID and identical inputs for transport retries. Idempotency-Key header is equivalent. Changed inputs return 409. Job lookup is retained at least 7 days; an expired key linked to a receipt is rejected rather than charged again. Legacy fast requests without async/requestId still create a job: they return HTTP 200 with the finished asset and its jobId, or HTTP 202 with the job handle when the generation cannot finish within the request. Never blindly resubmit after a timeout; poll the job instead. Reference images can be public https links (fetched once: public hosts only, port 443, up to 3 redirects and 20 MiB), a saved image's imageUrl, webUrl or share link, or base64 data URLs. They are validated (PNG/JPEG/WebP, GIF converted to PNG, up to 20 MiB each, 80 MiB total) and stored privately before any credits are charged. A refused reference returns its field, number (reference), reason and a nextStep, and nothing is charged. A link the same account sent within the last hour is reused rather than fetched again, and doesn't count; new links are read up to 120 per account every 10 minutes, 4 at a time (beyond that, 429 REFERENCE_LINK_LIMIT says how long to wait in retryAfterSeconds; saved image links and data URLs don't count). Links are only read when the account can afford the image. The whole HTTP request must still fit the hosting request-size limit, so prefer links for large images.

  Response (202): Queued job acknowledgement. Poll pollUrl; the final result is returned by the status endpoint.
  ```json
  {
    "status": "queued",
    "jobId": "uuid",
    "pollUrl": "/api/v1/generate/jobs/uuid",
    "retryAfterSeconds": 5
  }
  ```

### GET /api/v1/generate/jobs/:id

**Check a generation job**

Read a job owned by the authenticated account in this workspace. Pass waitSeconds (up to 25) to wait for a pending or processing job to finish before the response, so one request usually returns the finished image; otherwise poll every 5 seconds. A status-request timeout does not mean generation failed: continue polling the same job. Completed output can be recovered from its immutable receipt without another provider call.

  Path Parameters:
    - id: string (required) — Job UUID returned by generation admission.

  Query Parameters:
    - waitSeconds: number (default: 0) — Wait up to this many seconds (0 to 25) for the job to finish before answering. 20 suits most images. Omit to answer at once.

  Rate Limit: Doesn't count toward the daily request limit, so polling never uses it up.

  Response (200): Job status (pending, processing, completed, failed), public saved result, and safe error message. A failed job also has nextStep: it was refunded, and a retry needs a new requestId. When the image model's safety filter refused the request (result.code moderation_blocked, or a Google code such as image_safety), nextStep says how to reword it. With background "transparent", result.background says how it was made (native or removal); when removal didn't work, the generated image is still the result and background.note says why. In a brand, a completed job also has officialAssetsUsed: the official logos it was made with ({assetId, name, variant}, empty when none), to tell the user which logo was used.
  ```json
  {
    "jobId": "uuid",
    "status": "completed",
    "result": {
      "assetId": "uuid",
      "styleId": "uuid",
      "imageUrl": "https://...",
      "outputWidth": 1024,
      "outputHeight": 1024,
      "creditsCharged": 3,
      "webUrl": "https://tools.blueprintstudio.ai/asset-generator?brand=personal&asset=uuid&ref=api",
      "downloadUrl": "https://...",
      "suggestedFilename": "a-minimalist-coffee-cup-icon-1a2b3c4d.png"
    },
    "error": null
  }
  ```

### POST /api/v1/generate/suggest

**Get prompt suggestions**

Generate AI-powered prompt suggestions for a given style. Useful for inspiring users or populating prompt libraries.

  Request Body:
    - styleId: string — Style to base suggestions on. Omit for generic suggestions.
    - count: number (default: 5) — Number of suggestions to return (1–10).

  Response (200): Array of prompt suggestion strings.
  ```json
  {
    "suggestions": [
      "a minimalist coffee cup icon",
      "an abstract mountain landscape",
      "a geometric pattern with warm tones"
    ]
  }
  ```

### POST /api/v1/generate/ideas

**Generate asset ideas**

Generate creative asset concept ideas for a given topic. Returns an array of idea descriptions.

  Request Body:
    - topic: string (required) — Topic to brainstorm ideas for.

  Response (200): Array of idea strings.
  ```json
  {
    "ideas": [
      "A series of icons representing different coffee brewing methods",
      "Illustrated scenes of a cozy coffee shop atmosphere"
    ]
  }
  ```

## Assets

List, retrieve, delete, favorite, share, and remove backgrounds from generated assets.

### GET /api/v1/assets

**List assets**

List generated assets with pagination. Results are scoped to the authenticated user or brand.

  Query Parameters:
    - styleId: string — Filter by style ID.
    - limit: number (default: 50) — Max results per page (max 100).
    - offset: number (default: 0) — Pagination offset.

  Response (200): Paginated asset list with public image URLs (no expiry).
  ```json
  {
    "assets": [{
      "id": "uuid",
      "prompt": "a coffee cup icon",
      "imageUrl": "https://...",
      "aspectRatio": "1:1",
      "highQuality": false,
      "fileSizeBytes": 245000,
      "createdAt": "2025-01-01T00:00:00Z",
      "styleId": "uuid",
      "threadId": "uuid",
      "versionNumber": 1
    }],
    "total": 42,
    "limit": 50,
    "offset": 0
  }
  ```

### GET /api/v1/assets/:id

**Get an asset**

Get a single asset by ID, including both view and download URLs.

  Path Parameters:
    - id: string (required) — Asset UUID.

  Response (200): Full asset details with public image URL and download URL (no expiry).
  ```json
  {
    "id": "uuid",
    "prompt": "a coffee cup icon",
    "imageUrl": "https://...",
    "downloadUrl": "https://...",
    "aspectRatio": "1:1",
    "highQuality": false,
    "fileSizeBytes": 245000,
    "createdAt": "2025-01-01T00:00:00Z",
    "styleId": "uuid",
    "threadId": "uuid",
    "versionNumber": 1,
    "brandId": null
  }
  ```

### DELETE /api/v1/assets/:id

**Delete an asset**

Soft-delete an asset (sets is_deleted: true). The image file remains in storage.

  Path Parameters:
    - id: string (required) — Asset UUID.

  Response (200): Deletion confirmation.
  ```json
  { "deleted": true }
  ```

### POST /api/v1/assets/:id/favorite

**Toggle favorite**

Toggle or explicitly set the favorite status of an asset.

  Path Parameters:
    - id: string (required) — Asset UUID.

  Request Body:
    - favorite: boolean — Set explicitly, or omit to toggle.

  Response (200): Updated favorite state.
  ```json
  { "id": "uuid", "isFavorite": true }
  ```

### POST /api/v1/assets/:id/share

**Share an asset**

Create a public link to an asset, or return the one it already has. Anyone with the link can see the image; the prompt stays hidden unless the owner shows it in the web app. Links can be turned off in Settings, Shared links.

  Path Parameters:
    - id: string (required) — Asset UUID.

  Response (200): Share URL and token.
  ```json
  {
    "shareUrl": "https://tools.blueprintstudio.ai/s/abc123",
    "token": "abc123"
  }
  ```

### POST /api/v1/assets/:id/remove-bg

**Remove background**

Remove the background from a saved asset image and save the cutout as a new transparent PNG asset linked to the original, which stays unchanged. For game sprites, icons, logos, stickers and product shots. Costs 2 credits per image, charged once per operationId and refunded if removal fails.

  Path Parameters:
    - id: string (required) — Source asset UUID.

  Request Body:
    - operationId: string — This removal's key, a unique id you make up: 8 to 128 letters, digits, - and _ (a UUID works). Reuse it to retry or poll the same removal; it is charged once. Omit to start a new one (the response returns the generated ID).

  Note: Usually takes a few seconds. Out of credits answers 402 GENERATION_LIMIT, as generation does. A failed removal is refunded (creditsRefunded) and closed: retrying that operationId answers 409 BACKGROUND_REMOVAL_REFUNDED, so start a new one. An image with nothing to remove (422 NOTHING_TO_REMOVE) or no subject to keep (422 NO_SUBJECT_FOUND) is refunded too, and one that already has a transparent background (422 ALREADY_TRANSPARENT) is never charged. New removals are limited to 20 a minute per user (429). Sources up to 20 MiB and 40 megapixels; the result keeps the source's size. For a new image, generate with background "transparent" instead.

  Response (200): The new transparent PNG asset. HTTP 202 {status: processing, operationId, retryAfterSeconds} while it runs: call again with the same operationId.
  ```json
  {
    "imageUrl": "https://...",
    "assetId": "uuid",
    "parentAssetId": "uuid",
    "creditsCharged": 2,
    "operationId": "uuid"
  }
  ```

## Styles

Manage visual styles: list, create, update and delete custom styles, analyze images for style extraction, and manage reference images.

### GET /api/v1/styles

**List styles**

List available styles including system defaults and user/brand custom styles. With a brand key, isBrandDefault marks the brand's default Style: a suggestion, since POST /api/v1/generate applies no Style without styleId. Archived styles are left out; they still work by ID.

  Response (200): Styles with public details. Protected instructions are empty and instructionsVisible is false. isBrandDefault (brand keys only) is true for the brand's default Style.
  ```json
  {
    "styles": [{
      "id": "uuid",
      "name": "Minimalist",
      "description": "Clean, minimal design",
      "stylePrompt": "",
      "instructionsVisible": false,
      "colorPalette": [{ "hex": "#000", "name": "Black", "role": "primary" }],
      "styleDescriptors": ["clean", "modern"],
      "mood": "professional",
      "isSystem": true,
      "isDefault": false,
      "isBrandDefault": false,
      "brandId": null,
      "createdAt": "2025-01-01T00:00:00Z"
    }]
  }
  ```

### GET /api/v1/styles/:id

**Get a style**

Get a style by ID. Instructions are visible to their author; inherited protected instructions are withheld. An archived style says when it was archived (archivedAt).

  Path Parameters:
    - id: string (required) — Style UUID.

  Response (200): Style details and instruction visibility.
  ```json
  {
    "id": "uuid",
    "name": "Minimalist",
    "description": "Clean, minimal design",
    "stylePrompt": "",
    "instructionsVisible": false,
    "colorPalette": [...],
    "styleDescriptors": ["clean", "modern"],
    "mood": "professional",
    "isSystem": true,
    "isDefault": false,
    "brandId": null,
    "createdAt": "2025-01-01T00:00:00Z"
  }
  ```

### POST /api/v1/styles

**Create a style**

Create a new custom style with an AI generation prompt, optional color palette, and reference images.

  Request Body:
    - name: string (required) — Style name.
    - stylePrompt: string (required) — AI generation prompt for this style.
    - description: string — Human-readable description.
    - colorPalette: array — Array of { hex, name, role } color objects.
    - styleDescriptors: array — Array of descriptor strings (e.g., ["clean", "modern"]).
    - mood: string — Style mood (e.g., "playful", "professional").
    - brandId: string — Must match the API key's workspace: its brand ID for a brand key, omitted (or null) for a personal key. Styles are created in the key's workspace; a different brandId returns 403.
    - referenceImages: array — Array of { data (base64), mimeType, filename } objects. Max 5, up to 20 MB each. Read by their bytes, not mimeType: PNG and WebP are kept as sent; a JPEG keeps only its first picture, without EXIF, XMP, comments or anything after it (orientation kept); AVIF (up to 8 megapixels) and GIF are saved as a PNG or WebP of their first frame, as is a PNG or WebP with anything after its end; complete PDFs are kept; anything else is refused with 415. A filename, when given, must be text (400 otherwise).

  Response (201): Created style details.
  ```json
  {
    "id": "uuid",
    "name": "My Custom Style",
    "description": "...",
    "stylePrompt": "Your authored instructions",
    "instructionsVisible": true,
    "colorPalette": [...],
    "styleDescriptors": [...],
    "mood": "playful",
    "brandId": null,
    "createdAt": "2025-01-01T00:00:00Z"
  }
  ```

### PUT /api/v1/styles/:id

**Update a style**

Update a custom style. Protected styles accept metadata changes only; omit withheld instruction fields. System styles cannot be modified.

  Path Parameters:
    - id: string (required) — Style UUID.

  Request Body:
    - name: string — New name.
    - stylePrompt: string — New AI generation prompt.
    - description: string — New description.
    - colorPalette: array — New color palette.
    - styleDescriptors: array — Keywords describing the style.
    - mood: string — New mood.

  Response (200): Updated style details.
  ```json
  {
    "id": "uuid",
    "name": "Updated Style",
    "description": "...",
    "stylePrompt": "",
    "instructionsVisible": false,
    "colorPalette": [...],
    "mood": "bold",
    "updatedAt": "2025-01-01T00:00:00Z"
  }
  ```

### DELETE /api/v1/styles/:id

**Delete a style**

Delete a custom style. System styles cannot be deleted.

  Path Parameters:
    - id: string (required) — Style UUID.

  Response (200): Deletion confirmation.
  ```json
  { "deleted": true }
  ```

### POST /api/v1/styles/analyze

**Analyze images for style**

Upload images to extract a style guide using AI vision. Returns colors, descriptors, typography analysis, mood, and a generated prompt.

  Request Body:
    - files: array (required) — Array of { data (base64), mimeType, filename } objects. Max 5 images.

  Rate Limit: Up to 10 requests a minute per user, shared by style analysis, brand analysis and brand setup.

  Response (200): Extracted style analysis.
  ```json
  {
    "analysis": {
      "colors": ["#FF5733", "#333333"],
      "styleDescriptors": ["bold", "modern"],
      "typographyStyle": "sans-serif, clean",
      "mood": "energetic",
      "backgroundSuggestion": "white",
      "visualTechnique": "flat illustration",
      "generatedPrompt": "..."
    }
  }
  ```

### POST /api/v1/styles/:id/references

**Manage reference images**

Add or remove reference images from a custom style. System styles cannot be modified.

  Path Parameters:
    - id: string (required) — Style UUID.

  Request Body:
    - add: array — Array of { data (base64), mimeType, filename } objects to add. Max 5.
    - remove: array — Array of storage paths to remove.

  Response (200): Updated style with current reference files.
  ```json
  {
    "style": {
      "id": "uuid",
      "name": "My Style",
      "sourceFiles": [{ "storage_path": "...", "filename": "ref.png", "type": "image/png" }],
      "...": "..."
    }
  }
  ```

## Brands

Manage brands: create, update, delete, manage settings, list and add official logos and brand assets, set the brand icon, and analyze websites for brand identity. An API key works only in the workspace it is bound to: a brand key can use only its own brand ID (any other returns 404), and a personal key cannot act on brands.

### GET /api/v1/brands

**List brands**

List the brands this API key can work in: a brand-bound key sees only its own brand (while you are still a member), and a personal key gets an empty list.

  Response (200): Array of brands with role and member count.
  ```json
  {
    "brands": [{
      "id": "uuid",
      "name": "Acme Corp",
      "slug": "acme-corp",
      "role": "owner",
      "memberCount": 3,
      "createdAt": "2025-01-01T00:00:00Z",
      "updatedAt": "2025-01-01T00:00:00Z"
    }]
  }
  ```

### POST /api/v1/brands

**Create a brand**

Create a new brand. The authenticated user becomes the owner.

  Request Body:
    - name: string (required) — Brand name.

  Note: Brand-bound API keys cannot create brands (403). Use a personal key or the web app. The personal key that creates a brand can't act on it: to use the new brand through the API, create a key bound to it (choose the brand under API Keys in Settings).

  Response (201): Created brand details.
  ```json
  {
    "brand": {
      "id": "uuid",
      "name": "Acme Corp",
      "slug": "acme-corp",
      "role": "owner",
      "createdAt": "2025-01-01T00:00:00Z"
    }
  }
  ```

### GET /api/v1/brands/:id

**Get a brand**

Get a single brand with optional member list and settings includes.

  Path Parameters:
    - id: string (required) — Brand UUID.

  Query Parameters:
    - members: string (enum: true) — Set to "true" to include member list.
    - settings: string (enum: true) — Set to "true" to include brand settings.

  Response (200): Brand details with optional members and settings.
  ```json
  {
    "brand": {
      "id": "uuid",
      "name": "Acme Corp",
      "slug": "acme-corp",
      "role": "owner",
      "permissions": ["write"],
      "createdAt": "2025-01-01T00:00:00Z",
      "members": [{ "id": "uuid", "userId": "uuid", "role": "owner", "joinedAt": "...", "user": { "email": "...", "display_name": "..." } }],
      "settings": { "defaultStyleId": "uuid", "promptTemplate": null, "instructionsVisible": false }
    }
  }
  ```

### PUT /api/v1/brands/:id

**Update a brand**

Update a brand's name. Requires admin or owner role.

  Path Parameters:
    - id: string (required) — Brand UUID.

  Request Body:
    - name: string — New brand name.

  Response (200): Updated brand details.
  ```json
  {
    "brand": {
      "id": "uuid",
      "name": "New Name",
      "slug": "acme-corp",
      "updatedAt": "2025-01-01T00:00:00Z"
    }
  }
  ```

### DELETE /api/v1/brands/:id

**Delete a brand**

Permanently delete a brand with all its images and Styles, and remove its members and pending invites. Only the owner can delete. This can't be undone.

  Path Parameters:
    - id: string (required) — Brand UUID.

  Response (200): Deletion confirmation, with the number of images deleted.
  ```json
  { "deleted": true, "deletedImages": 42 }
  ```

### GET /api/v1/brands/:id/settings

**Get brand settings**

Get a brand's default style and prompt template settings.

  Path Parameters:
    - id: string (required) — Brand UUID.

  Response (200): Brand settings.
  ```json
  {
    "settings": {
      "defaultStyleId": "uuid",
      "promptTemplate": "Create a {prompt} in our brand style",
      "createdAt": "2025-01-01T00:00:00Z",
      "updatedAt": "2025-01-01T00:00:00Z"
    }
  }
  ```

### PUT /api/v1/brands/:id/settings

**Update brand settings**

Update a brand's default style and prompt template. Requires admin or owner role.

  Path Parameters:
    - id: string (required) — Brand UUID.

  Request Body:
    - defaultStyleId: string (nullable) — Default style ID for generation: a system Style or one of this brand's Styles (anything else returns 400 INVALID_DEFAULT_STYLE), or null to clear.
    - promptTemplate: string (nullable) — Prompt template with {prompt} placeholder, or null to clear.

  Response (200): Updated brand settings.
  ```json
  {
    "settings": {
      "defaultStyleId": "uuid",
      "promptTemplate": "Create a {prompt} in our brand style",
      "createdAt": "2025-01-01T00:00:00Z",
      "updatedAt": "2025-01-01T00:00:00Z"
    }
  }
  ```

### GET /api/v1/brands/:id/assets

**List official logos and brand assets**

List the brand's official logos, artwork and product images, newest first. Pass an item's generationInput in POST /api/v1/generate inputs to use that exact official version: the image model is told to keep the logo exactly. To choose a logo, pass kind=logo for the logos only, and see them with GET /api/v1/brands/:id/assets/sheet?kind=logo (one numbered image). To save an asset's file into a project, GET its downloadUrl: no key needed, for about an hour. Same answer as MCP list_brand_assets.

  Path Parameters:
    - id: string (required) — Brand UUID.

  Query Parameters:
    - limit: integer (default: 24) — Items per page, 1 to 50.
    - cursor: string — nextCursor from the previous page (with the same kind).
    - kind: string (enum: logo | artwork | product) — List only this kind, filtered before paging: logo (to choose a logo), artwork or product. Leave it out to list every official asset.
    - previews: string (enum: true) — Set to "true" to add previewDataUrl (a 256 px WebP) to up to 12 assets.

  Note: To save an asset's file, GET its downloadUrl (no key needed, about an hour); an official logo's publicUrl is public and lasting, for showing it. currentVersion.imageUrl and currentVersion.downloadUrl need a signed-in web session. To use an asset in an image, pass its generationInput, not the file. Add official logos with POST /api/v1/brands/:id/assets, or under Brand assets in the web app (brandAssetsUrl).

  Response (200): One page of official assets.
  Response fields:
    - kind: string — Only when kind was passed: the kind listed.
    - assets[].form: string (nullable) — A logo's form: mark (a symbol alone), wordmark (the name in type) or lockup (both together).
    - assets[].background: string (nullable) — The background it's made for: light, dark or any.
    - assets[].primary: boolean — The brand's main logo (one per brand).
    - assets[].downloadUrl: string — The asset's original file, to save into a project: a plain GET with no API key or sign-in returns it (curl -L -o <suggestedFilename> <downloadUrl>) until downloadExpiresAt. After that it answers 410; list the assets again for a new link. Treat it like a password: anyone holding it can download that file until it expires.
    - assets[].downloadExpiresAt: string — When downloadUrl stops working (ISO 8601), about an hour after the list.
    - assets[].suggestedFilename: string — A readable file name for the download, with the file's real extension.
    - assets[].mimeType: string — The downloaded file's content type: image/png, image/jpeg or image/webp.
    - assets[].publicUrl: string — Official logos only: the file at a public link that doesn't expire and can be cached, for a page, email or design that shows the logo. Gone once the logo is archived or its brand deleted.
    - assets[].previewUrl: string — Official logos only: a public copy at most 1024 px on its longest side.
    - officialLogos: integer — How many official logos the brand has (artwork and products not counted).
    - canManage: boolean — Whether this key's owner is a brand owner or admin, who can add and replace official assets.
    - brandAssetsUrl: string — Opens the brand's Brand assets panel in the web app, where owners and admins add logos.
    - nextStep: string — Only when the brand has no official logos (don't invent one, and how a person adds one), or when kind found none of that kind.
  ```json
  {
    "assets": [{
      "id": "uuid",
      "brandId": "uuid",
      "name": "Acme",
      "variant": "Dark background",
      "kind": "logo",
      "revision": 1,
      "currentVersion": { "id": "uuid", "version": 1, "mimeType": "image/png", "width": 912, "height": 204 },
      "generationInput": { "kind": "brand_asset", "assetId": "uuid", "versionId": "uuid" },
      "downloadUrl": "https://tools.blueprintstudio.ai/api/brand-asset-files/<token>",
      "downloadExpiresAt": "2026-10-12T10:00:00.000Z",
      "suggestedFilename": "acme-dark-background-7f1c2a9b.png",
      "mimeType": "image/png",
      "publicUrl": "https://<project>.supabase.co/storage/v1/object/public/brand-logos/<brand>/<asset>/<version>.png",
      "previewUrl": "https://<project>.supabase.co/storage/v1/render/image/public/brand-logos/<brand>/<asset>/<version>.png?width=1024&height=1024&resize=contain&quality=80"
    }],
    "nextCursor": null,
    "canManage": true,
    "officialLogos": 1,
    "brandAssetsUrl": "https://tools.blueprintstudio.ai/asset-generator?brand=uuid&panel=brand-assets"
  }
  ```

### GET /api/v1/brands/:id/assets/sheet

**Logo sheet image**

One PNG of a page of the brand's official assets (pass kind=logo for the logos only), to look at before choosing a logo: each numbered, with its name and variant (and Primary on the main logo). A light logo sits on a dark swatch so it shows; the swatch isn't part of the logo, and the labels (form, background) are in GET /api/v1/brands/:id/assets. Numbers follow the order of GET /api/v1/brands/:id/assets with the same kind, cursor and limit, counted across pages (the second sheet starts at 25); X-Sheet-Asset-Ids lists the drawn assets' ids in number order. The REST twin of MCP list_brand_assets with previews: true.

  Path Parameters:
    - id: string (required) — Brand UUID.

  Query Parameters:
    - kind: string (enum: logo | artwork | product) — Show only this kind: logo (to choose a logo), artwork or product. Leave it out for every official asset.
    - limit: integer (default: 24) — Assets on the sheet, 1 to 24.
    - cursor: string — nextCursor from GET /api/v1/brands/:id/assets with the same kind, or the X-Next-Cursor header of the previous sheet.

  Note: A brand with no official assets (of the kind asked for) answers 404 with code NO_BRAND_ASSETS and a nextStep saying what to do. An image that can't be read shows as "Preview unavailable"; the sheet still draws.

  Response (200): The logo sheet as image/png, 880 to about 1,600 px wide. Headers: X-Sheet-Asset-Ids (comma-separated asset ids, in number order), X-Sheet-First-Number, X-Total-Count (active assets in the brand) and X-Next-Cursor (when there's another page).
  ```http
  HTTP/1.1 200 OK
  Content-Type: image/png
  X-Sheet-Asset-Ids: 7f1c…,0b9e…,c2d4…
  X-Sheet-First-Number: 1
  X-Total-Count: 3
  
  <PNG image>
  ```

### POST /api/v1/brands/:id/assets

**Add an official logo or brand asset**

Add an official logo, artwork or product image to the brand, or a new version of one, so generation can use the real file. The same publication as the web app's Brand assets panel: owners and admins only, the file checked by its bytes (PNG, JPEG, WebP, GIF or AVIF; SVG is saved as a PNG drawn at 2048 px), stored privately with its versions kept (a logo also gets a public link: publicUrl when the assets are listed). Same answer as MCP add_brand_asset.

  Path Parameters:
    - id: string (required) — Brand UUID.

  Request Body:
    - url: string — A public https link to the image, or a saved image's imageUrl or webUrl. Send exactly one of url, assetId, dataUrl, uploadId, or a multipart file.
    - assetId: string — A saved image in this brand's library.
    - dataUrl: string — The image as base64: a data URL or bare base64 (its declared type is ignored).
    - uploadId: string — A file from POST /api/v1/uploads or an upload link.
    - name: string — Up to 100 characters. Defaults to the brand's name (or the current name when replacing).
    - variant: string — A short label telling versions apart, up to 60 characters, such as "White wordmark, for dark backgrounds".
    - kind: string (default: logo, enum: logo | artwork | product) — What it is.
    - form: string (enum: mark | wordmark | lockup) — Logos: mark (a symbol alone), wordmark (the name in type) or lockup (both together).
    - background: string (enum: light | dark | any) — The background it's made for.
    - primary: boolean — Make it the brand's main logo (the previous one is unset).
    - replaceAssetId: string — Replace this asset with a new version instead of adding a new asset. Needs expectedRevision. Every later image uses the new version; earlier versions are kept.
    - expectedRevision: integer — Required with replaceAssetId: the asset's revision from GET /api/v1/brands/:id/assets, so another person's change isn't overwritten.
    - requestId: string — This addition's key, a unique id you make up: 8 to 128 letters, digits, - and _ (a UUID works). Retrying with the same requestId adds nothing twice.

  Note: Or send multipart/form-data: the image in the field file (up to 4 MB) and the other fields as text. A refused image answers with code INVALID_REFERENCE_IMAGE (or REFERENCE_URL_BLOCKED, REFERENCE_URL_UNREACHABLE, REFERENCE_IMAGE_TOO_LARGE), field source, reason, the link's host and nextStep. Up to 30 additions an hour per person.

  Rate Limit: 30 an hour per person.

  Response (201): The official asset, with generationInput for POST /api/v1/generate inputs. 200 when a retry with the same requestId found it already added.
  ```json
  {
    "asset": {
      "id": "uuid",
      "name": "Acme",
      "variant": "White wordmark, for dark backgrounds",
      "kind": "logo",
      "revision": 1,
      "currentVersion": { "id": "uuid", "version": 1, "mimeType": "image/png", "width": 2048, "height": 512 },
      "generationInput": { "kind": "brand_asset", "assetId": "uuid", "versionId": "uuid" }
    },
    "replayed": false,
    "replaced": false,
    "officialLogos": 1,
    "brandAssetsUrl": "https://tools.blueprintstudio.ai/asset-generator?brand=uuid&panel=brand-assets"
  }
  ```

### PATCH /api/v1/brands/:id/assets/:assetId

**Label an official logo or brand asset**

Set the labels that pick the right logo for each image: form, the background it's made for, and the brand's main logo. Owners and admins only. Fields left out keep their value; null clears form or background. Changes only the labels, never the file. Same answer as MCP update_brand_asset.

  Path Parameters:
    - id: string (required) — Brand UUID.
    - assetId: string (required) — Asset UUID.

  Request Body:
    - form: string (nullable, enum: mark | wordmark | lockup) — Logos only: mark, wordmark or lockup.
    - background: string (nullable, enum: light | dark | any) — The background it's made for.
    - primary: boolean — Logos only: true makes it the brand's main logo and unsets the previous one.

  Response (200): The asset with its labels, and whether anything changed.
  ```json
  {
    "asset": { "id": "uuid", "name": "Acme", "kind": "logo", "form": "wordmark", "background": "dark", "primary": true, "generationInput": { "kind": "brand_asset", "assetId": "uuid", "versionId": "uuid" } },
    "changed": true
  }
  ```

### PUT /api/v1/brands/:id/logo

**Set brand icon**

Set the small brand icon shown next to the brand's name in the app. Requires admin or owner role. This is not an official logo: image generation never uses the icon. To use a logo in images, add it as an official logo with POST /api/v1/brands/:id/assets, then pass its generationInput to generate.

  Path Parameters:
    - id: string (required) — Brand UUID.

  Request Body:
    - data: string (required) — Base64-encoded image data.
    - mimeType: string (required) — Image MIME type (e.g., "image/png").

  Note: Saved as a trimmed PNG of up to 512 px. The response field is named logoUrl for compatibility; it is the icon's URL.

  Response (200): Public URL of the brand icon (no expiry).
  ```json
  { "logoUrl": "https://..." }
  ```

### POST /api/v1/brands/analyze

**Analyze brand website**

Analyze a website to extract brand visual identity using an AI agent. Returns an NDJSON stream with status updates, insights, and a final style guide with 1–3 recommended styles.

  Request Body:
    - url: string (required) — Website URL to analyze.

  Note: Response is streamed as application/x-ndjson. Each line is a JSON object. Processing may take up to 50 seconds. The url must be a public http(s) website: private-network addresses are refused with 400, and the analysis reads pages of that site only.

  Rate Limit: Up to 10 requests a minute per user, shared by style analysis, brand analysis and brand setup.

  Response (200): NDJSON stream of events: { type: "status" | "insight" | "result" | "error", ... }
  ```json
  {"type":"status","message":"Starting brand analysis..."}
  {"type":"status","message":"Extracting color palette..."}
  {"type":"result","styles":[{"id":"style-1","name":"Brand Style","stylePrompt":"...","colors":["#FF5733"]}],"orgType":"saas","brandName":"Acme"}
  ```

### PATCH /api/v1/brands/:id/members/:userId

**Update member role**

Update a team member's role in the brand. Only the brand owner can change roles.

  Path Parameters:
    - id: string (required) — Brand UUID.
    - userId: string (required) — Member user ID.

  Request Body:
    - role: string (required, enum: admin | member) — New role for the member.

  Response (200): Updated member details.
  ```json
  {
    "member": {
      "userId": "uuid",
      "role": "admin",
      "updatedAt": "2025-01-01T00:00:00Z"
    }
  }
  ```

### DELETE /api/v1/brands/:id/members/:userId

**Remove a member**

Remove a team member from the brand.

  Path Parameters:
    - id: string (required) — Brand UUID.
    - userId: string (required) — Member user ID.

  Response (200): Removal confirmation.
  ```json
  { "removed": true }
  ```

### POST /api/v1/brands/:id/members/invite

**Invite a member**

Invite a new member to the brand by email.

  Path Parameters:
    - id: string (required) — Brand UUID.

  Request Body:
    - email: string (required) — Email address of the person to invite.
    - role: string (default: member, enum: admin | member) — Role to assign the invited member.

  Rate Limit: Every 24 hours: up to 5 invite emails to the same address from one brand, 20 to the same address across all brands, and 50 invites in total per person.

  Response (201): Invite details including the invite URL.
  ```json
  {
    "invite": {
      "id": "uuid",
      "email": "user@example.com",
      "role": "member",
      "inviteUrl": "https://tools.blueprintstudio.ai/invite/abc123",
      "expiresAt": "2025-01-08T00:00:00Z"
    }
  }
  ```

## Uploads

Send image files that aren't at a public link: a logo to add as an official brand asset, or an image for generation. Each upload is checked by its bytes and works for 24 hours, in the key's workspace only.

### POST /api/v1/uploads

**Upload an image**

Upload one image of up to 4 MB as multipart/form-data, in the field file. PNG, JPEG, WebP, GIF, AVIF or SVG (saved as a PNG); anything else is refused by name, whatever its declared type. A brand key needs write access to the brand.

  Note: Use uploadId with POST /api/v1/brands/:id/assets, or generationInput in POST /api/v1/generate inputs. For files over 4 MB, use an upload link.

  Rate Limit: 60 files an hour per person, counted with upload link files, and at most 200 uploads from the last 24 hours (UPLOAD_QUOTA).

  Response (201): The checked upload, ready to use.
  ```json
  {
    "upload": {
      "uploadId": "uuid",
      "status": "ready",
      "source": "rest",
      "filename": "logo.png",
      "mimeType": "image/png",
      "width": 1024,
      "height": 256,
      "byteSize": 48211,
      "expiresAt": "2026-10-07T09:00:00.000Z",
      "generationInput": { "kind": "upload", "uploadId": "uuid" }
    }
  }
  ```

### POST /api/v1/upload-links

**Create an upload link**

A link that takes image files of up to 20 MB for 30 minutes. pageUrl is a page where the account owner, signed in, can drop up to 10 files. With direct: true, upload is also a single-use URL to PUT one file to (upload.curl is the command). A brand key needs write access to the brand. Same answer as MCP create_upload_link.

  Request Body:
    - direct: boolean (default: false) — true to also get upload, a single-use URL to PUT one file to. Leave it out when a person drops the files on pageUrl.

  Note: upload.url is a bearer link for one file: keep it private, and ask for it only when you'll PUT the file yourself. After the PUT, upload.uploadId works once GET /api/v1/upload-links/:id lists it as ready (or straight away in POST /api/v1/brands/:id/assets and POST /api/v1/generate, which check it themselves).

  Rate Limit: 20 links an hour per person.

  Response (201): The link and its page, and with direct its single-use upload URL.
  ```json
  {
    "uploadLinkId": "uuid",
    "expiresAt": "2026-10-06T09:30:00.000Z",
    "pageUrl": "https://tools.blueprintstudio.ai/upload/uuid",
    "upload": {
      "uploadId": "uuid",
      "url": "https://...supabase.co/storage/v1/object/upload/sign/agent-uploads/...?token=...",
      "method": "PUT",
      "headers": { "Content-Type": "application/octet-stream", "apikey": "..." },
      "curl": "curl -fsS -X PUT ... --data-binary @<file> https://...",
      "generationInput": { "kind": "upload", "uploadId": "uuid" }
    },
    "maxFiles": 10,
    "maxBytes": 20971520
  }
  ```

### GET /api/v1/upload-links/:id

**List an upload link's files**

The files received on an upload link, each checked by its bytes. Same answer as MCP get_upload_link.

  Path Parameters:
    - id: string (required) — The uploadLinkId.

  Note: A refused file has status rejected and rejection, its reason (such as html_page or too_large). A file that just arrived is waiting while it's checked. source says how each arrived: page, or direct (PUT to upload.url).

  Response (200): The link and its files.
  ```json
  {
    "uploadLinkId": "uuid",
    "brandId": "uuid",
    "open": true,
    "expiresAt": "2026-10-06T09:30:00.000Z",
    "pageUrl": "https://tools.blueprintstudio.ai/upload/uuid",
    "files": [{ "uploadId": "uuid", "status": "ready", "source": "page", "filename": "logo.svg", "mimeType": "image/png", "generationInput": { "kind": "upload", "uploadId": "uuid" } }],
    "directUpload": "ready"
  }
  ```

## API Keys

Manage API keys — list active keys, create new ones, and revoke existing keys. A key only sees and manages keys in its own workspace (its brand, or the personal library). API keys are on paid plans.

### GET /api/v1/api-keys

**List API keys**

List your active (non-revoked) API keys in the calling key's workspace: a brand key lists that brand's keys, a personal key lists personal keys.

  Response (200): Array of API key metadata (keys are never returned after creation).
  ```json
  {
    "keys": [{
      "id": "uuid",
      "name": "Production",
      "prefix": "bp_live_abc",
      "tier": "pro",
      "lastUsedAt": "2025-01-01T00:00:00Z",
      "requestsToday": 42,
      "createdAt": "2025-01-01T00:00:00Z",
      "expiresAt": null,
      "brandId": null
    }]
  }
  ```

### POST /api/v1/api-keys

**Create an API key**

Create a new API key. Accepts either a Bearer token or session cookie auth (for logged-in users bootstrapping their first key). The full key is only returned once — save it immediately. API keys are on paid plans: without Pro access where the key will work, this answers 403 with code PLAN_REQUIRED and plansUrl.

  Request Body:
    - name: string (default: Default) — Key name for identification.
    - brandId: string (nullable) — Brand to bind the key to, or null for the personal library. With a Bearer key this must be the calling key's own workspace; omit it to inherit that workspace.

  Note: The full key value is only returned in this response. Store it securely. A key can never create a wider key: a brand key only keys for the same brand, a personal key only personal keys (403 otherwise). A key created with a Bearer key stops working when the key that created it is revoked (and when the AI app that created that key is disconnected), and a chain of keys created by keys is at most 10 long (403 beyond). This endpoint also accepts session cookie auth — no existing API key needed if the user is logged in — which can create personal keys or keys for any brand you belong to, and those keys stand alone.

  Response (201): Full API key (shown only once) with metadata.
  ```json
  {
    "key": "bp_live_abc123...",
    "prefix": "bp_live_abc",
    "name": "Production",
    "tier": "pro",
    "brandId": null,
    "message": "Save this key — it will not be shown again."
  }
  ```

### DELETE /api/v1/api-keys/:id

**Revoke an API key**

Revoke an API key in the calling key's workspace, and every key it created. Keys in other workspaces return 404. You cannot revoke the key currently being used for authentication (400), or a key that created it (409), since that would revoke the key in use too.

  Path Parameters:
    - id: string (required) — API key UUID.

  Note: To rotate a key, create the replacement under API Keys in Settings (or with a signed-in session), not with the old key: a key created by the old key is revoked along with it.

  Response (200): Revocation confirmation.
  ```json
  { "revoked": true }
  ```

## Settings

Get and update user-level settings: default style, prompt template and the preset for new images (fast, balanced or best).

### GET /api/v1/settings

**Get user settings**

Get the authenticated user's default style, prompt template and image preset (what POST /api/v1/generate makes when modelId is left out). Personal API keys only; a brand-bound key gets 404.

  Response (200): User settings.
  ```json
  {
    "settings": {
      "defaultStyleId": "uuid",
      "promptTemplate": null,
      "imagePreset": "balanced"
    }
  }
  ```

### PUT /api/v1/settings

**Update user settings**

Update the authenticated user's default style, prompt template or image preset. Personal API keys only; a brand-bound key gets 404.

  Request Body:
    - defaultStyleId: string (nullable) — Default style ID: a system Style or one of your personal Styles (anything else returns 400 INVALID_DEFAULT_STYLE), or null to clear.
    - promptTemplate: string (nullable) — Prompt template with {prompt} placeholder, or null to clear.
    - imagePreset: string (enum: fast | balanced | best) — The preset for new images, used when a generation leaves modelId out: "fast" (Flare 2.6, low quality, 2K), "balanced" (Sunburst 2.6, medium quality, 2K), "best" (Sunburst 2.6, high quality, 4K). Everyone starts on "balanced".

  Response (200): Updated settings.
  ```json
  {
    "settings": {
      "defaultStyleId": "uuid",
      "promptTemplate": "Create a {prompt} in minimalist style",
      "imagePreset": "fast"
    }
  }
  ```

## Usage

Check current usage, rate limits, and remaining quotas.

### GET /api/v1/usage

**Get usage & limits**

Read the authenticated user's Asset Generator credits, when monthly credits reset (a paid plan's billing date; null when nothing refills: Free's credits are one-time), API request count and plansUrl (the plans page, tagged ?ref=api like every web-app link in an API response). On Free, when the balance can't cover a default image, checkoutUrl is a one-step Stripe checkout for Pro on this account (no sign-in, good for 7 days) and nextStep says to show it. Despite their names, the generations* fields count credits, not images: an image costs credits by model, size and quality (a standard image is creditsPerDefaultImage). To know whether you can generate, read generationsRemaining, or canAffordImage. Credits belong to the user across brands; Blueprint client-service subscriptions are not exposed. Pause on a limit, preserve the request and recheck usage after the user upgrades.

  Response (200): Current usage and limits.
  Response fields:
    - generationsRemaining: integer — Credits you can spend right now: monthly credits left plus any welcome credits and extra credits. The number to check before generating.
    - generationsUsed: integer — Monthly credits used since the last reset. Welcome credits are spent first and don't count here.
    - generationsLimit: integer — Monthly credits included in the plan: 900 on Pro and 10,000 on Max. 0 on Free, which starts with one-time welcome credits instead; an older Free account may still show 40 until its last monthly credits end (monthlyCreditsEndAt). generationsRemaining can be higher while welcome or extra credits last.
    - monthlyCreditsRemaining: integer — generationsLimit minus generationsUsed.
    - welcomeCredits: object (nullable) — One-time credits for new accounts (500, usable for 30 days): { remaining, expiresAt }. Spent before monthly credits; null when none are left.
    - grantedCredits: object (nullable) — Extra credits granted to the account: { remaining, expiresAt, granted }. expiresAt is the soonest expiry. Spent with welcome credits, before monthly credits; null when none are left.
    - addedCredits: object (nullable) — Credits added with credit packs (Max): { remaining, expiresAt, granted }. expiresAt is the soonest expiry; each pack lasts 12 months. Spent after monthly credits; null when none are left.
    - creditPacks: object (nullable) — Only for Max subscribers: the credit packs they can add ({ url, packs: [{ credits, price, currency }], usedAfterMonthlyCredits, validForMonths, nextStep }). nextStep is set when credits are nearly or all gone: a line to show the person, with url (Settings › Billing). Absent on other plans.
    - access: object — Which models and sizes the account can use, and why: { tier, tierName, accessFrom, note, until?, brands }. Every plan, Free included, has every model and size. tier is the tier for models and sizes outside a brand (match a model's minimumTier and its sizes in the catalog); accessFrom is plan, or credits when a client grant of 600 or more credits unlocks Pro until it expires or is used up (until). brands lists brands this key can use that include more than the plan (accessFrom brand), with their own tier and note. Credits are charged the same either way.
    - resetsAt: string (nullable) — When monthly credits next reset: the plan's billing date each month (monthly from the start of a yearly plan's billing period). Null when nothing refills: Free's credits are one-time, and a plan set to end doesn't renew, so only a plan change or new credits add more. Also null for a moment while a renewal's payment goes through (the credits come with it).
    - monthlyCreditsEndAt: string (nullable) — When an older Free account's last monthly credits stop (UTC, first of the month). They don't refill; null on paid plans and for every other Free account.
    - creditsPerDefaultImage: integer — What a standard image (default model, size and quality) costs.
    - approxDefaultImagesRemaining: integer — About how many standard images generationsRemaining buys.
    - canAffordImage: boolean — False when not even cheapestImage fits; nextStep then says what to do.
    - nextStep: string (nullable) — One line for the user once a default image no longer fits, with the plans link; null until then.
    - checkoutUrl: string — Free accounts once a default image no longer fits: a one-step Stripe checkout for Pro on this account, opened in a browser with no sign-in, good for 7 days. Absent otherwise. A bearer link: don't log or share it.
    - apiRequestsToday: integer — This key's API requests today (UTC). Job status polls don't count.
    - apiRequestsLimit: integer — This plan's daily API request limit. A Free account with Pro access from credits or the brand keeps Free's 100.
  ```json
  {
    "product": "asset_generator",
    "billingOwner": "current_user",
    "plansUrl": "https://tools.blueprintstudio.ai/asset-generator/plans?ref=api",
    "resetsAt": "2026-10-17T19:39:36.000Z",
    "monthlyCreditsEndAt": null,
    "creditUnit": "generation_credits",
    "tier": "pro",
    "generationsUsed": 150,
    "generationsLimit": 900,
    "generationsRemaining": 750,
    "monthlyCreditsRemaining": 750,
    "welcomeCredits": null,
    "grantedCredits": null,
    "addedCredits": null,
    "creditSummary": "750 of 900 monthly credits left, and no welcome credits",
    "creditsPerDefaultImage": 9,
    "approxDefaultImagesRemaining": 83,
    "canAffordImage": true,
    "canAffordDefaultImage": true,
    "cheapestImage": { "modelId": "gpt-image-2.5-sunburst", "quality": "low", "size": "2K", "credits": 1 },
    "nextStep": null,
    "access": { "tier": "pro", "tierName": "Pro", "accessFrom": "plan", "note": null, "brands": [] },
    "apiRequestsToday": 42,
    "apiRequestsLimit": 5000
  }
  ```

## Feedback

Tell the Blueprint Studio team what went wrong or what you wanted. Agents should use it when they get stuck, too.

### POST /api/v1/feedback

**Send feedback**

Send a bug report, something confusing, a missing feature, output quality, pricing or praise to the Blueprint Studio team, who see it immediately. Stored with the key's workspace. Quote what the user wrote only when they asked you to pass it on; never include generation prompts, API keys or tokens.

  Request Body:
    - category: string (required, enum: bug | confusing | missing_feature | quality | pricing | praise | other) — What the feedback is about.
    - message: string (required) — What happened or what the user wanted, 1–2000 characters. API keys and bearer tokens are redacted.
    - context: object — Optional identifiers: tool, jobId, assetId, errorCode, model, httpStatus. Other keys and free text are discarded.

  Rate Limit: 10 per hour per user

  Response (201): Feedback received.
  ```json
  {
    "id": "uuid",
    "category": "missing_feature",
    "createdAt": "2026-09-28T10:00:00.000Z",
    "message": "Sent to the Blueprint Studio team. Thank you."
  }
  ```
