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_reachedbefore anything runs. - Send
Idempotency-Keyon 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) andX-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"}}.
| HTTP | code | What to do |
|---|---|---|
| 400 | invalid_request, invalid_messages, invalid_n, … | Fix the request; param names the field. |
| 401 | invalid_api_key | Missing, wrong or revoked key. |
| 402 | insufficient_balance, key_spend_limit_reached | Top up (psydox.in/wallet) or raise the key's monthly limit. Nothing was charged. |
| 403 | insufficient_scope, model_not_allowed | The key lacks the scope, or the model needs a paid account. |
| 409 / 422 | idempotency_in_progress, idempotency_key_reused | Same Idempotency-Key still running, or reused with a different body. |
| 429 | rate_limited, quota_exceeded, too_many_in_flight | Wait for Retry-After. |
| 503 | ai_busy, model_unavailable, api_paused | Retry 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));
}