Picsha AI

picsha.ai API Specification (v1)

1. Product Philosophy

"S3 with a Brain." Picsha.ai is a serverless, usage-based media backend designed for AI Agents and "Vibe Coders." It abstracts complex processing pipelines (TUS, Rekognition, LibreOffice, Vector Search) into simple, developer-friendly endpoints.

2. Base URL & Authentication

  • Base URL: https://api.picsha.ai/v1
  • Authentication: Bearer Token via Header.
    Authorization: Bearer sk_live_51Mx...
    
  • Multi-Tenant Scoping:
    • Organizations: All requests are automatically scoped to the data silo of the Organization associated with your API Key.
    • End-User Attribution: When acting on behalf of one of your users, pass their identifier in the x-external-user-id header.
    x-external-user-id: user_abc123
    
    Picsha records that user as the owner of what the request creates: uploads and generated images carry it as userId, webhook subscriptions and the notification stream follow that user's events, and usage can be broken down by user.
    • Organization-Wide Reads: The header does not confine a REST request to that user. With an Organization key, listing, search, reading, updating, deleting, downloading and rendering reach every asset in the Organization, whether or not the header is sent. Deduplication is Organization-wide too: an upload whose bytes already exist in the Organization links to the existing file and inherits its tags and metadata, whichever user uploaded it first.
    • Showing each user only their own assets: That is your backend's job. Keep the API key server-side and filter what you return — by the userId on each asset, or by metadata you set at upload, for example GET /assets?metadata={"orderId":"1001"}.
    • Confining an AI agent to one user: The hosted MCP endpoint and the A2A agent do confine a session to the user named in x-external-user-id: every tool then reads and changes only that user's assets.
    • Personal API Keys: x-external-user-id belongs to Organization keys. A personal key — one that does not belong to an Organization — is always scoped to its owner's own assets, and the header is ignored for it: uploads are filed under the key's owner, and listing, search, download, delete and restore only reach the owner's assets.
  • Dynamic Transaction Cost Tracking:
    • Automatic Cost Decoration: JSON object responses from the /v1 operational API include a "cost" attribute in standard currency format (e.g. "$0.0050"), representing the standard retail value of that specific transaction. Endpoints that return a bare JSON array (GET /webhooks, GET /transformations, GET /backgrounds, GET /dam/{assetId}/events) are not decorated — there is nowhere to attach the field.
    • No-Charge Error Guarantee: Any API transaction that results in an HTTP failure (status code >= 300, e.g., validation errors, 401 Unauthorized, or 500 Server Error) reports a transaction cost of "$0.0000".
    • Toggle: Response cost decoration is a deployment-level setting (ENABLE_RESPONSE_COST_TRACKING). Contact support to have it disabled for your account; it is not currently a self-service dashboard toggle.

API Fundamentals

Rate Limits & Quotas

The REST API is not currently rate-limited per request. What it does enforce is your plan quota: when an operation would exceed your organization's free-tier allowance and no billing method is on file, the API responds 402 Payment Required rather than throttling you.

{ "message": "Payment Required: You have exceeded your free tier limits for this operation. Please add a billing method in your dashboard to continue." }

The one rate-limited surface is the hosted MCP endpoint (POST /v1/mcp), capped at 120 requests per minute per credential. It returns 429 Too Many Requests with the standard x-ratelimit-limit, x-ratelimit-remaining, and x-ratelimit-reset headers. Other endpoints do not emit those headers today — do not build client logic that depends on them.

Retries

Requests are not idempotency-keyed. Retrying a failed POST /assets or POST /assets/generate will create a second asset (and a second billing event) if the original actually succeeded, so treat a timeout as "unknown" and reconcile with GET /assets before retrying billed operations. Deduplication does protect uploads of byte-identical files: re-uploading the same bytes returns the existing asset rather than storing and billing a second copy. Files are matched within your Organization; with a personal API key they are matched only against your own uploads, never another account's.

Error Codes

Errors return a flat JSON object with a human-readable message:

{ "message": "Asset not found" }

Some routes use { "error": "..." } instead, and schema-validation failures come back in Fastify's format:

{ "statusCode": 400, "code": "FST_ERR_VALIDATION", "error": "Bad Request", "message": "querystring/limit must be <= 100" }

Always branch on the HTTP status code rather than parsing the body — there is no stable machine-readable code field.

  • 400 Bad Request: Validation failed or invalid parameters were supplied.
  • 401 Unauthorized: Invalid or missing API key.
  • 402 Payment Required: The operation exceeded your plan quota — add a billing method.
  • 403 Forbidden: Missing or invalid delivery signature (Strict Transformations), the asset is blocked by moderation, an x-org-id that does not match your credential, or an s3Key that is not one of your uploads.
  • 404 Not Found: The resource does not exist — or it exists but belongs to another organization. Cross-tenant reads are deliberately indistinguishable from missing records.
  • 429 Too Many Requests: Hosted MCP endpoint rate limit exceeded.
  • 500 Internal Server Error: An unexpected error occurred on Picsha's end.

3. Core Endpoints

A. Ingest (The "Magic" Upload)

POST /assets

Handles all file ingestion types (Multipart, URL, Raw) and triggers the AI processing pipeline based on configuration.

Headers:

  • Content-Type: multipart/form-data OR application/json (for URL fetch)
  • x-external-user-id: string (Optional, Organization keys only) - Unique identifier for the end-user uploading the asset. Without it, the asset belongs to the key's owner. Ignored for personal keys.

Query Parameters:

  • ?ephemeral=true: Marks the asset for automatic deletion after 24 hours. Ideal for temporary processing pipelines where you only need to extract AI summaries or metadata without incurring long-term storage costs.
  • ?expires_at=2026-12-31T23:59:59Z: Optional ISO-8601 timestamp for when the asset should be automatically deleted by our cleanup workers.

Parameters (JSON Body for URL / Config):

{
  "url": "https://example.com/files/quarterly_report.docx",
  "config": {
    "auto_tag": true,          // Vision AI object and face detection. Defaults to true.
    "auto_summarize": true,    // AI summaries for images and documents, transcription for audio/video. Defaults to true.
    "vectorize": true,         // Generates multimodal embeddings for vector similarity search. Defaults to true.
    "location_lookup": true,   // Reverse geocodes EXIF coordinates. Defaults to true.
    "adaptive_stream": false,  // Triggers an async adaptive-bitrate (HLS) transcode job for videos. Defaults to false.
    "content_moderation": false, // Runs the content-moderation safety check. Defaults to false.
    "antivirus_scan": true,    // High-speed malware/virus scanning at ingest. Defaults to true; set false to opt out.
    "quality_metrics": true,   // Deterministic image quality scores + face attributes (no external calls, no per-metric billing). Defaults to true.
    "remove_background": false, // Generates a background-removed alpha cutout at ingest for instant bg_rem / bg_asset rendering. One charge per unique image. Defaults to false.
    "render_on_upload": "w=300&q=80; w=800&fit=cover", // Pre-warms dynamic delivery CDN cache with specific sizes
    "expires_at": "2026-12-31T23:59:59Z" // ISO-8601 timestamp for automatic asset deletion
  },
  "tags": ["finance", "report", "q4"],
  "metadata": {
    "project_id": "my_replit_app_123"
  }
}

Response (Success):

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "url": "https://picsha-assets.s3.us-east-1.amazonaws.com/org_abc/user_pete/550e8400-e29b-41d4-a716-446655440000/my_file.jpg",
  "message": "Asset uploaded successfully",
  "cost": "$0.0050"
}

Note: Ingestion processing runs asynchronously. You will receive an immediate response while the AI extraction, transcoding, and embedding run in the background. Subscribe to the asset.processed webhook to be notified when metadata and derivatives are ready.

B. Client-Side Presigned Upload URL

GET /assets/presigned

Generates a presigned S3 upload URL for direct client-side PUT uploading (e.g. for Uppy or other upload widgets). After the PUT succeeds, register the upload (section B2) to create the asset.

Query Parameters:

  • filename: string (Required) - Original name of the file to be uploaded.
  • contentType: string (Required) - MIME type of the file.

Response:

{
  "method": "PUT",
  "url": "https://picsha-ingress.s3.amazonaws.com/org_abc123/user_pete/550e8400-e29b-41d4-a716-446655440000/my_file.jpg?AWSAccessKeyId=...",
  "headers": {
    "Content-Type": "image/jpeg"
  }
}

B2. Register a Direct Upload

POST /assets with an s3Key

A presigned or resumable upload only puts the bytes in storage. Nothing is processed until you register it: this call creates the asset record (or, for POST /upload/sign, finishes the one already created) and queues it for the ingest pipeline. Until then the file is not searchable or deliverable.

Request Body:

{
  "s3Key": "org_abc123/user_pete/550e8400-e29b-41d4-a716-446655440000/my file.jpg",
  "originalName": "my file.jpg",
  "config": { "auto_tag": true },
  "tags": ["finance"],
  "metadata": { "project_id": "my_replit_app_123" }
}
  • s3Key: string (Required) - Where the upload was stored. See the table below.
  • originalName: string (Optional) - The filename to record. Defaults to the last segment of the key.
  • config, tags, metadata: Optional, exactly as in Ingest above.

Which key to send:

You uploaded withs3Key is
GET /assets/presignedThe path of the presigned url: drop the leading / and the query string, then URL-decode it. A file named my file.jpg appears in the URL as my%20file.jpg, and the key is …/my file.jpg.
POST /upload/resumable (TUS)tus-uploads/{id}, where {id} is the last path segment of the upload URL the TUS server returned in Location. For …/v1/upload/resumable/tus-uploads/9d8c7b6a-… the key is tus-uploads/9d8c7b6a-…. Send the filename as originalName.
POST /upload/signThe key field of the response, exactly as returned. The asset keeps the assetId you were given.

Sending the encoded path, or the bare TUS id without tus-uploads/, is the most common mistake: the upload succeeds and the registration fails. The React SDK and the MCP server derive the key for you.

Response: 201 Created with the same body as a direct upload (id, url, message). 200 OK with "message": "Asset already exists (Deduplicated)" when the bytes match a file you already have.

Errors:

  • 403 — {"message": "This s3Key is not one of your uploads. …"}. The key must be one the API handed to you: a finished resumable upload, or a key under your own prefix — {orgId}/… for an Organization, default-org/{userId}/… for a personal API key. You cannot register another tenant's file by naming its key, and the answer is the same whether or not that file exists.
  • 400 — {"message": "Upload payload missing in storage. …"}. Nothing is stored at that key: the upload did not finish, or the key is wrong.

C. List Assets

GET /assets

Lists metadata and retrieval URLs for assets belonging to the authenticated Organization or User.

Query Parameters:

  • page: number (Default: 1) - Pagination page index.
  • limit: number (Default: 20) - Number of assets to return per page (max 100).
  • type: string (Optional) - Filter by mime-type prefix (e.g., image, video, application).
  • tag: string (Optional) - Filter by exact metadata tag.
  • search: string (Optional) - Simple keyword search on the original filename.
  • sort: string (Optional) - Sorting order. Available options: added-desc, added-asc, created-desc, created-asc, name-asc, name-desc.
  • albumId: string (Optional) - Filter by exact album group identifier.
  • metadataField + metadataValue: string (Optional) - Filter by an exact metadata key/value pair, e.g. metadataField=apiKeyName&metadataValue=prints app to list assets ingested by a specific API key (see Ingest attribution).
  • metadata: string (Optional) - JSON object of exact matches on metadata keys, e.g. metadata={"asset_status":"Retired"} or metadata={"asset_visibility":["@all","@sales"]}. All keys must match; a list of values matches any of them, including any element of a stored array. Dotted keys descend the JSON.
  • anyOf: string (Optional) - JSON array of up to 10 {"metadata": {...}} blocks. An asset matches when it satisfies at least one block, combined with the other parameters. Malformed JSON returns 400.

Response:

{
  "cost": "$0.0005",
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "status": "completed",
      "mimeType": "image/jpeg",
      "originalName": "team-pic.jpg",
      "url": "https://cdn.picsha.ai/...",
      "thumbnailUrl": "https://cdn.picsha.ai/render/550e8400-e29b-41d4-a716-446655440000?w=600&fmt=webp",
      "tags": ["team", "cambridge"],
      "metadata": {
        "project_id": "my_app_123"
      }
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 1
  }
}

Asset Status Lifecycle

Because ingestion is heavily asynchronous, assets transition through several system states during their lifecycle. The status property will always reflect one of the following canonical enum values:

  • pending_upload: The asset was created by POST /upload/sign and is waiting for its file to be uploaded and registered (section B2). It is not processed, searchable or deliverable in this state.
  • queued: The asset has been saved and is waiting for an available Queue Worker node to begin processing.
  • active: The worker node has started the ingestion pipeline (derivative generation, metadata extraction, and AI processing).
  • completed: All AI analysis, transcription, multimodal embeddings, and primary web proxies have been generated successfully. The asset is fully indexed and ready for dynamic CDN delivery.
  • failed: An unrecoverable error occurred during the ingestion pipeline.
  • infected: The ClamAV sidecar detected malware or a virus. The file is quarantined and blocked from CDN delivery.
  • pending_moderation: (If enabled) AWS Rekognition Content Moderation flagged the asset for unsafe content. It is isolated pending manual review.
  • rejected: The asset was manually rejected during moderation review.

D. Get Asset Details

GET /assets/{id}

Retrieves full details and signed URLs for a single asset.

Response:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "cost": "$0.0002",
  "status": "completed",
  "mimeType": "image/jpeg",
  "originalName": "team-pic.jpg",
  "url": "https://cdn.picsha.ai/...",
  "thumbnailUrl": "https://cdn.picsha.ai/render/550e8400-e29b-41d4-a716-446655440000?w=600&fmt=webp",
  "tags": ["team", "cambridge"],
  "metadata": {
    "project_id": "my_app_123"
  }
}

E. Update Asset Metadata

PATCH /assets/{id}

Partially updates an asset's title, tags, or custom metadata object.

Request Body:

{
  "meta": {
    "title": "New Title"
  },
  "tags": ["add_this_tag", "-remove_this_tag"],
  "metadata": {
    "project_id": "updated_project_id"
  },
  "expires_at": "2026-12-31T23:59:59Z",
  "requiresSignature": true
}

Note: Prefixing a tag with a - sign (e.g. "-remove_this_tag") will remove it from the asset's tags list, while standard strings will append it. Pass null to expires_at to permanently remove an expiration date.

Response:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "message": "Asset updated successfully",
  "cost": "$0.0000"
}

F. Delete, Trash & Restore

DELETE /assets/{id}

By default, this moves the asset into a 30-day Trash state (soft delete), preventing accidental or malicious permanent data loss. Trashed assets immediately stop appearing in list and search results, their delivery URLs return 404 (cached CDN copies are evicted within seconds), and GET /assets/{id} continues to return them with status: "trashed" so you can build a recycle-bin UI. After 30 days, a nightly cleanup permanently purges them.

Parameters:

  • force (boolean): Pass ?force=true to skip the Trash and permanently, irreversibly destroy the asset (database record, search index entry, and stored files).

Response:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "message": "Asset deleted successfully",
  "trashed": true,
  "purgeAt": "2026-08-08T15:46:11.385Z",
  "cost": "$0.0005"
}

(With force=true, trashed is false and purgeAt is null.)

POST /assets/{id}/restore

Restores a trashed asset to its pre-trash state. Delivery, listing, and search resume immediately — no re-processing or re-analysis occurs. Returns 404 if the asset is not in the Trash.

Response:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "completed",
  "message": "Asset restored successfully",
  "cost": "$0.0005"
}

POST /assets/bulk-delete and POST /assets/bulk-restore

Bulk equivalents accepting { "ids": ["...", "..."] }. bulk-delete also accepts "force": true for permanent deletion. Unknown or unauthorized IDs are silently skipped, never deleted. Responses report deletedCount / restoredCount.

Listing the Trash

GET /assets?status=trashed returns only trashed assets (the recycle-bin view). All other list and search calls exclude trashed assets automatically.

G. AI Summarization

POST /assets/{id}/summarize

Returns the AI summary produced for an asset. Summaries are generated by the ingest pipeline (Anthropic Claude via Bedrock) and stored on the asset, so this call serves the existing summary and is not billed again.

Response (summary available): 200 OK

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "summary": "A detailed shot of a collaborative workspace in Cambridge...",
  "message": "Summary retrieved successfully",
  "cost": "$0.0000"
}

Response (no summary yet): 202 Accepted — analysis is re-queued for you.

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "message": "No summary available yet — analysis has been queued. Poll GET /v1/assets/{id} for ai.summaryLong.",
  "cost": "$0.0000"
}

Assets belonging to another organization return 404. To force a fresh pass over an asset that already has a summary, use POST /assets/{id}/analyze (below).

G2. Re-run AI Analysis

POST /assets/{id}/analyze

Re-runs the full ingest pipeline on an existing asset — face and object detection, summaries, quality metrics, and embeddings. Useful after enabling a capability you originally opted out of, or when an upstream analysis failed.

Faces are re-detected with new faceId values, and face names on this asset are reset — see Re-analysis Resets Faces.

Request Body (optional):

{
  "config": {
    "auto_tag": true,
    "auto_summarize": true,
    "vectorize": true,
    "location_lookup": true,
    "quality_metrics": true
  }
}

Response: 202 Accepted

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "message": "Re-analysis queued",
  "cost": "$0.0015"
}

H. Resumable Uploads (TUS Protocol)

POST /upload/resumable

  • Standard TUS 1.0 Protocol endpoint.
  • Supports large files (>100MB) and unstable connections.
  • Implementation: Wraps internal tus-node-server.
  • SDK Support: Compatible with uppy and picsha-uploader SDK.
  • Authentication: Send Authorization: Bearer … on every TUS request.
  • Filename: The upload's TUS metadata must include name; creation is rejected with 400 without it.
  • Finishing: A completed upload is held under tus-uploads/{id} until you register it — see B2. Register a Direct Upload above.

I. Generate New Images (Text-to-Image)

POST /assets/generate

Generates a brand-new image from a natural language prompt via MIMI and ingests it as a normal asset (analysis, DAM, renders, and further MIMI edits all work on it).

{
  "prompt": "a foggy pine forest at dawn",
  "aspect_ratio": "16:9",
  "quality": "standard"
}
  • prompt (string, required): Natural language description of the image to generate.
  • aspect_ratio (string, optional): One of 1:1, 3:4, 4:3, 9:16, 16:9, 2:3, 3:2, 4:5, 5:4. Default 1:1.
  • quality (string, optional): lite | standard | 4k. Default standard.

Response: 201 Created

{
  "assetId": "550e8400-e29b-41d4-a716-446655440000",
  "url": "https://cdn.picsha.ai/render/550e8400-e29b-41d4-a716-446655440000"
}

Billing: the same event/price as the corresponding MIMI mode (Lite 6.0¢, Standard 16.0¢, 4K 32.0¢).

J. Batch AI Analysis

POST /assets/analysis

Fetches the complete ingest analysis for up to 200 assets in a single call — EXIF capture time, camera, location, labels with confidence, face bounding boxes and attributes, AI captions/summaries, quality metrics, and (optionally) the 1024-dimensional multimodal embeddings. Built for AI agents and layout engines that need to reason over a whole photo set at once instead of issuing hundreds of GET /assets/{id} calls.

{
  "ids": ["550e8400-...", "661f9511-...", "..."],
  "includeEmbeddings": false
}
  • ids (string[], required): 1–200 asset IDs.
  • includeEmbeddings (bool, optional, default false): include each asset's multimodal vector. Off by default — embeddings add ~8KB per asset to the response.

Response: 200 OK

{
  "assets": [
    {
      "id": "550e8400-...",
      "originalName": "IMG_2041.jpg",
      "mimeType": "image/jpeg",
      "status": "completed",
      "width": 4032,
      "height": 3024,
      "captureDate": "2026-06-14T18:42:11.000Z",
      "cameraMake": "Apple",
      "cameraModel": "iPhone 15 Pro",
      "locationString": "Carmel-by-the-Sea, CA",
      "exif": { "...": "..." },
      "ai": {
        "labels": [{ "name": "Beach", "confidence": 99.2 }],
        "faces": [{ "boundingBox": { "...": "..." } }],
        "quality": { "sharpness": 82, "brightness": 61, "contrast": 55 },
        "faceDetails": [{ "eyesOpen": true, "smile": true, "poseYaw": 4.1 }]
      },
      "cutout": null,
      "embedding": null
    }
  ],
  "requested": 3,
  "returned": 3
}
  • Assets the caller is not authorized to read — and trashed assets — are silently omitted; compare returned against requested to detect gaps.
  • The ai object is the asset's full analysis record as produced by the ingest pipeline (see the Ingestion Pipeline doc); fields appear only when the corresponding pipeline stage ran.

4. Delivery & Transformation

GET /assets/{id}/render GET /seo/{id}/{seoUrlString}

Dynamic, edge-cached image transformations. Note that the CDN (cdn.picsha.ai/render/:assetId) sits directly in front of the API (api.picsha.ai/v1/assets/:assetId/render). Both URL path shapes are valid and route to the exact same transformation engine. The /seo alias route functions exactly the same as /render but supports semantic URL crawlers.

Forced Downloads You can force the browser to securely download an asset rather than displaying it by appending ?download=true (or its alias ?export=true). The file is served with a Content-Disposition: attachment header using a secure filename derived from the original asset's name and format.

Authorization for Generative Operations (always on)

Plain transformations (resizing, cropping, format conversion, watermarking, overlays) are publicly deliverable by default — anonymous <img src> embedding keeps working. Generative operations are not: any delivery URL carrying a billed generative parameter (mimi, mimi_bg, gen_rem, gen_fill, gen_mask, or bg_rem=true) must prove it is authorized, or it is rejected with 401 before any generation runs. There are two ways to authorize:

  1. Authenticated API call (server-side): include your Authorization: Bearer header as with any other endpoint. Nothing else is required — the engine validates that the asset belongs to your organization and automatically stamps a delivery signature onto its CDN redirect, so standard server-side render calls work unchanged. Authenticated generative calls are tenant-scoped: you can only run paid generations against your own organization's assets.
  2. Signed URL (public embedding): for generative URLs you hand to browsers or embed in pages, mint a ?sig= signature with POST /assets/{id}/sign-delivery (below). Sign the exact path form you will serve — API-form (/v1/assets/{id}/render, /v1/seo/...) or CDN-form (/render/{id}, /seo/{id}/{seoUrlString}) — and the exact query permutation.

This protection is enforced at both the API and the CDN edge and cannot be disabled. It exists so that nobody who discovers an asset ID can burn paid generations against your account.

Signed Delivery URLs & Strict Transformations (Security)

Beyond the always-on generative protection above, Picsha AI supports Strict Transformations for full lockdown. When Strict Transformations are enabled globally in your Organization settings (or individually per-asset via requiresSignature: true), you must append a cryptographically valid ?sig= parameter to the delivery URL for any query permutation — including plain transforms. This ensures that malicious users cannot artificially inflate your usage by enumerating parameters (e.g., ?w=1, ?w=2).

You can generate a signed URL using the helper endpoint below, or compute the signature on your own server with your API key and no API call at all. The scheme is documented and versioned in Signed Delivery URLs. POST /assets/{id}/sign-delivery

{
  "urlPath": "/v1/seo/123e4567-e89b-12d3-a456-426614174000/image.webp",
  "queryParams": {
    "w": 600,
    "fit": "cover"
  }
}

Response:

{
  "signedUrl": "/v1/seo/123e4567-e89b-12d3-a456-426614174000/image.webp?fit=cover&w=600&sig=39950449a74b1306",
  "signature": "39950449a74b1306"
}

Note: the signature covers the exact query permutation — include download=true in queryParams when signing a forced-download URL.

For comprehensive details on standard parameters (dimensions, cropping, formats), smart AI cropping, watermarking, Background Operations (background removal/replacement), and the signature MIMI natural language generative editing capability (mimi, mimi_mode, upscale, mimi_bg, async), see the Transformations & Generative AI guide.


5. Search (Vector & Semantic)

Picsha provides hybrid searching combining Amazon Titan multimodal vectors with structural metadata filters.

A. Search via POST

POST /search

Query using a JSON body. Ideal for programmatic queries and complex filtering.

Request Body:

  • query: string (Default: "") - Conversational NLP search query.
  • mode: "standard" | "ai" (Default: "standard") - "standard" performs pure text/keyword search. "ai" performs vector-driven semantic search.
  • threshold: number (Default: 0.6) - Relevance confidence threshold (only for AI mode).
  • sort: string (Optional) - Sorting order: relevance, added-desc, added-asc, created-desc, created-asc, name-asc, name-desc.
  • limit: number (Default: 40) - Number of assets to return per page (max 100). Pass 0 to receive only pagination.total and facets.
  • facets: array (Optional) - Up to 10 { field, size?, order? } entries requesting distinct-value counts over the result scope. field is one of faces, labels, places, tags, mimeType, cameraMake, cameraModel; size 1–1000 (default 50); order count (default) or key. See Facets.
  • filters / advancedFilters: object (Optional) - Precise filter constraints:
    • addedAfter / addedBefore: string (ISO date format) - Ingestion timestamp range.
    • capturedAfter / capturedBefore: string (ISO date format) - EXIF photo capture date range.
    • face: string - Filter by specific recognized human face name.
    • object: string - Filter by specific auto-detected visual label/object.
    • place: string - Filter by reverse-geocoded location label.
    • ocr: string - Filter by text extracted via OCR.
    • type: string - Filter by mime-type prefix (e.g., image, video).
    • style: string - Filter by custom style categorization tag.
    • metadataField / metadataValue: string - Filter by specific custom/legacy metadata field and matching value.
    • minRating: integer (1–5) - Only assets whose metadata.rating is at least this many stars.
    • metadata: object - Exact matches on custom metadata keys ({"asset_status": "Retired"}); all keys must match. A key may list values ({"asset_visibility": ["@all", "@sales"]}), any of which matches (max 200 per key).
    • anyOf: array - Up to 10 filter blocks with the same fields as this object (minus anyOf). An asset matches when it satisfies at least one block; combined with the other filters. See Search.

Request JSON:

{
  "query": "photos of dogs on the beach",
  "mode": "ai",
  "threshold": 0.65,
  "advancedFilters": {
    "type": "image",
    "capturedAfter": "2026-01-01",
    "metadataField": "pictures_meta_subject.value",
    "metadataValue": "#special events"
  },
  "facets": [
    { "field": "faces", "size": 100, "order": "key" }
  ]
}

Response:

{
  "results": [
    {
      "id": "123e4567-e89b-12d3-a456-426614174000",
      "score": 0.92,
      "url": "https://cdn.picsha.ai/123e4567-e89b-12d3-a456-426614174000/source.jpg"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 40,
    "total": 1
  },
  "facets": {
    "faces": [
      { "value": "Sally Jones", "count": 1 }
    ]
  },
  "cost": "$0.0025"
}

facets is included only when the request asked for facets. Facets always reflect the same tenant and filter scope as the hits.

B. Pagination

page and limit are supplied as query parameters even on the POST request, while the query and filters travel in the body:

POST https://api.picsha.ai/v1/search?page=2&limit=40

[!NOTE] Search is POST-only. There is no GET /search — send the query in a JSON body and keep pagination in the query string.

C. Similar Assets

GET /assets/:id/similar · POST /assets/:id/similar

Returns the assets that look most like the given one: the nearest neighbours of its embedding, most similar first. No query text is involved, and no model is called at request time; the comparison uses the embedding stored when the asset was ingested.

Query Parameters (GET) / Body Fields (POST):

  • limit: integer (1–50, Default: 12) - Number of matches to return.
  • minScore: number (0–1, Optional) - Drops matches scoring below this. Omitted, the nearest limit are returned whatever they score.

Body Fields (POST only):

  • filters / advancedFilters: object (Optional) - The same filters POST /search accepts. They are applied inside the nearest-neighbour search, so limit matches come back from the filtered scope.
  • excludeAssetIds: string[] (Optional, max 10,000) - Asset ids never returned as matches.

Request JSON (POST):

{
  "limit": 24,
  "minScore": 0.6,
  "advancedFilters": {
    "type": "image",
    "metadata": { "asset_status": "Active" }
  },
  "excludeAssetIds": ["9b2e6c1a-7d43-4f0e-8a55-1c2d3e4f5a6b"]
}

Response:

{
  "assetId": "5e92fe62-40a4-42fe-aa99-0a2d84a91cb7",
  "results": [
    {
      "id": "123e4567-e89b-12d3-a456-426614174000",
      "matchScore": 0.91,
      "originalName": "harbor-at-dusk.jpg",
      "mimeType": "image/jpeg",
      "metadata": { "albumId": "2026-09-25_100229_bae8" }
    }
  ],
  "total": 1
}

Each result is the asset record plus matchScore. The asset itself and trashed assets are never returned. The scope is the same as search: an organization-scoped key compares across the organization, a personal key across its own uploads. An asset of another tenant answers 404.

An asset without an embedding (ingested with vectorize: false, or still processing) returns an empty list with "reason": "no-embedding", not an error. See Similar Assets.

[!NOTE] Similar assets are available from API version 1.0.94.


6. Billing & Usage

GET /usage

Returns your organization's metered usage events, aggregated by billing event type.

Query Parameters:

  • groupBy: "userId" (Optional) - Additionally break each event type down by end user (the x-external-user-id you sent at write time).

Headers:

  • x-org-id: string (Optional) - Defaults to the organization your API key belongs to. If sent, it must match that organization (dashboard sessions: an organization you are a member of) — a mismatch is rejected with 403.

Response:

{
  "usage": [
    { "type": "storage_upload", "userId": null, "totalQuantity": 10.5, "count": 42 },
    { "type": "mimi_job", "userId": null, "totalQuantity": 6, "count": 6 }
  ],
  "cost": "$0.0005"
}

totalQuantity is the summed metered quantity for that event type (GB for storage, job count for AI operations); count is the number of events. Per-event retail pricing is listed in your dashboard's billing section.


7. Digital Asset Management (DAM) Primitives

These endpoints support advanced DAM workflows like asset grouping, explicit 1-to-1 relationships (e.g., source RAW to web JPEG), and asset event/comment tracking.

  • Base Prefix: /v1/dam
  • Scoping: Every DAM endpoint requires authentication and is scoped to the organization of your API key (a personal key is always scoped to its owner). With an Organization key, x-external-user-id is recorded as the user on the events you create; it does not narrow which of the Organization's assets you can reference. Organization and user are taken from the credential — never from the request body — and any asset you reference must belong to you, or the call returns 404.

A. Asset Events & Comments

Track comments, edits, status updates, or custom activities for specific assets.

POST /events

Create an asset event or user comment.

Request Body:

{
  "assetId": "550e8400-e29b-41d4-a716-446655440000",
  "eventType": "COMMENT_ADDED",
  "eventData": {
    "username": "Pete",
    "comment": "Nice composition in this image!"
  }
}

GET /:assetId/events

Fetch all events and comments for a specific asset.

Response (Success):

[
  {
    "id": "9f2a1c60-4d7b-4a12-8f3e-77c1b2d5e604",
    "assetId": "550e8400-e29b-41d4-a716-446655440000",
    "orgId": "org_abc123",
    "userId": "usr_dev_456",
    "eventType": "COMMENT_ADDED",
    "eventData": {
      "username": "Pete",
      "comment": "Nice composition in this image!"
    },
    "createdAt": "2026-05-22T23:00:00Z"
  }
]

(This endpoint returns a bare array, so it carries no cost attribute.)

B. Asset Relationships

Explicitly relate assets (e.g., versions, derivations, replacements).

POST /relationships

Create an explicit relationship between two assets.

Request Body:

{
  "sourceAssetId": "222e8400-e29b-41d4-a716-446655440000",
  "targetAssetId": "333e8400-e29b-41d4-a716-446655440000",
  "relationshipType": "REPLACES"
}

C. Asset Groups & Collections

Logically bucket assets into folders or campaigns.

POST /groups

Create a new group.

Request Body:

{
  "name": "Q3 Marketing Campaign",
  "metadata": {
    "project_id": "proj_123"
  }
}

GET /groups/:id

Retrieve a specific group by ID.


8. Webhooks

Manage your Enriched Agentic Webhooks. Webhooks allow your system to receive rich, cognitive payloads asynchronously when assets finish processing. For full details on the payload structure and security signatures, please see the Enriched Agentic Webhooks guide.

  • Scoping: A subscription belongs to an organization and a user id — the x-external-user-id you register with, system when an Organization key sends none, or your own account for a personal key. These endpoints only see subscriptions under the same pair. An Organization's subscription receives the events of the end user it names, or of the whole Organization when it names none; a personal key's subscription receives the events of its own assets. See who a subscription belongs to.

A. List Webhooks

GET /webhooks

Retrieve all registered webhooks for the authenticated user/organization.

Response:

[
  {
    "id": 42,
    "url": "https://api.yourdomain.com/receiver",
    "events": ["asset.processed"],
    "config": {
      "include_embeddings": true,
      "include_text": true
    },
    "createdAt": "2026-06-02T02:58:00.000Z"
  }
]

B. Register Webhook

POST /webhooks

Register a new webhook subscription.

Request Body:

{
  "url": "https://api.yourdomain.com/receiver",
  "events": ["asset.processed"],
  "config": {
    "include_embeddings": true,
    "include_text": true
  }
}

C. Update Webhook

PATCH /webhooks/{id}

Update an existing webhook's endpoint URL, event subscriptions, or payload configuration.

Request Body (Partial updates allowed):

{
  "events": ["asset.processed", "asset.failed"]
}

D. Delete Webhook

DELETE /webhooks/{id}

Permanently remove a webhook subscription.

Response:

{
  "message": "Webhook deleted successfully"
}

9. Named Transformations

Named transformations allow you to save a complex set of rendering parameters (like smart cropping, watermarking, and format conversion) into a single alias. You can then apply all of these parameters to an asset delivery URL by simply appending ?t=your_alias_name.

A. List Transformations

GET /transformations

Retrieve all named transformations for your organization.

Response:

[
  {
    "id": "tf_1a2b3c",
    "name": "Social Media Auto Crop",
    "alias": "social_square",
    "commands": {
      "w": 1080,
      "h": 1080,
      "fit": "cover",
      "pos": "attention",
      "fmt": "webp"
    },
    "createdAt": "2026-06-01T12:00:00.000Z"
  }
]

B. Create Transformation

POST /transformations

Save a new rendering parameter configuration under a short alias.

Request Body:

{
  "name": "Watermarked Thumbnail",
  "alias": "thumb_watermark",
  "commands": {
    "w": 400,
    "wm": "444e8400-e29b-41d4-a716-446655440000",
    "wm_pos": "southeast"
  }
}

C. Get Transformation

GET /transformations/{id}

Retrieve a specific transformation configuration by ID.

D. Update Transformation

PUT /transformations/{id}

Overwrite the commands or details for an existing transformation.

Request Body:

{
  "name": "Updated Name",
  "alias": "thumb_watermark",
  "commands": {
    "w": 500,
    "wm": "444e8400-e29b-41d4-a716-446655440000"
  }
}

E. Delete Transformation

DELETE /transformations/{id}

Permanently delete a transformation.


9b. Backgrounds Library

Backgrounds are org-scoped scene images used for cutout compositing: pair them with the render engine's bg_asset parameter to layer any asset's background-removed cutout over a stored scene — a pure-CPU operation served from the CDN edge once cached. A background references an existing asset, so it inherits the full ingest pipeline (moderation, derivatives) automatically.

A. List Backgrounds

GET /backgrounds

B. Register Background

POST /backgrounds

Registers an already-uploaded image asset as a reusable compositing background.

Request Body:

{
  "name": "Rainy London Street",
  "assetId": "550e8400-e29b-41d4-a716-446655440000",
  "metadata": { "weather": "rain", "region": "uk" }
}

Usage with the render engine:

GET /v1/assets/{productAssetId}/render?w=800&h=800&bg_asset={backgroundId}

The product's stored cutout is composited over the scene. If no cutout exists yet, one is generated and persisted on first request (billed once per unique image), and every subsequent render — any size, any background — is a fast, cached CPU composite.

C. Get Background

GET /backgrounds/{id}

D. Update Background

PATCH /backgrounds/{id}

Update the name or metadata of a background.

E. Delete Background

DELETE /backgrounds/{id}


9c. Moderation Review Queue

When content_moderation is enabled at ingest, flagged assets land in pending_moderation and are blocked from delivery until a human resolves them.

A. List the Queue

GET /assets/moderation/pending

Lists your organization's assets awaiting manual review, newest first.

Query Parameters:

  • limit: number (Default: 20, max 100)
  • offset: number (Default: 0)

Response:

{ "assets": [ { "id": "550e8400-...", "status": "pending_moderation", "originalName": "upload.jpg" } ], "cost": "$0.0000" }

B. Approve or Reject

POST /assets/{id}/moderation

Request Body:

{ "action": "approve" }

approve returns the asset to completed and restores delivery; reject sets rejected, which keeps it permanently blocked. Assets not in pending_moderation return 400.


9d. Downloads, Signing & Notifications

A. Download the Original

GET /assets/{id}/download

Streams the original, un-transformed bytes with a Content-Disposition: attachment header. Authenticated and tenant-scoped — for public download links, mint a signed delivery URL with ?download=true instead.

Query Parameters:

  • inline: "true" | "false" (Default: "false") - true streams for inline browser display rather than forcing a download.

B. Sign a Delivery URL

POST /assets/{id}/sign-delivery — mints the HMAC ?sig= for a transformation URL; see Delivery & Transformation above.

C. Time-Limited Link to the Original File

POST /sign

Returns a presigned S3 URL to the asset's original stored file — the raw upload, not a render. Use it for expiring download links to source files (a RAW, a master video, a signed contract PDF).

Request Body:

{ "asset_id": "550e8400-e29b-41d4-a716-446655440000", "expires": 3600 }
  • expires: number (Default: 3600) - Link lifetime in seconds.

Response:

{ "url": "https://picsha-assets.s3.amazonaws.com/...&X-Amz-Expires=3600", "expiresAt": "2026-08-15T18:30:00.000Z", "cost": "$0.0005" }

D. Signed Upload URL

POST /upload/sign

Server-side counterpart to GET /assets/presigned, taking a JSON body. Unlike GET /assets/presigned, it creates the asset immediately, in pending_upload status, and returns its id, so you can store the id before the file has been uploaded.

Request Body:

{ "contentType": "image/jpeg", "filename": "sunset.jpg" }

Response:

{ "uploadUrl": "https://...", "assetId": "550e8400-...", "key": "org_abc/user_pete/550e8400-.../sunset.jpg", "method": "PUT" }

Completing the upload takes two more calls:

  1. PUT the file bytes to uploadUrl, with a Content-Type header equal to the contentType you signed. The URL is valid for one hour.
  2. Register the upload (section B2): POST /assets with { "s3Key": "<key>" }, plus any config, tags or metadata. It answers 201 with the same assetId and queues the asset for processing.

Until step 2 the asset stays in pending_upload with a size of 0. Registering before the file has arrived answers 400. Register each upload once.

E. Live Notification Stream (SSE)

GET /notifications/stream

A Server-Sent Events stream of the same events that drive webhooks (asset.processed, face.propagation.*), scoped to the authenticated user — or to x-external-user-id when you authenticate with an Organization API key. A personal API key always receives its owner's events; the header is ignored for it. Useful for updating a UI as uploads finish, without standing up a webhook receiver.

Because EventSource cannot set headers, this endpoint also accepts the credential as a ?token= query parameter.

F. Escalate to Support

POST /support/escalate

Sends a message to the Picsha engineering team — the REST endpoint behind the escalate_to_support MCP and A2A tools.

Request Body:

{ "subject": "Docs gap: signed URL expiry", "headline": "exp parameter unclear", "message": "..." }

G. Account Statistics

GET /account/stats

Returns storage totals, per-feature AI usage, and retail cost breakdowns for the organization — the data behind the dashboard's usage panel.


10. Asynchronous Work & Completion Signals

Ingestion, transcoding, and generative rendering all run outside the request that triggers them. There are two completion channels, and which one you use depends on the workload:

WorkloadHow you learn it finished
Ingest, AI analysis, embeddings, HLS transcode (adaptive_stream)The asset.processed webhook, or poll GET /assets/{id} until status leaves queued/active
MIMI / generative renders sent with &async=trueGET https://cdn.picsha.ai/render/status/{jobId} — see the Transformations & MIMI guide

[!NOTE] There is no general-purpose Core API job-polling endpoint. Use the webhook (or asset status) for pipeline work, and the CDN status endpoint for generative renders.


11. Rekognition Face Indexing

Picsha natively integrates with AWS Rekognition to provide automatic biometric face indexing.

Faces are indexed automatically at ingest: when auto_tag is enabled, every detected face is added to your organization's Rekognition Collection and stored with a Rekognition-issued faceId. You then put a name to one of those detected faces, and Picsha propagates that name across every other matching face in your library.

A. Name a Detected Face

PUT /v1/rekognition/faces/{faceId}

Assigns a person's name to a face that ingest already detected, then propagates it to matching faces across the organization (asynchronously — subscribe to the face.propagation.* webhooks to track it). Propagation names every face at 98% similarity or higher, and only faces on assets of your own organization — or, with a personal API key, on your own assets.

Path Parameters:

  • faceId: The Rekognition-issued face identifier from the asset's analysis record. Read it from GET /assets/{id} (ai.faces[].faceId) or POST /assets/analysis.

[!IMPORTANT] faceId must be an identifier Picsha already indexed on an asset of your organization. You cannot invent your own key here — an unknown id, or a face that belongs to another organization, returns 404 with {"message": "Face not found"} and changes nothing. Find the face first, then name it.

Request Body:

{
  "name": "John Doe"
}

Response:

{
  "id": "b2c4d6e8-...",
  "faceId": "0f4a1c62-8b3d-4e21-9f77-6a1e2d3c4b5a",
  "name": "John Doe",
  "confidence": "99.4",
  "cost": "$0.0120"
}

Errors:

  • 404 — the face does not exist in your organization (see above). Earlier API versions returned 500 here.
  • 403 — an x-org-id header that does not match the credential's organization.

Once named, the name is searchable as a hard entity filter — {"advancedFilters": {"face": "John Doe"}} (see Search).

Sending a new name to an already-named face replaces it everywhere the face appears.

B. Clear a Face Name

DELETE /v1/rekognition/faces/{faceId}/name

Removes the name from a face — for example to undo a wrong tag. No request body.

Clearing is deliberately not propagated: only this faceId loses its name. Similar faces that received the same name (by propagation or by hand) keep theirs, so correcting one mistagged photo never wipes a person's tags across the library. To rename someone everywhere, PUT the new name instead.

Response: 200 OK

{
  "id": "b2c4d6e8-...",
  "faceId": "0f4a1c62-8b3d-4e21-9f77-6a1e2d3c4b5a",
  "name": null
}

The name is removed from the asset's analysis record (ai.faces[]) and from the search index, so the asset stops matching {"advancedFilters": {"face": "John Doe"}} unless another face on it still carries that name. Clearing a face that has no name is a no-op that still returns 200.

Errors: 404 with {"message": "Face not found"} when the face does not exist in your organization. No face.propagation.* webhooks are emitted.

C. Organization Scoping

Face naming is tenant-isolated. A faceId can be named or cleared only by a credential of the organization that owns the asset the face was detected on; for everyone else the face does not exist (404). Propagation never crosses organizations either, even when the same person appears in several libraries.

Personal accounts: with a personal API key — one that does not belong to an Organization — the boundary is your account. You can name or clear only faces detected on your own assets, and a name propagates only to your own assets. A face on another personal account's asset returns 404, exactly like a face in another organization.

D. Duplicate Uploads Share Faces

Re-uploading byte-identical content creates a new asset record that reuses the original's stored file and analysis (deduplication) — including the same faceId values. Naming or clearing a face therefore applies to every asset in your organization that shows that faceId (for a personal account, every asset of yours): the original and all of its duplicates are updated and re-indexed together, and each one becomes searchable by the name. Deleting one copy leaves the face, and its name, intact on the remaining copies.

E. Re-analysis Resets Faces

POST /assets/{id}/analyze re-detects faces and issues new faceId values for that asset; the previous face records are removed from your Rekognition Collection first. Names on the re-analyzed asset are not carried over — they return the next time a matching face is named (propagation), or name the new faceId directly.