Skip to content

Documentation

Your first image search.

Call Pluko over HTTP from your backend: create a search job, follow its progress, and read image URLs and metadata. No SDK required.

Quick start

  1. 1Create a project and an API key

    In your workspace, create a project, then generate an API key with the search and read scopes. Copy it when it’s shown: the full key is displayed only once.

  2. 2Set your environment variables

    Keep the key on your server. Use your deployment’s address when hosted.

    .env
    PLUKO_URL=https://your-pluko-host
    PLUKO_API_KEY=YOUR_API_KEY
  3. 3Create your first search

    POST /v1/images/search
    curl -X POST "$PLUKO_URL/v1/images/search" \
      -H "Authorization: Bearer $PLUKO_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
      "query": "concrete architecture",
      "count": "15",
      "min_width": 1000
    }'

    Note

    A 202 response means the job was accepted, not that images are ready. Use the returned job_id with the status and results endpoints below.

Authentication

Send Authorization: Bearer YOUR_API_KEY on every public API request. Keep keys on your server, never in browser code, mobile apps, URLs or logs.

ScopeAllows
searchCreate, cancel and retry jobs.
readRead job state, results and usage.

Tip

Keys expire automatically and can be rotated or revoked from the API keys page of your workspace. Rotate before a key expires to avoid an outage.

Jobs and statuses

A search is a background job. Poll GET /v1/jobs/JOB_ID with backoff, or use a webhook. A job is always in one of these states:

StatusMeaning
queuedAccepted and waiting for the worker.
runningPages are being fetched; results appear as they are found.
completeEvery query found what it asked for.
partialFinished with fewer images than requested. Check each query’s reason.
cancelledStopped before it finished. Results found so far are kept.

Each query in a job reports a reason when it stops:

ReasonWhat happened
requested_count_reachedAll requested images were found.
source_exhausted_or_no_new_matchesThe source ran out of new matching images.
daily_page_quotaStopped at the project’s daily page limit. It resets at 00:00 UTC.
provider_failedThe image source kept failing for this query.
key_revoked_or_expiredThe API key was revoked or expired during the job.
user_cancelledThe job was cancelled.

Reading results

Read GET /v1/jobs/JOB_ID/results?limit=100&after=0. A page holds up to 100 images. Pass next_cursor as after to read the next page.

When next_cursor is null you have reached the end of the results available right now. If the job is still running, wait and continue from poll_after to fetch only new results without reading earlier rows again.

Retries and idempotency

Add a unique Idempotency-Key header when you create a job. Retrying with the same key and identical input returns the original job instead of starting a second one. Reusing the key with different input returns 409 IDEMPOTENCY_CONFLICT.

POST/v1/images/searchScope: search

Starts a job and returns 202 straight away. Send either query (with an optional count) or a queries array for a batch.

FieldTypeDescription
querystringThe search text, up to 200 characters.
countstringImages wanted per query, as a positive decimal string. Default "15". No product cap; the source may have fewer.
queriesarrayA batch of { query, count } objects. Queries must be unique.
min_widthintegerMinimum width in pixels, 0 to 20000. Default 800.
exclude_domainsstring[]Plain domains to skip, such as example.com.
watermark_policystringprefer_clean (default) or any. A hint from titles and domains, not a guarantee.
safe_search, image_type, layout, size, colorstringOptional filters.
Request
curl -X POST "$PLUKO_URL/v1/images/search" \
  -H "Authorization: Bearer $PLUKO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "query": "concrete architecture",
  "count": "15",
  "min_width": 1000
}'
Response · 202
{
  "job_id": "job_example",
  "reused": false,
  "status_url": "/v1/jobs/job_example",
  "results_url": "/v1/jobs/job_example/results",
  "request_id": "req_example"
}

Get a job

GET/v1/jobs/{job_id}Scope: read

Returns the job’s status, per-query progress, the filters it ran with, and expires: when its results will be removed.

Request
curl "$PLUKO_URL/v1/jobs/JOB_ID" \
  -H "Authorization: Bearer $PLUKO_API_KEY"
Response · 200 (excerpt)
{
  "job": {
    "id": "job_example",
    "status": "running",
    "requested": "15",
    "returned": 6
  },
  "request_id": "req_example"
}

Read results

GET/v1/jobs/{job_id}/resultsScope: read
FieldTypeDescription
limitinteger1 to 100. Default 50.
afterintegerCursor from the previous page’s next_cursor, or poll_after while the job runs. Default 0.
Request
curl "$PLUKO_URL/v1/jobs/JOB_ID/results?limit=100&after=0" \
  -H "Authorization: Bearer $PLUKO_API_KEY"
Response · 200 (excerpt)
{
  "images": [
    {
      "id": "img_example",
      "title": "Concrete architecture",
      "image_url": "https://source.example/image.jpg",
      "thumbnail_url": "https://source.example/preview.jpg",
      "source_url": "https://source.example/photo",
      "width": 1600,
      "height": 1200,
      "dimensions_verified": false,
      "license_status": "unknown"
    }
  ],
  "next_cursor": null,
  "request_id": "req_example"
}

List jobs

GET/v1/jobsScope: read

Lists the key’s project jobs, newest first.

FieldTypeDescription
limitinteger1 to 100. Default 50.
statusstringqueued, running, complete, partial or cancelled.
beforeintegerPass next_cursor from the previous page.
Request
curl "$PLUKO_URL/v1/jobs?status=running" \
  -H "Authorization: Bearer $PLUKO_API_KEY"

Cancel and retry

POST/v1/jobs/{job_id}/cancelScope: search
POST/v1/jobs/{job_id}/retryScope: search

Cancel stops a queued or running job. A job that already finished returns 409 JOB_FINISHED and keeps its status. Retry creates a new job from the original request and returns its job_id.

Request
curl -X POST "$PLUKO_URL/v1/jobs/JOB_ID/cancel" \
  -H "Authorization: Bearer $PLUKO_API_KEY"

Send feedback

POST/v1/feedbackScope: search
FieldTypeDescription
job_idrequiredstringThe job the result belongs to.
result_idrequiredstringThe image’s id.
reasonrequiredstringnot_relevant, watermark, broken or other.
notestringUp to 1000 characters.
Request
curl -X POST "$PLUKO_URL/v1/feedback" \
  -H "Authorization: Bearer $PLUKO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "job_id": "JOB_ID",
  "result_id": "IMAGE_ID",
  "reason": "not_relevant",
  "note": "The result does not match the query."
}'

List your reports, newest first, with GET /v1/feedback (read scope). Each one includes an image with its title, URLs and source domain, or null once retention has removed the result.

Usage

GET/v1/usageScope: read

Today’s provider pages used, the project’s daily limit, pages remaining, when it resets (00:00 UTC) and the last week. A page is one request to the image source and returns up to 35 images.

Request
curl "$PLUKO_URL/v1/usage" \
  -H "Authorization: Bearer $PLUKO_API_KEY"

Service status

GET/v1/statusNo API key needed

Reports whether the API, the search worker and the image source are working. It needs no key and is also shown on the status page.

Request
curl "$PLUKO_URL/v1/status"

Batch searches

Send several unique queries in one job, each with its own count. JSON bodies are limited to 64 KiB, so split very large batches across jobs. There’s no image-count cap, but source availability and your project’s daily limit still apply.

Request body
{
  "queries": [
    {
      "query": "mountain landscape",
      "count": "15"
    },
    {
      "query": "wireless headphones",
      "count": "30"
    }
  ],
  "min_width": 800,
  "watermark_policy": "prefer_clean"
}

Filters

Narrow a search with optional fields next to query. Omit a field, or send any, to leave it unfiltered. Filters are passed to the image source on a best-effort basis, so some results may not match exactly.

FieldValues
safe_searchmoderatestrict
image_typeanyphotoclipartline_drawinganimated_giftransparent
layoutanysquarewidetall
sizeanysmallmediumlargewallpaper
coloranycolorblack_whiteredorangeyellowgreentealbluepurplepinkbrownblackgraywhite

Important

safe_search defaults to moderate and can’t be switched off. An invalid value returns 400 INVALID_FILTER.
Request body
{
  "query": "city skyline",
  "count": "20",
  "image_type": "photo",
  "layout": "wide",
  "size": "large",
  "safe_search": "strict"
}

Webhooks

Skip polling: add a webhook to a project (Projects → Webhook) and Pluko sends a signed POST to your https endpoint when a job becomes complete, partial or cancelled. Fetch the results with the usual results endpoint using the event’s job_id.

Event body
{
  "id": "evt_example",
  "type": "job.finished",
  "created": 1760000000000,
  "data": {
    "job_id": "job_example",
    "project_id": "prj_example",
    "status": "complete",
    "requested": "20",
    "returned": 20,
    "queries": [
      {
        "query": "city skyline",
        "requested": "20",
        "found": 20,
        "status": "complete",
        "reason": "requested_count_reached"
      }
    ],
    "results_url": "/v1/jobs/job_example/results"
  }
}

Each request carries Pluko-Event, Pluko-Delivery (unique per event; use it to ignore duplicates) and Pluko-Signature as t=<ms>,v1=<hex>. v1 is the HMAC-SHA256 of "<t>.<raw body>" using your signing secret, which is shown once when you save the webhook. Verify it, and reject timestamps older than five minutes.

verify.js
import {createHmac,timingSafeEqual} from 'node:crypto';

// rawBody must be the exact request body string, before any JSON parsing.
export function verify(secret,header,rawBody){
  const parts=Object.fromEntries(header.split(',').map(part=>part.split('=')));
  if(Math.abs(Date.now()-Number(parts.t))>300000)return false; // reject replays
  const expected=createHmac('sha256',secret).update(`${parts.t}.${rawBody}`).digest('hex');
  const given=Buffer.from(parts.v1||''),wanted=Buffer.from(expected);
  return given.length===wanted.length&&timingSafeEqual(given,wanted);
}
RuleDetail
Answer quicklyAny 2xx status within 8 seconds.
RetriesUp to 5, after about 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours. Retry a failed one from the dashboard.
Endpointhttps on port 443 with a public domain name. Private and internal addresses are refused.
OrderingAt least once, not strictly ordered. Treat the job status as the source of truth.

Errors and limits

Errors share one shape. Keep request_id and job_id when you investigate a failure, and never include credentials in support reports.

Error body
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Provide a Bearer API key"
  },
  "request_id": "req_example"
}
StatusWhen
400Invalid query, count or other input.
401Missing, expired or revoked key.
403Missing scope or unavailable account access.
409An idempotency key was reused with different input, or the job already finished.
413The request body is larger than 64 KiB.
429Rate or queue limit. Respect the Retry-After header.
LimitValue
Requests per key120 per minute
Requests per account240 per minute across all keys
Active jobs per project10 queued or running
Daily provider pagesPer project; see Usage

Every limit is listed on the limits page.

Image metadata and reuse

FieldNotes
image_urlThe full image on its original host.
thumbnail_urlA smaller preview, when the source provides one.
source_urlThe page the image was found on.
width, heightReported by the source; dimensions_verified says whether they were checked.
license_statusAlways unknown unless a source supplies verifiable licence data.
watermark_hintA title and domain heuristic, not a guarantee.

Search results grant no reuse rights

Review the original source and its licensing before you use an image. Pluko never removes watermarks, and it stores metadata and logs, not image files. Source URLs can expire or block embedding, so handle image-load failures in your app.

OpenAPI and tools

GET /v1/openapi.json is the OpenAPI 3.1 description. Import it into Postman, Insomnia or a code generator.

Results are removed after the retention period shown on each job (expires), 30 days after it finishes by default. Export what you need before then.

Was this page helpful?

Tell us what’s missing. We read every note.