Nightjar for developers

Build complete image workflows with one API.

Upload source images, organize Products, author reusable creative ingredients, and run every Nightjar workflow programmatically.

Start here

Your first authenticated request

An active Nightjar Subscription includes API access. A Team owner creates a read-only or full API Credential in Settings → API. The key is shown once, so store it in a secret manager and never put it in source control.

const response = await fetch('https://api.nightjar.so/v1/team', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

A successful response identifies the Team, the credential profile, remaining Credits, and the effective Team-wide API concurrency budget. Every response carries a Request-Id header. If you contact support, send the Request-Id, endpoint, and approximate time; never send the API Key.

For coding agents

Treat the OpenAPI contract as authoritative for paths, schemas, and validation. The Markdown guide mirrors this entire page for agents, and llms.txt indexes every machine-readable surface.

Complete runnable examples

Upload → Product → Product Photography → poll → output Asset.

Spends 1 Credit

Runs against production

Set NIGHTJAR_API_KEY and NIGHTJAR_IMAGE_PATH. Running an example creates real resources in your Library and spends one Credit on a 1K image. Every example is verified against the contract in CI.
import { randomUUID } from 'node:crypto';
import { readFile } from 'node:fs/promises';

const apiKey = requiredEnvironmentVariable('NIGHTJAR_API_KEY');
const imagePath = requiredEnvironmentVariable('NIGHTJAR_IMAGE_PATH');
const apiBase = process.env.NIGHTJAR_BASE_URL ?? 'https://api.nightjar.so/v1';

function requiredEnvironmentVariable(name: string) {
  const value = process.env[name];
  if (!value) {
    throw new Error(`${name} is required.`);
  }
  return value;
}

function idempotencyKey(prefix: string) {
  return `${prefix}-${randomUUID()}`;
}

async function apiRequest<T>(path: string, init: RequestInit = {}) {
  const headers = new Headers(init.headers);
  headers.set('Authorization', `Bearer ${apiKey}`);
  const response = await fetch(`${apiBase}${path}`, { ...init, headers });
  if (!response.ok) {
    const requestId = response.headers.get('Request-Id') ?? 'unavailable';
    const problem = await response.text();
    throw new Error(
      `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${problem}`
    );
  }
  return (await response.json()) as T;
}

async function postJson<T>(path: string, body: unknown, requestKey: string) {
  return await apiRequest<T>(path, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Idempotency-Key': requestKey,
    },
    body: JSON.stringify(body),
  });
}

async function waitForGeneration(
  currentGeneration: Generation,
  attemptsRemaining = 120
): Promise<Generation> {
  if (
    currentGeneration.status === 'completed' ||
    currentGeneration.status === 'failed' ||
    attemptsRemaining === 0
  ) {
    return currentGeneration;
  }
  await new Promise((resolve) => setTimeout(resolve, 2000));
  const next = await apiRequest<Generation>(
    `/generations/${currentGeneration.id}`
  );
  return waitForGeneration(next, attemptsRemaining - 1);
}

type UploadSession = {
  id: string;
  upload: {
    method: string;
    url: string;
    headers: Record<string, string>;
  };
};

type Asset = { id: string; url: string };
type Product = { id: string };
type Generation = {
  id: string;
  status: 'queued' | 'processing' | 'completed' | 'failed';
  outputs: Array<{
    status: 'pending' | 'processing' | 'completed' | 'failed';
    asset: { id: string } | null;
  }>;
};

const image = await readFile(imagePath);
const upload = await postJson<UploadSession>(
  '/uploads',
  { content_type: 'image/jpeg', size_bytes: image.byteLength },
  idempotencyKey('upload')
);

const uploadResponse = await fetch(upload.upload.url, {
  method: upload.upload.method,
  headers: upload.upload.headers,
  body: image,
});
if (!uploadResponse.ok) {
  throw new Error(`Direct upload failed with HTTP ${uploadResponse.status}.`);
}
const receipt = (await uploadResponse.json()) as { upload_token: string };
const sourceAsset = await postJson<Asset>(
  `/uploads/${upload.id}/complete`,
  receipt,
  idempotencyKey('complete-upload')
);

const product = await postJson<Product>(
  '/products',
  {
    name: 'API quickstart product',
    asset_ids: [sourceAsset.id],
    primary_asset_id: sourceAsset.id,
  },
  idempotencyKey('create-product')
);

// This Generation spends one Credit.
const admittedGeneration = await postJson<Generation>(
  '/generations/product-photography',
  {
    product_ids: [product.id],
    background: { type: 'automatic' },
    fashion_model: { type: 'none' },
    output_mode: 'single_shots',
    image_count: 1,
    aspect_ratio: '1:1',
    resolution: '1k',
    output_format: 'jpeg',
  },
  idempotencyKey('create-generation')
);
const generation = await waitForGeneration(admittedGeneration);

const outputId = generation.outputs.find(
  (candidate) => candidate.status === 'completed'
)?.asset?.id;
if (!outputId) {
  throw new Error(`Generation ${generation.id} produced no completed output.`);
}
const outputAsset = await apiRequest<Asset>(`/assets/${outputId}`);
console.log(
  JSON.stringify({
    generation_id: generation.id,
    asset_id: outputAsset.id,
    url: outputAsset.url,
  })
);
End TypeScript example.

Mental model

Six durable concepts

Team

Every credential acts as one Team: its Library, Products, Credits, and Subscription.

Asset

A durable image identity from an upload or Generation, with a stable public-by-link URL.

Product

A reusable group of one or more Product Photo Assets, with one Primary Product Photo.

Reusable ingredient

A Photography Style, Background, Pose, or Fashion Model, created once and referenced in any Generation.

Operation

Non-cancelable asynchronous authoring work that creates or replaces a reusable ingredient.

Generation

Non-cancelable creative work with stable output slots, Credit reservation, and per-output results.

Files

Upload once, then reference the Asset

  1. Create one Upload Session with the source content type and exact byte length.
  2. POST raw bytes to the returned short-lived URL with every returned header. Do not send the API Credential to that URL.
  3. Exchange the returned upload token at the Session’s complete endpoint.
  4. Keep the resulting Asset ID and use it in Products, authoring, Edit Images, or Upscale.

JPG, PNG, GIF, WebP, and AVIF sources up to 70 MiB are accepted. Nightjar normalizes the durable Asset to JPEG, PNG, or WebP. A failed or expired Session creates no placeholder Asset.

Delivery

Asset URLs are stable and public by link while the underlying Asset exists. Fetch metadata with GET /v1/assets/:id. Downloading the URL does not require the API Credential.

Library

Create the same resources the web app uses

Assets, Products, Photography Styles, Backgrounds, Poses, and Fashion Models are Team-scoped Library resources. Ingredient lists include usable Team and global resources by default; use scope=team or scope=global to narrow them.

Product Photos are many-to-many: the same Asset can belong to several Products. Product updates replace the declared membership set atomically and keep a deterministic Primary Product Photo.

Which writes return an Operation

Product writes and ingredient metadata updates are synchronous. Ingredient creation and source replacement return an Operation because Nightjar may analyze the source or generate new media.

Authoring

Poll Operations until they are terminal

A 202 authoring response contains the Operation and a Location header. Poll that resource until completed or failed. Operations cannot be canceled.

type OperationStatus = 'queued' | 'processing' | 'completed' | 'failed';
type Operation = { id: string; status: OperationStatus };

const operationId = process.env.OPERATION_ID;
if (!operationId) throw new Error('OPERATION_ID is required.');
let operation: Operation;

do {
  const response = await fetch(
    `https://api.nightjar.so/v1/operations/${operationId}`,
    { headers: { Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}` } }
  );
  if (!response.ok) throw new Error(await response.text());
  operation = (await response.json()) as Operation;
  if (operation.status !== 'completed' && operation.status !== 'failed') {
    await new Promise((resolve) => setTimeout(resolve, 2000));
  }
} while (operation.status !== 'completed' && operation.status !== 'failed');
End TypeScript example.

Creation failure leaves no partial resource. Update failure preserves the previous version. While a source-changing Operation is active, PATCH and DELETE return 409 resource_operation_in_progress with its ID.

Credits follow generated media

Photography Styles are free. Backgrounds charge one Credit only if AI foreground removal commits. Pose and Fashion Model cards charge one Credit on successful commit. Metadata-only changes are always free.

Creative work

Stable outputs, even when work partially succeeds

Generation admission returns 202, reserves the complete planned Credit amount, and allocates every output slot. Poll the Generation until it is terminal; replaying the same Idempotency-Key returns the original admission response and never restarts work.

type GenerationStatus = 'queued' | 'processing' | 'completed' | 'failed';
type Generation = { id: string; status: GenerationStatus };

const generationId = process.env.GENERATION_ID;
if (!generationId) throw new Error('GENERATION_ID is required.');
let generation: Generation;

do {
  const response = await fetch(
    `https://api.nightjar.so/v1/generations/${generationId}`,
    { headers: { Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}` } }
  );
  if (!response.ok) throw new Error(await response.text());
  generation = (await response.json()) as Generation;
  if (generation.status !== 'completed' && generation.status !== 'failed') {
    await new Promise((resolve) => setTimeout(resolve, 2000));
  }
} while (generation.status !== 'completed' && generation.status !== 'failed');
End TypeScript example.

A Generation is terminal only when every output is terminal. If at least one output succeeds, the Generation is completed; failed slots retain provider-neutral errors and successful slots retain Asset references. If every slot fails, the Generation is failed. Only completed outputs are charged.

No cancellation in v1

Once admitted, creative work runs to completion and reserved Credits stay committed. Check the documented Credit cost and Team concurrency before admission; never submit work speculatively.

Creative surface

Three workflows, one lifecycle

Product Photography

Use Product IDs, loose Asset IDs, or both. output_mode is required: single_shots produces 1–6 independent images; photoshoot produces one cohesive four-image set.

Edit Images

Use one to eight Assets and natural-language instructions to produce exactly one edited output. All inputs jointly guide the result.

Upscale

Upscale one Asset to 2K or 4K. An Asset already at or above the requested target is rejected synchronously and costs nothing.

The API runs the same controls, validation, pricing, and Generation planning as the Nightjar web app, so API results match the app exactly.

Resilience

Design retries around explicit contracts

Idempotency

Every side-effecting admission requires an Idempotency-Key. Scope keys to one logical intent. Same key and same normalized intent replay the admission; changed intent returns 422. Records live for at least 24 hours.

Pagination

Lists default to 25 and accept at most 100. Follow page.next_cursor with the same filters. Cursors are opaque; invalid or mismatched cursors return 400.

Limits

Each credential may make 120 requests per minute, with a Team-wide ceiling of 600. Separately, a Team may have 20 public-API creative operations in progress. Read the effective creative limit from GET /v1/team, honor Retry-After on every 429, and use exponential backoff with jitter.

Deletion

DELETE is permanent with no restore endpoint. Deleting an Asset dissolves Product memberships and reports reassigned or deleted Products; historical Generation provenance remains.

Problem details

Machine codes stay stable; details stay human

Errors use application/problem+json. Branch on code, not title or detail. request_id matches the Request-Id response header.

HTTPCodeMeaningRecovery
400
invalid_request
Malformed JSON, unsupported query parameters, or a missing required header.Correct the request before retrying.
400
invalid_cursor
The pagination cursor is invalid for this collection or filter set.Restart pagination without the cursor.
401
authentication_required
The API Credential is missing or invalid.Supply a valid Bearer credential.
401
credential_expired
The API Credential reached its configured expiry.Create a replacement credential in Settings.
401
credential_revoked
The API Credential was revoked.Use a different active credential.
402
insufficient_credits
The Team cannot reserve enough Credits for this work.Add Credits, then submit a new logical request.
403
permission_denied
The credential profile cannot perform this operation.Use a full credential for writes.
403
subscription_required
Creative writes require an active Nightjar Subscription.Restore the Team Subscription; reads remain available.
404
not_found
The resource does not exist or is not visible to this Team.Treat the resource as unavailable.
404
upload_not_found
The Upload Session is unavailable.Create a new Upload Session.
404
asset_not_found
The Asset is unavailable to this Team.Use a live Asset from the Team Library.
404
product_not_found
The Product is unavailable to this Team.Use a live Product from the Team Library.
409
idempotency_in_progress
The same Idempotency-Key is still being admitted.Wait, then replay the same key and intent.
409
resource_operation_in_progress
The resource already has source-changing authoring work in progress.Poll active_operation_id before retrying.
409
upload_bytes_conflict
Different bytes were sent to an Upload Session that already accepted bytes.Create a new Upload Session for the different file.
410
upload_expired
The direct-upload window expired.Create a new Upload Session.
422
invalid_input
A field or field combination violates the documented request contract.Correct the reported fields before retrying.
422
invalid_reference
A referenced Asset, Product, or ingredient is not usable by this Team.Replace the invalid reference.
422
idempotency_key_reused
The key was already used for a different normalized request intent.Use the original intent or a new key.
422
already_at_target_resolution
The Asset already meets or exceeds the requested Upscale target.Use the Asset as-is or request a higher target.
429
concurrency_limit_reached
The Team reached its public-API creative-work concurrency budget.Honor Retry-After and wait for existing work to finish.
429
rate_limited
The request rate exceeded the per-credential or Team limit.Honor Retry-After and retry with backoff.
503
service_unavailable
Nightjar temporarily paused new API write admission.Honor Retry-After. Reads and already accepted work remain available.
500
internal_error
Nightjar could not complete the HTTP request.Retry transient failures and include Request-Id if support is needed.

Stability

A small compatibility promise you can plan around

Nightjar versions breaking changes in the path. Additive endpoints and optional fields may ship inside /v1; removals, renames, changed meanings, or new required inputs require a new major version. Clients must ignore unknown response fields.

24-month major-version support

When a new stable major version becomes generally available, Nightjar will support the superseded major for at least 24 months, except where an urgent security or legal requirement makes that unsafe.

Deprecated behavior is announced in the changelog and migration guide, then marked with the standard Deprecation header and a Link with rel="deprecation". Planned removal also includes the standard Sunset HTTP-date header.

Changelog

Published the initial v1 contract: 38 operations for uploads, Library resources, authoring Operations, and all three Generation workflows.

Generated from OpenAPI

Complete endpoint reference

Every operation below, including its language examples, response statuses, and Credit label, is derived from the downloadable canonical contract.

Team

Team Credits and API capabilities.

get/v1/teamRetrieve the credential's Team

Parameters

No parameters.

Responses200401429default

Request

const response = await fetch('https://api.nightjar.so/v1/team', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

The credential, its Team, Credits, and effective API capabilities.

200 response JSON
{
  "object": "team",
  "id": "string",
  "name": "string",
  "credits": 0,
  "credential": {
    "object": "api_credential",
    "id": "string",
    "name": "string",
    "prefix": "string",
    "profile": "read_only",
    "expires_at": "2026-08-16T10:00:00.000Z"
  },
  "capabilities": {
    "api_access": false,
    "subscription_active": false,
    "creative_writes_allowed": false,
    "api_concurrency": {
      "limit": 0,
      "used": 0
    }
  }
}

Uploads

Direct-upload session creation and finalization.

post/v1/uploadsCreate an Upload Session

Parameters

NameInTypeRequiredDescription
Idempotency-KeyheaderstringYesScoped by API Credential, HTTP method, and the concrete canonical path including resolved path parameters, and retained for at least 24 hours. The fingerprint uses normalized request intent rather than raw JSON. Same-key/same-intent replays the original admission status, headers, and body exactly; an in-flight duplicate returns 409 and mismatched reuse returns 422.
content_typebodyimage/jpeg | image/png | image/gif | image/webp | image/avifYesAccepted source formats match the web app. Nightjar normalizes the stored Asset to jpeg, png, or webp.
size_bytesbodyintegerYesRaw source byte length. The maximum is 70 MiB.
Responses201400401403409422429503default

Request

const response = await fetch('https://api.nightjar.so/v1/uploads', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(
    {
      "content_type": "image/jpeg",
      "size_bytes": 184320
    }
  ),
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

201

One short-lived Upload Session. No Asset exists yet.

201 response JSON
{
  "object": "upload",
  "id": "string",
  "status": "awaiting_upload",
  "upload": {
    "method": "POST",
    "url": "https://example.com/resource",
    "headers": {},
    "expires_at": "2026-08-16T10:00:00.000Z",
    "response": {
      "content_type": "application/json",
      "body": {
        "upload_token": "string"
      }
    }
  }
}
post/v1/uploads/{upload_id}/completeFinalize an Upload Session into an Asset

Parameters

NameInTypeRequiredDescription
upload_idpathstringYes
Idempotency-KeyheaderstringYesScoped by API Credential, HTTP method, and the concrete canonical path including resolved path parameters, and retained for at least 24 hours. The fingerprint uses normalized request intent rather than raw JSON. Same-key/same-intent replays the original admission status, headers, and body exactly; an in-flight duplicate returns 409 and mismatched reuse returns 422.
upload_tokenbodystringYes
Responses201400401403404409410422429503default

Request

const response = await fetch('https://api.nightjar.so/v1/uploads/upl_example/complete', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(
    {
      "upload_token": "upl_token_from_direct_upload"
    }
  ),
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

201

The complete usable Asset created by finalization.

201 response JSON
{
  "object": "asset",
  "id": "ast_example",
  "media_type": "image",
  "source": "upload",
  "source_generation_id": null,
  "format": "jpeg",
  "width": 1200,
  "height": 800,
  "size_bytes": 184320,
  "url": "https://assets.nightjar.so/team/example.jpg",
  "created_at": "2026-08-16T10:00:00.000Z"
}

Assets

Durable uploaded and generated images.

get/v1/assetsList Team Assets

Parameters

NameInTypeRequiredDescription
limitqueryintegerNo
afterquerystringNo
sourcequeryupload | generationNo
Responses200400401403429default

Request

const response = await fetch('https://api.nightjar.so/v1/assets', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

Team Assets ordered by created_at desc, id desc.

200 response JSON
{
  "data": [
    {
      "object": "asset",
      "id": "ast_example",
      "media_type": "image",
      "source": "upload",
      "source_generation_id": null,
      "format": "jpeg",
      "width": 1200,
      "height": 800,
      "size_bytes": 184320,
      "url": "https://assets.nightjar.so/team/example.jpg",
      "created_at": "2026-08-16T10:00:00.000Z"
    }
  ],
  "page": {
    "has_more": false,
    "next_cursor": "string"
  }
}
get/v1/assets/{asset_id}Retrieve an Asset

Parameters

NameInTypeRequiredDescription
asset_idpathstringYes
Responses200401403404429default

Request

const response = await fetch('https://api.nightjar.so/v1/assets/ast_example', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

One active Asset.

200 response JSON
{
  "object": "asset",
  "id": "ast_example",
  "media_type": "image",
  "source": "upload",
  "source_generation_id": null,
  "format": "jpeg",
  "width": 1200,
  "height": 800,
  "size_bytes": 184320,
  "url": "https://assets.nightjar.so/team/example.jpg",
  "created_at": "2026-08-16T10:00:00.000Z"
}
delete/v1/assets/{asset_id}Delete an Asset

Parameters

NameInTypeRequiredDescription
asset_idpathstringYes
Responses200401403404409429503default

Request

const response = await fetch('https://api.nightjar.so/v1/assets/ast_example', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

Deleted from authenticated API and UI use. Every Product Photo membership is removed; affected nonempty Products receive a deterministic replacement Primary Product Photo, and only Products left empty are deleted. Historical Generation output references remain and may resolve to 404.

200 response JSON
{
  "object": "asset_deletion",
  "id": "ast_example",
  "removed_product_memberships": 2,
  "reassigned_primary_product_ids": [
    "prd_surviving"
  ],
  "deleted_product_ids": [
    "prd_left_empty"
  ]
}

Products

Reusable collections of Product Photos.

get/v1/productsList Products

Parameters

NameInTypeRequiredDescription
limitqueryintegerNo
afterquerystringNo
Responses200400401403429default

Request

const response = await fetch('https://api.nightjar.so/v1/products', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

Team Products ordered by created_at desc, id desc.

200 response JSON
{
  "data": [
    {
      "object": "product",
      "id": "prd_example",
      "name": "Field Bag",
      "description": "Waxed canvas with leather handles",
      "dimensions": "40 × 28 × 12 cm",
      "asset_ids": [
        "ast_example_front",
        "ast_example_detail"
      ],
      "primary_asset_id": "ast_example_front",
      "created_at": "2026-08-16T10:00:00.000Z",
      "updated_at": "2026-08-16T10:00:00.000Z"
    }
  ],
  "page": {
    "has_more": false,
    "next_cursor": "string"
  }
}
post/v1/productsCreate a Product

Parameters

NameInTypeRequiredDescription
Idempotency-KeyheaderstringYesScoped by API Credential, HTTP method, and the concrete canonical path including resolved path parameters, and retained for at least 24 hours. The fingerprint uses normalized request intent rather than raw JSON. Same-key/same-intent replays the original admission status, headers, and body exactly; an in-flight duplicate returns 409 and mismatched reuse returns 422.
namebodystringYes
descriptionbodystringNo
dimensionsbodystringNo
asset_idsbodystring[]Yes
primary_asset_idbodystringYesMust be present in asset_ids.
Responses201401403409422429503default

Request

const response = await fetch('https://api.nightjar.so/v1/products', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(
    {
      "name": "Field Bag",
      "description": "Waxed canvas with leather handles",
      "dimensions": "40 × 28 × 12 cm",
      "asset_ids": [
        "ast_example_front",
        "ast_example_detail"
      ],
      "primary_asset_id": "ast_example_front"
    }
  ),
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

201

Product created synchronously and atomically.

201 response JSON
{
  "object": "product",
  "id": "prd_example",
  "name": "Field Bag",
  "description": "Waxed canvas with leather handles",
  "dimensions": "40 × 28 × 12 cm",
  "asset_ids": [
    "ast_example_front",
    "ast_example_detail"
  ],
  "primary_asset_id": "ast_example_front",
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z"
}
get/v1/products/{product_id}Retrieve a Product

Parameters

NameInTypeRequiredDescription
product_idpathstringYes
Responses200401403404429default

Request

const response = await fetch('https://api.nightjar.so/v1/products/prd_example', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

One Product.

200 response JSON
{
  "object": "product",
  "id": "prd_example",
  "name": "Field Bag",
  "description": "Waxed canvas with leather handles",
  "dimensions": "40 × 28 × 12 cm",
  "asset_ids": [
    "ast_example_front",
    "ast_example_detail"
  ],
  "primary_asset_id": "ast_example_front",
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z"
}
patch/v1/products/{product_id}Update a Product

Parameters

NameInTypeRequiredDescription
product_idpathstringYes
Idempotency-KeyheaderstringYesScoped by API Credential, HTTP method, and the concrete canonical path including resolved path parameters, and retained for at least 24 hours. The fingerprint uses normalized request intent rather than raw JSON. Same-key/same-intent replays the original admission status, headers, and body exactly; an in-flight duplicate returns 409 and mismatched reuse returns 422.
namebodystringNo
descriptionbodystringNo
dimensionsbodystringNo
asset_idsbodystring[]No
primary_asset_idbodystringNo
Responses200401403404409422429503default

Request

const response = await fetch('https://api.nightjar.so/v1/products/prd_example', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(
    {
      "name": "Field Bag · Navy",
      "asset_ids": [
        "ast_example_front",
        "ast_example_navy"
      ],
      "primary_asset_id": "ast_example_navy"
    }
  ),
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

Updated synchronously and atomically.

200 response JSON
{
  "object": "product",
  "id": "prd_example",
  "name": "Field Bag",
  "description": "Waxed canvas with leather handles",
  "dimensions": "40 × 28 × 12 cm",
  "asset_ids": [
    "ast_example_front",
    "ast_example_detail"
  ],
  "primary_asset_id": "ast_example_front",
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z"
}
delete/v1/products/{product_id}Delete a Product

Parameters

NameInTypeRequiredDescription
product_idpathstringYes
Responses204401403404409429503default

Request

const response = await fetch('https://api.nightjar.so/v1/products/prd_example', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

204

Permanently removed from API and UI use. Product Photo relationships dissolve; Assets and historical Product Attribution remain.

No response body.

Photography Styles

Reusable visual style ingredients.

get/v1/photography-stylesList Photography Styles

Returns every usable Team-owned and global resource by default, ordered together by created_at desc, id desc. Use scope to narrow the collection.

Parameters

NameInTypeRequiredDescription
limitqueryintegerNo
afterquerystringNo
scopequeryall | team | globalNoOmit for every resource the Team can use, or filter to Team-owned or global premade resources.
Responses200400401403429default

Request

const response = await fetch('https://api.nightjar.so/v1/photography-styles', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

Team and premade Photography Styles.

200 response JSON
{
  "data": [
    {
      "object": "photography_style",
      "id": null,
      "scope": null,
      "name": null,
      "source_asset_ids": null,
      "created_at": null,
      "updated_at": null,
      "description": "string",
      "preview_image_urls": []
    }
  ],
  "page": {
    "has_more": false,
    "next_cursor": "string"
  }
}
post/v1/photography-stylesCreate a Photography StyleFree

Creates a Photography Style asynchronously from exactly three Team Assets. No image is generated, so it costs zero Credits.

Parameters

NameInTypeRequiredDescription
Idempotency-KeyheaderstringYesScoped by API Credential, HTTP method, and the concrete canonical path including resolved path parameters, and retained for at least 24 hours. The fingerprint uses normalized request intent rather than raw JSON. Same-key/same-intent replays the original admission status, headers, and body exactly; an in-flight duplicate returns 409 and mismatched reuse returns 422.
namebodystringYes
descriptionbodystringNo
asset_idsbodystring[]Yes
Responses202401403409422429503default

Request

const response = await fetch('https://api.nightjar.so/v1/photography-styles', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(
    {
      "name": "Quiet Editorial",
      "description": "Soft daylight and restrained warm neutrals.",
      "asset_ids": [
        "ast_style_one",
        "ast_style_two",
        "ast_style_three"
      ]
    }
  ),
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

202

Non-cancelable authoring work accepted.

202 response JSON
{
  "object": "operation",
  "id": "string",
  "type": "string",
  "status": "queued",
  "request": {},
  "target": {
    "object": "photography_style",
    "id": "string"
  },
  "result": {
    "object": "photography_style",
    "id": "string"
  },
  "error": {
    "code": null,
    "message": null,
    "retryable": null
  },
  "credits": {
    "reserved": 0,
    "charged": 0
  },
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z",
  "started_at": "2026-08-16T10:00:00.000Z",
  "finished_at": "2026-08-16T10:00:00.000Z"
}
get/v1/photography-styles/{photography_style_id}Retrieve a Photography Style

Parameters

NameInTypeRequiredDescription
photography_style_idpathstringYes
Responses200401403404429default

Request

const response = await fetch('https://api.nightjar.so/v1/photography-styles/sty_example', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

One Photography Style.

200 response JSON
{
  "object": "photography_style",
  "id": "string",
  "scope": "team",
  "name": "string",
  "source_asset_ids": [
    "string"
  ],
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z",
  "description": "string",
  "preview_image_urls": [
    "https://example.com/resource"
  ]
}
patch/v1/photography-styles/{photography_style_id}Update a Photography StyleFree

Metadata updates and source replacement are free because no image is generated.

Parameters

NameInTypeRequiredDescription
photography_style_idpathstringYes
Idempotency-KeyheaderstringYesScoped by API Credential, HTTP method, and the concrete canonical path including resolved path parameters, and retained for at least 24 hours. The fingerprint uses normalized request intent rather than raw JSON. Same-key/same-intent replays the original admission status, headers, and body exactly; an in-flight duplicate returns 409 and mismatched reuse returns 422.
namebodystringNo
descriptionbodystringNo
asset_idsbodystring[]No
Responses200202401403404409422429503default

Request

const response = await fetch('https://api.nightjar.so/v1/photography-styles/sty_example', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(
    {
      "name": "Quiet Editorial · Spring"
    }
  ),
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

Returned only for a metadata-only patch, which commits synchronously and free.

200 response JSON
{
  "object": "photography_style",
  "id": "string",
  "scope": "team",
  "name": "string",
  "source_asset_ids": [
    "string"
  ],
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z",
  "description": "string",
  "preview_image_urls": [
    "https://example.com/resource"
  ]
}
delete/v1/photography-styles/{photography_style_id}Delete a Photography Style

Parameters

NameInTypeRequiredDescription
photography_style_idpathstringYes
Responses204401403404409429503default

Request

const response = await fetch('https://api.nightjar.so/v1/photography-styles/sty_example', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

204

Permanently removed from API and UI use.

No response body.

Backgrounds

Reusable background ingredients.

get/v1/backgroundsList Backgrounds

Returns every usable Team-owned and global resource by default, ordered together by created_at desc, id desc. Use scope to narrow the collection.

Parameters

NameInTypeRequiredDescription
limitqueryintegerNo
afterquerystringNo
scopequeryall | team | globalNoOmit for every resource the Team can use, or filter to Team-owned or global premade resources.
Responses200400401403429default

Request

const response = await fetch('https://api.nightjar.so/v1/backgrounds', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

Team and premade Backgrounds.

200 response JSON
{
  "data": [
    {
      "object": "background",
      "id": null,
      "scope": null,
      "name": null,
      "source_asset_ids": null,
      "created_at": null,
      "updated_at": null,
      "description": "string",
      "kind": "backdrop",
      "card_image_url": "https://example.com/resource"
    }
  ],
  "page": {
    "has_more": false,
    "next_cursor": "string"
  }
}
post/v1/backgroundsCreate a Background0 or 1 Credit

Creates a Background from one Team Asset. It is free when the scene is already clean and costs exactly one Credit only when foreground-removal image generation runs and commits.

Parameters

NameInTypeRequiredDescription
Idempotency-KeyheaderstringYesScoped by API Credential, HTTP method, and the concrete canonical path including resolved path parameters, and retained for at least 24 hours. The fingerprint uses normalized request intent rather than raw JSON. Same-key/same-intent replays the original admission status, headers, and body exactly; an in-flight duplicate returns 409 and mismatched reuse returns 422.
namebodystringYes
kindbodybackdrop | locationYes
asset_idbodystringYes
Responses202401402403409422429503default

Request

const response = await fetch('https://api.nightjar.so/v1/backgrounds', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(
    {
      "name": "Warm Stone Studio",
      "kind": "backdrop",
      "asset_id": "ast_background_source"
    }
  ),
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

202

Non-cancelable authoring work accepted.

202 response JSON
{
  "object": "operation",
  "id": "string",
  "type": "string",
  "status": "queued",
  "request": {},
  "target": {
    "object": "photography_style",
    "id": "string"
  },
  "result": {
    "object": "photography_style",
    "id": "string"
  },
  "error": {
    "code": null,
    "message": null,
    "retryable": null
  },
  "credits": {
    "reserved": 0,
    "charged": 0
  },
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z",
  "started_at": "2026-08-16T10:00:00.000Z",
  "finished_at": "2026-08-16T10:00:00.000Z"
}
get/v1/backgrounds/{background_id}Retrieve a Background

Parameters

NameInTypeRequiredDescription
background_idpathstringYes
Responses200401403404429default

Request

const response = await fetch('https://api.nightjar.so/v1/backgrounds/bkg_example', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

One Background.

200 response JSON
{
  "object": "background",
  "id": "string",
  "scope": "team",
  "name": "string",
  "source_asset_ids": [
    "string"
  ],
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z",
  "description": "string",
  "kind": "backdrop",
  "card_image_url": "https://example.com/resource"
}
patch/v1/backgrounds/{background_id}Update a Background0 or 1 Credit

Metadata updates are free. Replacing the source costs one Credit only when foreground-removal image generation runs and commits.

Parameters

NameInTypeRequiredDescription
background_idpathstringYes
Idempotency-KeyheaderstringYesScoped by API Credential, HTTP method, and the concrete canonical path including resolved path parameters, and retained for at least 24 hours. The fingerprint uses normalized request intent rather than raw JSON. Same-key/same-intent replays the original admission status, headers, and body exactly; an in-flight duplicate returns 409 and mismatched reuse returns 422.
namebodystringNo
kindbodybackdrop | locationNo
asset_idbodystringNo
Responses200202401402403404409422429503default

Request

const response = await fetch('https://api.nightjar.so/v1/backgrounds/bkg_example', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(
    {
      "name": "Warm Stone Studio · Clean"
    }
  ),
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

Returned only for a metadata-only patch, which commits synchronously and free.

200 response JSON
{
  "object": "background",
  "id": "string",
  "scope": "team",
  "name": "string",
  "source_asset_ids": [
    "string"
  ],
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z",
  "description": "string",
  "kind": "backdrop",
  "card_image_url": "https://example.com/resource"
}
delete/v1/backgrounds/{background_id}Delete a Background

Parameters

NameInTypeRequiredDescription
background_idpathstringYes
Responses204401403404409429503default

Request

const response = await fetch('https://api.nightjar.so/v1/backgrounds/bkg_example', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

204

Permanently removed from API and UI use.

No response body.

Poses

Reusable pose ingredients.

get/v1/posesList Poses

Returns every usable Team-owned and global resource by default, ordered together by created_at desc, id desc. Use scope to narrow the collection.

Parameters

NameInTypeRequiredDescription
limitqueryintegerNo
afterquerystringNo
scopequeryall | team | globalNoOmit for every resource the Team can use, or filter to Team-owned or global premade resources.
Responses200400401403429default

Request

const response = await fetch('https://api.nightjar.so/v1/poses', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

Team and premade Poses.

200 response JSON
{
  "data": [
    {
      "object": "pose",
      "id": null,
      "scope": null,
      "name": null,
      "source_asset_ids": null,
      "created_at": null,
      "updated_at": null,
      "description": "string",
      "card_image_url": "https://example.com/resource",
      "supported_camera_distances": []
    }
  ],
  "page": {
    "has_more": false,
    "next_cursor": "string"
  }
}
post/v1/posesCreate a Pose1 Credit

Creates a Pose from one Team Asset. A new neutral card image is generated, so a successful commit costs exactly one Credit.

Parameters

NameInTypeRequiredDescription
Idempotency-KeyheaderstringYesScoped by API Credential, HTTP method, and the concrete canonical path including resolved path parameters, and retained for at least 24 hours. The fingerprint uses normalized request intent rather than raw JSON. Same-key/same-intent replays the original admission status, headers, and body exactly; an in-flight duplicate returns 409 and mismatched reuse returns 422.
namebodystringYes
asset_idbodystringYes
supported_camera_distancesbodyclose | medium | far[]No
Responses202401402403409422429503default

Request

const response = await fetch('https://api.nightjar.so/v1/poses', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(
    {
      "name": "Relaxed Standing",
      "asset_id": "ast_pose_source"
    }
  ),
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

202

Non-cancelable authoring work accepted.

202 response JSON
{
  "object": "operation",
  "id": "string",
  "type": "string",
  "status": "queued",
  "request": {},
  "target": {
    "object": "photography_style",
    "id": "string"
  },
  "result": {
    "object": "photography_style",
    "id": "string"
  },
  "error": {
    "code": null,
    "message": null,
    "retryable": null
  },
  "credits": {
    "reserved": 0,
    "charged": 0
  },
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z",
  "started_at": "2026-08-16T10:00:00.000Z",
  "finished_at": "2026-08-16T10:00:00.000Z"
}
get/v1/poses/{pose_id}Retrieve a Pose

Parameters

NameInTypeRequiredDescription
pose_idpathstringYes
Responses200401403404429default

Request

const response = await fetch('https://api.nightjar.so/v1/poses/pos_example', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

One Pose.

200 response JSON
{
  "object": "pose",
  "id": "string",
  "scope": "team",
  "name": "string",
  "source_asset_ids": [
    "string"
  ],
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z",
  "description": "string",
  "card_image_url": "https://example.com/resource",
  "supported_camera_distances": [
    "close"
  ]
}
patch/v1/poses/{pose_id}Update a Pose0 or 1 Credit

Metadata updates are free. Replacing the source generates a new card and costs one Credit on successful commit.

Parameters

NameInTypeRequiredDescription
pose_idpathstringYes
Idempotency-KeyheaderstringYesScoped by API Credential, HTTP method, and the concrete canonical path including resolved path parameters, and retained for at least 24 hours. The fingerprint uses normalized request intent rather than raw JSON. Same-key/same-intent replays the original admission status, headers, and body exactly; an in-flight duplicate returns 409 and mismatched reuse returns 422.
namebodystringNo
asset_idbodystringNo
supported_camera_distancesbodyclose | medium | far[]No
Responses200202401402403404409422429503default

Request

const response = await fetch('https://api.nightjar.so/v1/poses/pos_example', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(
    {
      "name": "Relaxed Standing · Front"
    }
  ),
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

Returned only for a metadata-only patch, which commits synchronously and free.

200 response JSON
{
  "object": "pose",
  "id": "string",
  "scope": "team",
  "name": "string",
  "source_asset_ids": [
    "string"
  ],
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z",
  "description": "string",
  "card_image_url": "https://example.com/resource",
  "supported_camera_distances": [
    "close"
  ]
}
delete/v1/poses/{pose_id}Delete a Pose

Parameters

NameInTypeRequiredDescription
pose_idpathstringYes
Responses204401403404409429503default

Request

const response = await fetch('https://api.nightjar.so/v1/poses/pos_example', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

204

Permanently removed from API and UI use.

No response body.

Fashion Models

Reusable fashion model ingredients.

get/v1/fashion-modelsList Fashion Models

Returns every usable Team-owned and global resource by default, ordered together by created_at desc, id desc. Use scope to narrow the collection.

Parameters

NameInTypeRequiredDescription
limitqueryintegerNo
afterquerystringNo
scopequeryall | team | globalNoOmit for every resource the Team can use, or filter to Team-owned or global premade resources.
Responses200400401403429default

Request

const response = await fetch('https://api.nightjar.so/v1/fashion-models', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

Team and premade Fashion Models.

200 response JSON
{
  "data": [
    {
      "object": "fashion_model",
      "id": null,
      "scope": null,
      "name": null,
      "source_asset_ids": null,
      "created_at": null,
      "updated_at": null,
      "age_range": null,
      "gender": null,
      "card_image_url": "https://example.com/resource"
    }
  ],
  "page": {
    "has_more": false,
    "next_cursor": "string"
  }
}
post/v1/fashion-modelsCreate a Fashion Model1 Credit

Creates a Fashion Model from one to five Team Assets and required metadata. A new identity card is generated, so a successful commit costs exactly one Credit.

Parameters

NameInTypeRequiredDescription
Idempotency-KeyheaderstringYesScoped by API Credential, HTTP method, and the concrete canonical path including resolved path parameters, and retained for at least 24 hours. The fingerprint uses normalized request intent rather than raw JSON. Same-key/same-intent replays the original admission status, headers, and body exactly; an in-flight duplicate returns 409 and mismatched reuse returns 422.
namebodystringYes
age_rangebody18-25 | 25-35 | 35-45 | 45-55 | 55+Yes
genderbodymale | female | neutralYes
asset_idsbodystring[]Yes
Responses202401402403409422429503default

Request

const response = await fetch('https://api.nightjar.so/v1/fashion-models', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(
    {
      "name": "Alex",
      "age_range": "25-35",
      "gender": "female",
      "asset_ids": [
        "ast_model_front",
        "ast_model_profile"
      ]
    }
  ),
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

202

Non-cancelable authoring work accepted.

202 response JSON
{
  "object": "operation",
  "id": "string",
  "type": "string",
  "status": "queued",
  "request": {},
  "target": {
    "object": "photography_style",
    "id": "string"
  },
  "result": {
    "object": "photography_style",
    "id": "string"
  },
  "error": {
    "code": null,
    "message": null,
    "retryable": null
  },
  "credits": {
    "reserved": 0,
    "charged": 0
  },
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z",
  "started_at": "2026-08-16T10:00:00.000Z",
  "finished_at": "2026-08-16T10:00:00.000Z"
}
get/v1/fashion-models/{fashion_model_id}Retrieve a Fashion Model

Parameters

NameInTypeRequiredDescription
fashion_model_idpathstringYes
Responses200401403404429default

Request

const response = await fetch('https://api.nightjar.so/v1/fashion-models/mdl_example', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

One Fashion Model.

200 response JSON
{
  "object": "fashion_model",
  "id": "string",
  "scope": "team",
  "name": "string",
  "source_asset_ids": [
    "string"
  ],
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z",
  "age_range": "18-25",
  "gender": "male",
  "card_image_url": "https://example.com/resource"
}
patch/v1/fashion-models/{fashion_model_id}Update a Fashion Model0 or 1 Credit

Metadata updates are free. Replacing source Assets generates a new identity card and costs one Credit on successful commit.

Parameters

NameInTypeRequiredDescription
fashion_model_idpathstringYes
Idempotency-KeyheaderstringYesScoped by API Credential, HTTP method, and the concrete canonical path including resolved path parameters, and retained for at least 24 hours. The fingerprint uses normalized request intent rather than raw JSON. Same-key/same-intent replays the original admission status, headers, and body exactly; an in-flight duplicate returns 409 and mismatched reuse returns 422.
namebodystringNo
age_rangebody18-25 | 25-35 | 35-45 | 45-55 | 55+No
genderbodymale | female | neutralNo
asset_idsbodystring[]No
Responses200202401402403404409422429503default

Request

const response = await fetch('https://api.nightjar.so/v1/fashion-models/mdl_example', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(
    {
      "name": "Alex · Summer"
    }
  ),
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

Returned only for a metadata-only patch, which commits synchronously and free.

200 response JSON
{
  "object": "fashion_model",
  "id": "string",
  "scope": "team",
  "name": "string",
  "source_asset_ids": [
    "string"
  ],
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z",
  "age_range": "18-25",
  "gender": "male",
  "card_image_url": "https://example.com/resource"
}
delete/v1/fashion-models/{fashion_model_id}Delete a Fashion Model

Parameters

NameInTypeRequiredDescription
fashion_model_idpathstringYes
Responses204401403404409429503default

Request

const response = await fetch('https://api.nightjar.so/v1/fashion-models/mdl_example', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

204

Permanently removed from API and UI use.

No response body.

Operations

Asynchronous reusable-ingredient authoring work.

get/v1/operationsList authoring Operations

Returns Team authoring Operations ordered by created_at desc, id desc. Use the filters to recover specific in-flight or historical work.

Parameters

NameInTypeRequiredDescription
limitqueryintegerNo
afterquerystringNo
statusqueryqueued | processing | completed | failedNo
typequeryphotography_style.create | photography_style.update | background.create | background.update | pose.create | pose.update | fashion_model.create | fashion_model.updateNo
target_idquerystringNo
Responses200400401403429default

Request

const response = await fetch('https://api.nightjar.so/v1/operations', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

Matching authoring Operations.

200 response JSON
{
  "data": [
    {
      "object": "operation",
      "id": "string",
      "type": "string",
      "status": "queued",
      "request": {},
      "target": null,
      "result": null,
      "error": null,
      "credits": {
        "reserved": null,
        "charged": null
      },
      "created_at": "2026-08-16T10:00:00.000Z",
      "updated_at": "2026-08-16T10:00:00.000Z",
      "started_at": "2026-08-16T10:00:00.000Z",
      "finished_at": "2026-08-16T10:00:00.000Z"
    }
  ],
  "page": {
    "has_more": false,
    "next_cursor": "string"
  }
}
get/v1/operations/{operation_id}Retrieve an authoring Operation

Parameters

NameInTypeRequiredDescription
operation_idpathstringYes
Responses200401403404429default

Request

const response = await fetch('https://api.nightjar.so/v1/operations/op_example', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

Authoritative authoring lifecycle snapshot.

200 response JSON
{
  "object": "operation",
  "id": "string",
  "type": "string",
  "status": "queued",
  "request": {},
  "target": {
    "object": "photography_style",
    "id": "string"
  },
  "result": {
    "object": "photography_style",
    "id": "string"
  },
  "error": {
    "code": null,
    "message": null,
    "retryable": null
  },
  "credits": {
    "reserved": 0,
    "charged": 0
  },
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z",
  "started_at": "2026-08-16T10:00:00.000Z",
  "finished_at": "2026-08-16T10:00:00.000Z"
}

Generations

Asynchronous creative workflow executions.

get/v1/generationsList Generations

Parameters

NameInTypeRequiredDescription
limitqueryintegerNo
afterquerystringNo
workflowqueryproduct_photography | edit_images | upscaleNo
statusqueryqueued | processing | completed | failedNo
Responses200400401403429default

Request

const response = await fetch('https://api.nightjar.so/v1/generations', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

Public-API Generations for the Team across all of its API Credentials, ordered by created_at desc, id desc. Web-only Generations that cannot satisfy this contract are excluded.

200 response JSON
{
  "data": [
    {}
  ],
  "page": {
    "has_more": false,
    "next_cursor": "string"
  }
}
get/v1/generations/{generation_id}Retrieve a Generation

Parameters

NameInTypeRequiredDescription
generation_idpathstringYes
Responses200401403404429default

Request

const response = await fetch('https://api.nightjar.so/v1/generations/gen_example', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
  },
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

200

Authoritative Generation lifecycle and output snapshot.

200 response JSON
{
  "object": "generation",
  "id": "string",
  "initiated_by": null,
  "status": null,
  "output_summary": null,
  "outputs": [],
  "credits": null,
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z",
  "started_at": "2026-08-16T10:00:00.000Z",
  "finished_at": "2026-08-16T10:00:00.000Z",
  "workflow": "product_photography",
  "request": null
}
post/v1/generations/product-photographyStart Product Photography or a Photoshoot1 Credit per generated image

Every selected Product and loose Asset is a joint subject in every output. single_shots costs one Credit per requested 1k or 2k image and two Credits per requested 4k image. photoshoot costs two Credits total and produces four output slots. The complete amount is planned and reserved before admission.

Parameters

NameInTypeRequiredDescription
Idempotency-KeyheaderstringYesScoped by API Credential, HTTP method, and the concrete canonical path including resolved path parameters, and retained for at least 24 hours. The fingerprint uses normalized request intent rather than raw JSON. Same-key/same-intent replays the original admission status, headers, and body exactly; an in-flight duplicate returns 409 and mismatched reuse returns 422.
Responses202401402403409422429503default

Request

const response = await fetch('https://api.nightjar.so/v1/generations/product-photography', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(
    {
      "product_ids": [
        "prd_01JPRODUCT"
      ],
      "asset_ids": [
        "ast_01JLOOSEANGLE"
      ],
      "photography_style_id": "sty_01JEDITORIAL",
      "background": {
        "type": "background",
        "background_id": "bkg_01JWARMSTUDIO"
      },
      "fashion_model": {
        "type": "selected",
        "fashion_model_id": "mdl_01JALEX"
      },
      "pose_id": "pos_01JSTANDING",
      "camera_distance": "medium",
      "custom_directions": "Keep both products clearly visible.",
      "output_mode": "single_shots",
      "image_count": 2,
      "aspect_ratio": "4:5",
      "resolution": "2k",
      "output_format": "webp"
    }
  ),
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

202

Non-cancelable Generation admitted with stable output slots.

202 response JSON
{
  "object": "generation",
  "id": "string",
  "initiated_by": null,
  "status": null,
  "output_summary": null,
  "outputs": [],
  "credits": null,
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z",
  "started_at": "2026-08-16T10:00:00.000Z",
  "finished_at": "2026-08-16T10:00:00.000Z",
  "workflow": "product_photography",
  "request": null
}
post/v1/generations/edit-imagesStart Edit Images1 or 2 Credits

All one to eight input Assets jointly guide exactly one edited output. Cost is one Credit at 1k or 2k and two Credits at 4k, planned and reserved before admission.

Parameters

NameInTypeRequiredDescription
Idempotency-KeyheaderstringYesScoped by API Credential, HTTP method, and the concrete canonical path including resolved path parameters, and retained for at least 24 hours. The fingerprint uses normalized request intent rather than raw JSON. Same-key/same-intent replays the original admission status, headers, and body exactly; an in-flight duplicate returns 409 and mismatched reuse returns 422.
aspect_ratiobody21:9 | 1:1 | 4:3 | 3:2 | 2:3 | 5:4 | 4:5 | 3:4 | 16:9 | 9:16Yes
resolutionbody1k | 2k | 4kYes
output_formatbodyjpeg | png | webpYes
asset_idsbodystring[]Yes
instructionsbodystringYes
Responses202401402403409422429503default

Request

const response = await fetch('https://api.nightjar.so/v1/generations/edit-images', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(
    {
      "asset_ids": [
        "ast_01JPRODUCT",
        "ast_01JSCENE"
      ],
      "instructions": "Place the product from image 1 into the scene from image 2.",
      "aspect_ratio": "4:5",
      "resolution": "2k",
      "output_format": "webp"
    }
  ),
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

202

Non-cancelable Generation admitted with stable output slots.

202 response JSON
{
  "object": "generation",
  "id": "string",
  "initiated_by": null,
  "status": null,
  "output_summary": null,
  "outputs": [],
  "credits": null,
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z",
  "started_at": "2026-08-16T10:00:00.000Z",
  "finished_at": "2026-08-16T10:00:00.000Z",
  "workflow": "product_photography",
  "request": null
}
post/v1/generations/upscaleStart Upscale1 or 2 Credits

Produces exactly one upscaled output. Cost is one Credit at 2k and two Credits at 4k, planned and reserved before admission. An Asset already at or above the requested target is rejected synchronously and costs nothing.

Parameters

NameInTypeRequiredDescription
Idempotency-KeyheaderstringYesScoped by API Credential, HTTP method, and the concrete canonical path including resolved path parameters, and retained for at least 24 hours. The fingerprint uses normalized request intent rather than raw JSON. Same-key/same-intent replays the original admission status, headers, and body exactly; an in-flight duplicate returns 409 and mismatched reuse returns 422.
asset_idbodystringYes
target_resolutionbody2k | 4kYes
output_formatbodyjpeg | png | webpYes
Responses202401402403409422429503default

Request

const response = await fetch('https://api.nightjar.so/v1/generations/upscale', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NIGHTJAR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(
    {
      "asset_id": "ast_01JPRODUCT",
      "target_resolution": "2k",
      "output_format": "webp"
    }
  ),
});

if (!response.ok) {
  const requestId = response.headers.get('Request-Id') ?? 'unavailable';
  throw new Error(
    `Nightjar request failed (${response.status}, Request-Id ${requestId}): ${await response.text()}`
  );
}
const result: unknown =
  response.status === 204 ? null : await response.json();
console.log(result);
End TypeScript example.

Successful response

202

Non-cancelable Generation admitted with stable output slots.

202 response JSON
{
  "object": "generation",
  "id": "string",
  "initiated_by": null,
  "status": null,
  "output_summary": null,
  "outputs": [],
  "credits": null,
  "created_at": "2026-08-16T10:00:00.000Z",
  "updated_at": "2026-08-16T10:00:00.000Z",
  "started_at": "2026-08-16T10:00:00.000Z",
  "finished_at": "2026-08-16T10:00:00.000Z",
  "workflow": "product_photography",
  "request": null
}