Skip to main content
The Pindeck Convex backend exposes a set of HTTP action endpoints for use by external services such as the Discord bot. All endpoints are mounted on your Convex site URL. Base URL
https://<deployment-name>.convex.site
Replace <deployment-name> with your actual Convex deployment name (e.g. tremendous-jaguar-953).

Authentication

All endpoints (except /smartAnalyzeImage) require a Bearer token that matches the INGEST_API_KEY environment variable set in your Convex Project Settings.
Authorization: Bearer <INGEST_API_KEY>
Requests with a missing or incorrect token receive a 401 Unauthorized response.

POST /ingestExternal

Ingest an external image URL into Pindeck for a specific user. The image is downloaded, processed, and persisted to NextCloud. A Convex image record is then created for the target user. For Discord imports (sourceType: "discord"), the image lands with status: pending and waits for moderation before any AI analysis runs. All other source types trigger AI analysis automatically.
If a record with a matching externalId or the same imageUrl already exists for the user, the endpoint returns the existing image ID without creating a duplicate.

Request body

userId
string
required
Convex user ID of the target user. The imported image is assigned to this account.
imageUrl
string
required
URL of the image to import. Angle-bracket wrapping (e.g. <https://...>) is stripped automatically.
title
string
default:"Discord Import"
Human-readable title for the image.
description
string
Optional description text.
tags
string[]
Array of tag strings. "original" is always appended automatically.
category
string
default:"General"
Image category. See the gallery for available category values.
source
string
Human-readable source label (e.g. a creator name or platform).
sref
string
Style reference number extracted from the source message.
externalId
string
Deduplication key. If an image with this ID already exists it is returned immediately without re-importing.
sourceType
string
Origin of the image. One of "upload", "discord", "pinterest", or "ai". Discord imports are held as pending until moderated.
sourceUrl
string
Original source URL before any normalization. Stored for reference and used as fallback during backfill.
importBatchId
string
Convex ID of an importBatches record to group this image with a batch import.

Response

imageId
string
required
Convex ID of the created (or existing) image record.
userId
string
required
Convex user ID the image was assigned to. Echoes back the userId from the request.

Error responses

StatusMeaning
400Missing imageUrl or userId, or both were empty after normalization
401Bearer token missing or does not match INGEST_API_KEY
502NextCloud persist failed — image could not be downloaded or stored

Example

curl -X POST https://<deployment-name>.convex.site/ingestExternal \
  -H "Authorization: Bearer $INGEST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "jd7abc123def456",
    "imageUrl": "https://cdn.discordapp.com/attachments/123/456/photo.jpg",
    "title": "Rooftop golden hour",
    "description": "Cinematic wide shot at dusk",
    "tags": ["cinematic", "golden-hour"],
    "category": "Film",
    "sref": "98765",
    "externalId": "discord-msg-789",
    "sourceType": "discord",
    "sourceUrl": "https://discord.com/channels/111/222/333"
  }'
{
  "imageId": "kg2abc123def4567",
  "userId": "jd7abc123def456"
}

POST /discordQueue

Fetch the list of images that are pending moderation for a specific user. Used by the Discord bot to present items for review. Only images with status: pending that belong to a Discord lineage are returned. Each item includes resolved lineage root metadata so the bot can display the original source context.

Request body

userId
string
required
Convex user ID whose pending queue to fetch.
limit
number
default:"5"
Maximum number of items to return. Clamped to a minimum of 1 and a maximum of 25.
imageId
string
When provided, the response is filtered to only include the item matching this Convex image ID. Useful for checking the status of a specific image.

Response

items
object[]
required
Array of pending image records.
userId
string
required
Echoes back the userId from the request.

Error responses

StatusMeaning
400Missing or empty userId
401Bearer token missing or does not match INGEST_API_KEY

Example

curl -X POST https://<deployment-name>.convex.site/discordQueue \
  -H "Authorization: Bearer $INGEST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "jd7abc123def456",
    "limit": 5
  }'
{
  "items": [
    {
      "_id": "kg2abc123def4567",
      "title": "Rooftop golden hour",
      "imageUrl": "https://cloud.example.com/public.php/dav/files/<share-token>/pindeck/media-uploads/2025/01_15/original/photo.jpg",
      "status": "pending",
      "aiStatus": "queued",
      "sourceType": "discord",
      "sref": "98765",
      "lineageRootImageId": "kg2abc123def4567",
      "lineageRootTitle": "Rooftop golden hour"
    }
  ],
  "userId": "jd7abc123def456"
}

POST /discordModerate

Approve, reject, or trigger variation generation for a pending Discord-imported image.
  • Approve — moves the image to status: draft and schedules AI (VLM) analysis.
  • Reject — deletes the image record and cleans up any associated NextCloud storage.
  • Generate — schedules variation generation immediately. The image must already be approved (status: active or draft) before calling generate.
Rejection is permanent. The image record and all associated NextCloud files are deleted and cannot be recovered.

Request body

userId
string
required
Convex user ID that owns the image.
imageId
string
required
Convex image ID to act on.
action
string
required
One of "approve", "reject", or "generate".
variationCount
number
Number of variations to generate when action is "generate". Clamped to 1–12. Defaults to 2.
modificationMode
string
Variation style for "generate". One of "shot-variation", "action-shot", "b-roll", "coverage", "style-variation", or "subtle-variation". Defaults to "shot-variation".
variationDetail
string
Optional direction passed to the generation prompt (e.g. "medium close-up, wet street").
aspectRatio
string
Output aspect ratio for generated variations. One of "16:9", "9:16", "1:1", "4:3", or "3:4". Defaults to "16:9".

Response

ok
boolean
required
true when the action was applied successfully.
message
string
required
Human-readable description of the outcome (e.g. "Image approved.", "Image rejected and deleted.").
imageId
string
required
Echoes back the imageId from the request.
userId
string
required
Echoes back the userId from the request.
status
string
Updated status field of the image record. Not present after a rejection.
aiStatus
string
Updated aiStatus field of the image record. Not present after a rejection.

Error responses

StatusMeaning
400Missing userId or imageId; invalid action value; image is in the wrong state (e.g. calling generate on a still-pending image)
401Bearer token missing or does not match INGEST_API_KEY

Examples

Approve an image
curl -X POST https://<deployment-name>.convex.site/discordModerate \
  -H "Authorization: Bearer $INGEST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "jd7abc123def456",
    "imageId": "kg2abc123def4567",
    "action": "approve"
  }'
{
  "ok": true,
  "message": "Image approved.",
  "imageId": "kg2abc123def4567",
  "userId": "jd7abc123def456",
  "status": "draft",
  "aiStatus": "processing"
}
Generate variations
curl -X POST https://<deployment-name>.convex.site/discordModerate \
  -H "Authorization: Bearer $INGEST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "jd7abc123def456",
    "imageId": "kg2abc123def4567",
    "action": "generate",
    "variationCount": 4,
    "modificationMode": "shot-variation",
    "aspectRatio": "16:9"
  }'
{
  "ok": true,
  "message": "Started generation (4 variations).",
  "imageId": "kg2abc123def4567",
  "userId": "jd7abc123def456",
  "status": "draft",
  "aiStatus": "processing"
}

POST /admin/backfillNextcloud

Scan for image records whose URLs are not yet canonical NextCloud URLs and migrate them. Images are either re-uploaded from a source URL or published from an existing NextCloud storage path. Use dryRun: true to inspect what would be migrated before making any changes.
Run with dryRun: true first to assess scope, then re-run without it to apply. Results in the response are capped at 50 entries regardless of how many images are scanned.

Request body

limit
number
default:"200"
Maximum number of images to scan and process. Clamped to 1–1000.
dryRun
boolean
default:"false"
When true, the endpoint reports what it would do without writing any changes.
refreshVariants
boolean
default:"false"
When true, images that already have a NextCloud storagePath but are missing or have stale derivative variants (preview, small, medium, large) are reprocessed to rebuild those derivatives.

Response

dryRun
boolean
required
Whether the request ran in dry-run mode.
refreshVariants
boolean
required
Whether variant refresh was enabled.
limit
number
required
The effective limit that was applied.
scanned
number
required
Total number of image records examined.
migrated
number
required
Number of images successfully migrated (or that would be migrated in dry-run mode).
failed
number
required
Number of images that could not be migrated.
skipped
number
required
Number of images that were already on canonical NextCloud URLs and required no action.
results
object[]
required
Per-image result entries (up to 50).

Error responses

StatusMeaning
401Bearer token missing or does not match INGEST_API_KEY

Example

curl -X POST https://<deployment-name>.convex.site/admin/backfillNextcloud \
  -H "Authorization: Bearer $INGEST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "limit": 50,
    "dryRun": true
  }'
{
  "dryRun": true,
  "refreshVariants": false,
  "limit": 50,
  "scanned": 45,
  "migrated": 40,
  "failed": 2,
  "skipped": 3,
  "results": [
    {
      "imageId": "kg2abc123def4567",
      "title": "Rooftop golden hour",
      "status": "re-upload-source",
      "imageUrl": "https://cdn.discordapp.com/attachments/123/456/photo.jpg",
      "sourceType": "discord"
    }
  ]
}

POST /smartAnalyzeImage

Schedule async VLM analysis for an already-uploaded image. The endpoint enqueues the analysis job and returns immediately — it does not wait for the analysis to complete. This endpoint is called internally by Pindeck after an image is ingested. It is exposed as an HTTP action for external orchestration (e.g. re-triggering analysis from a script or CI pipeline).
This endpoint does not require a Bearer token. It relies on the Convex session context passed in the request body.

Request body

imageId
string
required
Convex ID of the image record to analyze.
userId
string
required
Convex user ID that owns the image.
storageId
string
Convex storage ID of the uploaded file. Either storageId or imageUrl must be provided.
imageUrl
string
Direct URL of the image to analyze. Required when storageId is not provided.
title
string
Current title of the image, passed as context to the VLM.
description
string
Current description, passed as context to the VLM.
tags
string[]
Existing tags, passed as context to the VLM.
category
string
Current category, passed as context to the VLM.
source
string
Source label, passed as context to the VLM.
sref
string
Style reference number, stored on the record after analysis.
variationCount
number
If greater than 0, variation generation is scheduled after analysis completes.
modificationMode
string
Variation mode to use if variationCount is set.

Response

success
boolean
required
true when the analysis job was successfully enqueued.

Error responses

StatusMeaning
400Missing imageId, userId, or both storageId and imageUrl
500Unexpected server error

Example

curl -X POST https://<deployment-name>.convex.site/smartAnalyzeImage \
  -H "Content-Type: application/json" \
  -d '{
    "imageId": "kg2abc123def4567",
    "userId": "jd7abc123def456",
    "imageUrl": "https://cloud.example.com/public.php/dav/files/<share-token>/pindeck/media-uploads/2025/01_15/original/photo.jpg",
    "title": "Rooftop golden hour",
    "tags": ["cinematic", "golden-hour"],
    "category": "Film",
    "variationCount": 0
  }'
{
  "success": true
}

Legacy aliases

The following paths are legacy aliases retained for older Discord bot configurations. They map to the same handlers as their canonical equivalents and accept identical request/response payloads.
Legacy pathCanonical path
POST /discord/queuePOST /discordQueue
POST /discord/moderationPOST /discordModerate
Two additional malformed aliases (/discord/queue/discordQueue and /discord/moderation/discordModerate) are also supported for backward compatibility with a previous bot URL concatenation bug. Do not use these paths in new integrations.

Build docs developers (and LLMs) love