PsyDox API for Developers

An OpenAI-compatible API for product images and listings: chat, image generation and image checks, billed from your PsyDox wallet.

Base URL https://api.psydox.in/v1 · create a key in Settings → API keys (private beta) · openapi.json

Quickstart

Send your key as a Bearer token. Keys starting pdx_test_ return fixed sample answers at ₹0 — use them while you build.

curl

curl https://api.psydox.in/v1/chat/completions \
  -H "Authorization: Bearer $PSYDOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "psydox-spark-1",
       "messages": [{"role": "user", "content": "Write a title for a white cotton kurta"}]}'

Python (official openai SDK)

# pip install openai
from openai import OpenAI

client = OpenAI(api_key="pdx_live_...", base_url="https://api.psydox.in/v1")

reply = client.chat.completions.create(
    model="psydox-spark-1",
    messages=[{"role": "user", "content": "Write a title for a white cotton kurta"}],
)
print(reply.choices[0].message.content)

image = client.images.generate(model="psydox-prism-1", prompt="white sneaker on a white background", n=1)
print(image.data[0].url)

JavaScript / TypeScript (official openai SDK)

// npm install openai
import OpenAI from "openai";

const client = new OpenAI({ apiKey: process.env.PSYDOX_API_KEY, baseURL: "https://api.psydox.in/v1" });

const stream = await client.chat.completions.create({
  model: "psydox-spark-1",
  messages: [{ role: "user", content: "Three bullet points for a steel water bottle" }],
  stream: true,
});
for await (const chunk of stream) process.stdout.write(chunk.choices[0]?.delta?.content ?? "");

Check a product photo

curl https://api.psydox.in/v1/vision/analyze \
  -H "Authorization: Bearer $PSYDOX_API_KEY" -H "Content-Type: application/json" \
  -d '{"model": "psydox-lens-1", "task": "qa", "marketplace": "amazon",
       "image_url": "https://example.com/product.jpg"}'

Models and prices

Prices are the same as in the app and come from your wallet. Images are charged only for images actually produced; a failed call costs nothing. PsyDox Spark chat is free.

Loading models…

Endpoints

  • POST /v1/chat/completions — Chat (OpenAI shape): messages, tools, stream, max_tokens. Scope: chat.
  • POST /v1/images/generations — psydox-prism-1: prompt, n (1–4), size, response_format url | b64_json, image_url to edit, webhook_url for async. Scope: images.
  • GET /v1/images/jobs/{id} — Status and result of an async image job. Scope: images.
  • POST /v1/vision/analyze — psydox-lens-1: image_url or file (base64), task qa | attributes | describe, marketplace. Scope: vision.
  • POST /v1/audio/transcriptions — psydox-echo-1: not available yet (501).
  • GET /v1/models — The models your key can use.
  • GET /v1/usage?month=YYYY-MM — Requests, tokens, images and charges by model and day — this key and your whole account. Scope: usage.

Limits, idempotency and request ids

  • Per key, per minute: chat 60, images 10, vision 30, reads 120. Over the limit → 429 with Retry-After.
  • At most 4 images per request and 20 in progress per account.
  • Optional monthly spend limit per key (Settings → API keys) → 402 key_spend_limit_reached before anything runs.
  • Send Idempotency-Key on any POST: a repeat within 24 h returns the first answer and is never charged twice.
  • Every response has X-Request-ID (send your own to correlate) and X-PsyDox-Charged-Paise.
  • Server-to-server only: browsers can't call the API directly (no CORS). Never put a key in front-end code.

Errors

Same shape as OpenAI: {"error": {"message", "type", "code", "param"}}.

HTTPcodeWhat to do
400invalid_request, invalid_messages, invalid_n, …Fix the request; param names the field.
401invalid_api_keyMissing, wrong or revoked key.
402insufficient_balance, key_spend_limit_reachedTop up (psydox.in/wallet) or raise the key's monthly limit. Nothing was charged.
403insufficient_scope, model_not_allowedThe key lacks the scope, or the model needs a paid account.
409 / 422idempotency_in_progress, idempotency_key_reusedSame Idempotency-Key still running, or reused with a different body.
429rate_limited, quota_exceeded, too_many_in_flightWait for Retry-After.
503ai_busy, model_unavailable, api_pausedRetry after Retry-After. Nothing was charged.

Webhooks (async images)

Add webhook_url (public https) to an image request to get 202 and a job id at once. When it finishes we POST image.completed or image.failed, signed with your key's webhook secret (shown once when the key is created): X-PsyDox-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "t." + raw body)>. Failed deliveries are retried 3 times (2 s, 8 s, 32 s). Verify before trusting a delivery:

import hashlib, hmac, time

def verify(secret: str, raw_body: bytes, header: str, tolerance=300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))   # "t=...,v1=..."
    ts = int(parts["t"])
    if abs(time.time() - ts) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

# verify("whsec_...", request.body, request.headers["X-PsyDox-Signature"])
import crypto from "node:crypto";

export function verify(secret, rawBody, header, tolerance = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const ts = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - ts) > tolerance) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${ts}.`).update(rawBody).digest("hex");
  return expected.length === (parts.v1 ?? "").length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}