Build with nocensor.ai
REST API for image generation, video, face swap, enhance, and multi-stage pipelines. Scoped API key auth, async job polling, webhooks — same credit system as the UI.
API key auth
Bearer token with per-key rate limits. Max 5 keys per account.
Async jobs
202 Accepted + polling. No blocking requests. Signed URLs when complete.
Same credit system
Credits deducted on submission. Failed jobs auto-refunded by cron.
Prompt safety
Same content filter as the UI. CSAM blocked at all tiers.
Authentication
All endpoints (except /api/v1/health) require a Bearer token:
Authorization: Bearer nc_live_<your_key>
API keys are generated in Settings → API Keys. Keys are shown once at creation — store them securely.
Each key carries one or more scopes. Using a key on an endpoint outside its scope returns 403 INSUFFICIENT_SCOPE.
| Scope | Grants access to |
|---|---|
| generation | POST /generate, /video, /face-swap, /enhance, /pipelines, /undress, /loras, /loras/{id}/train, /voice-calls |
| mgmt | GET /jobs, /jobs/{id}, /account, /credits, /payments, /models, /characters, /pipelines/{id}; DELETE /jobs/{id} |
| webhooks | All /webhooks/* endpoints |
| affiliate | GET /affiliate/summary, /affiliate/daily, /affiliate/clicks, /affiliate/signups, /affiliate/sales (your own affiliate stats, as JSON or CSV) |
API Reference
Full OpenAPI 3.1 spec available at /openapi.yaml.
Generation
/api/v1/generateGenerate an image
/api/v1/videoGenerate a video
/api/v1/undressAI undress
/api/v1/face-swapSwap faces in an image
/api/v1/enhanceEnhance / upscale an image
Pipelines
/api/v1/pipelinesSubmit a multi-stage image pipeline
Add `?dry_run=true` to get a cost breakdown (200) without submitting the job (202).
/api/v1/pipelines/{id}Get pipeline status
Jobs
/api/v1/jobsList generation jobs
/api/v1/jobs/{id}Get job status / result
/api/v1/jobs/{id}Cancel a pending job
/api/v1/jobs/{id}/eventsStream live job events (Server-Sent Events)
Events: `snapshot` (job detail) on connect; `status`; `progress` `{step,total,percent,stage:{key,label}}` (best-effort); terminal `completed` | `failed` | `cancelled` then close; `reconnect` at ~55s. Resume with `Last-Event-ID`. Authenticate with a Bearer key, or with `?token=` from `events_url` (browser-safe, 10 min, one job).
Webhooks
/api/v1/webhooksList webhooks
/api/v1/webhooksCreate a webhook
The `secret` field is only returned at creation time.
/api/v1/webhooks/{id}Get webhook details
/api/v1/webhooks/{id}Update a webhook
/api/v1/webhooks/{id}Delete a webhook
/api/v1/webhooks/{id}/deliveriesList webhook delivery attempts
/api/v1/webhooks/{id}/testSend a test delivery
Characters
/api/v1/voice-callsOpen a voice call with a character
Finds or creates your companion session with a character and returns `call_url`, the link that opens the live voice call. The call runs in the browser of the account that owns the API key, which must be signed in to nocensor.ai. It is billed per minute from that account (`credit_per_minute`) and needs at least `min_credits_to_start` credits. This endpoint charges nothing itself. Phone-number bridging is not available yet.
/api/v1/lorasList your trained characters
Returns every character (LoRA) on your account, newest first, with the `id` that generation requests take in `loras[]` and that `POST /api/v1/loras/{id}/train` and `POST /api/v1/loras/{id}/download` take in the path. Only `ready` characters can generate or be downloaded; `pending` drafts can still be trained and `failed` ones report why training did not complete through their job. Deleted characters are not listed. Free; nothing is charged.
/api/v1/lorasCreate a character training draft and get photo upload URLs
Step 1 of character (LoRA) training. Creates a pending character and returns one presigned S3 URL per photo. PUT each photo to its `url` with exactly the declared `size` in bytes and the given `Content-Type` header, and NO `Authorization` header, within `expires_in` seconds. Then call `POST /api/v1/loras/{id}/train`. Repeating this call with the same `name` re-arms the same untrained draft with fresh URLs. Nothing is charged here.
/api/v1/loras/{id}/trainStart training a character from its uploaded photos
Step 2 of character training. Verifies every photo from step 1 finished uploading, charges the tier chosen at creation, and queues training. The body must be empty or `{}` — name and tier are fixed at creation. Track it with `GET /api/v1/jobs/{id}`; once the job completes, use the character id in `loras[]` on generation requests. Training a character again after it was accepted returns `LORA_NOT_TRAINABLE`, never a second charge.
/api/v1/loras/{id}/downloadDownload your trained character file (.safetensors)
Returns a signed download URL, valid for `expires_in_seconds`, for one of YOUR ready characters, plus the trigger word to add to prompts and the base model it was trained on. Every call must attest `attest_real_person_consent: true` (the character is your own likeness, or an adult who consented to it) and is recorded against your account. The file inherits the licence of the base model it was trained on (`license_url`): it binds you and anyone you share the file with. When that licence requires a notice to travel with the file (Ultra characters, Krea 2 Community License), `license_notice` carries its text: keep it beside the file as NOTICE.txt; otherwise it is null. Free; nothing is charged. Characters trained before downloads existed return `LORA_NOT_EXPORTABLE`.
Account
/api/v1/accountGet account details
/api/v1/creditsGet credit balance
/api/v1/paymentsList payment history
/api/v1/charactersList available characters
/api/v1/modelsList available models
Affiliate stats
/api/v1/affiliate/summaryAffiliate totals and balances
Lifetime and last-30-day clicks, signups, sales and commission, plus commission balances by status. Equals the sum of `/daily` rows for the same window. Requires an API key with the `affiliate` scope. Dates are UTC. `format=csv` returns the same data as CSV; errors are always JSON.
/api/v1/affiliate/dailyAffiliate totals per UTC day
One row per UTC day (and per placement tag with `group_by=day_sub`). Sales are bucketed by sale date; commission status is as of the read. Click columns are null before `meta.clicks_tracked_since`. Requires an API key with the `affiliate` scope. Dates are UTC. `format=csv` returns the same data as CSV; errors are always JSON.
/api/v1/affiliate/clicksAffiliate link presses
Every press of your link, newest first, with placement tag, utm values, your `click_id` and a bot flag. `counted=false` presses repeat a visitor within the dedupe window. Window max 92 days. Requires an API key with the `affiliate` scope. Dates are UTC. `format=csv` returns the same data as CSV; errors are always JSON.
/api/v1/affiliate/signupsReferred signups
One row per referred account, by UTC signup date. No buyer identity and no time of day. Requires an API key with the `affiliate` scope. Dates are UTC. `format=csv` returns the same data as CSV; errors are always JSON.
/api/v1/affiliate/salesReferred sales and commissions
One row per commission, by UTC sale date, with gross, rate, commission, status and payout. No buyer identity and no time of day. Requires an API key with the `affiliate` scope. Dates are UTC. `format=csv` returns the same data as CSV; errors are always JSON.
System
/api/v1/healthHealth check
Quick start
Use the official TypeScript SDK (@nocensor/sdk) for end-to-end type safety, retries, and polling — or hit the REST API directly with curl.
SDK (Node, browsers, Deno, Bun, Workers)
npm install @nocensor/sdk
import { NoCensor } from '@nocensor/sdk'
const nc = new NoCensor({ apiKey: process.env.NOCENSOR_API_KEY })
const job = await nc.generate.createAndWait({
prompt: 'beautiful woman, photorealistic',
model: 'realistic',
})
console.log(job.outputs[0].url)1. Submit a job with curl (202 Accepted)
curl -X POST https://nocensor.ai/api/v1/generate \
-H "Authorization: Bearer nc_live_<your_key>" \
-H "Content-Type: application/json" \
-d '{"prompt": "beautiful woman, photorealistic", "model": "realistic"}'
# 202 Accepted
# {
# "data": { "id": "job_...", "status": "pending" },
# "meta": { "poll_url": "/api/v1/jobs/job_...", "credits_remaining": 490 }
# }2. Poll until complete
curl https://nocensor.ai/api/v1/jobs/<job_id> \
-H "Authorization: Bearer nc_live_<your_key>"
# 200 OK — when done:
# {
# "data": {
# "id": "job_...", "status": "completed",
# "outputs": [{ "url": "https://...", "media_type": "image", "dimensions": { "width": 832, "height": 1216 }, "expires_at": "..." }]
# }
# }Rate limits
| Limit | Value | Scope |
|---|---|---|
| Generation | 10 rpm | POST /generate, /video, /face-swap, /enhance, /pipelines, /undress, /loras, /loras/{id}/train |
| Management | 60 rpm | GET /jobs, /jobs/{id}, /account, /credits, /payments, /models, /characters, /pipelines/{id}; DELETE /jobs/{id}; POST /voice-calls |
| Webhooks | 10 rpm | All /webhooks/* endpoints |
| Active keys | 5 max | Per account |
All responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. Rate-limited responses include a Retry-After header.
Error codes
| Code | HTTP | Trigger |
|---|---|---|
| UNAUTHORIZED | 401 | Missing, invalid, or revoked API key |
| INVALID_EVENTS_TOKEN | 401 | /jobs/{id}/events `token` is invalid, expired, or issued for another job |
| FORBIDDEN | 403 | Account suspended or key missing required scope |
| INSUFFICIENT_SCOPE | 403 | API key lacks the required scope for this endpoint |
| NOT_AN_AFFILIATE | 403 | Affiliate stats endpoint called with a key whose account has no affiliate profile |
| AFFILIATE_SUSPENDED | 403 | Affiliate stats endpoint called for a suspended affiliate account |
| INSUFFICIENT_CREDITS | 402 | Not enough credits for the workflow, or below the voice-call minimum on /voice-calls |
| PURCHASE_REQUIRED | 402 | Endpoint requires a prior real purchase (e.g. /undress) |
| VIDEO_REQUIRES_PAID_TIER | 402 | /video — paid tier required; route the user to the /credits upsell page |
| RATE_LIMITED | 429 | Per-key or per-user rate limit exceeded |
| VALIDATION_ERROR | 422 | Request failed schema validation — `details[]` lists every failing field |
| INVALID_REQUEST | 400 | Malformed or unparseable JSON body |
| INVALID_IDEMPOTENCY_KEY | 400 | Idempotency-Key is not 1-255 printable ASCII characters |
| INVALID_INPUT | 400 | Invalid or missing request field |
| INVALID_WORKFLOW | 400 | Unknown model or workflow id in the request |
| UNKNOWN_WORKFLOW | 400 | Workflow id not registered at dispatch time |
| PROMPT_BLOCKED | 400 | Content policy filter triggered |
| PROMPT_REJECTED | 400 | Content policy filter triggered (enhance operations) |
| CONTENT_POLICY_VIOLATION | 400 | Source image rejected by the image safety check |
| CONSENT_REQUIRED | 400 | /video img2video submitted without biometricConsent: true or realPersonConsent: true, or /face-swap without realPersonConsent: true |
| PAYLOAD_TOO_LARGE | 413 | Image exceeds 3.5MB |
| SOURCE_IMAGE_UNREACHABLE | 422 | /video i2v image URL could not be fetched — a nocensor output URL expires about an hour after it is issued, so re-read it from GET /api/v1/jobs/{id} or send inline base64 |
| NOT_FOUND | 404 | Job does not exist or not owned by caller |
| SOURCE_NOT_FOUND | 404 | job_<uuid> source reference not owned by caller or has no output |
| PIPELINE_NOT_FOUND | 404 | Pipeline correlation_id has no jobs owned by caller |
| LORA_NOT_FOUND | 404 | LoRA id in `loras[]` not owned by caller, or a /loras/{id}/train or /voice-calls id that is not your character |
| JOB_NOT_CANCELLABLE | 409 | DELETE on a job already completed, failed, or cancelled |
| JOB_FAILED | 200 | /jobs/{id}/events `failed` event data.code — the job itself failed, not the stream |
| IDEMPOTENCY_KEY_REUSED | 409 | Idempotency-Key reused with a different request body |
| IDEMPOTENCY_IN_PROGRESS | 409 | A request with this Idempotency-Key is still running — retry after Retry-After |
| LORA_NOT_READY | 409 | LoRA is soft-deleted, still training, failed, or missing output |
| LORA_LIMIT_REACHED | 409 | POST /loras when the account already holds the maximum number of characters |
| LORA_NOT_TRAINABLE | 409 | POST /loras/{id}/train on a character already submitted, training, ready, or failed |
| LORA_UPLOAD_INCOMPLETE | 409 | POST /loras/{id}/train before every photo finished uploading — the draft is retired; create it again |
| LORA_NOT_EXPORTABLE | 409 | POST /loras/{id}/download on a character trained before downloads existed — retrain it to export |
| ATTESTATION_REQUIRED | 400 | POST /loras/{id}/download without the per-export real-person attestation (a missing or false attest_real_person_consent body field returns 422) |
| LORA_NOT_OWNED | 403 | LoRA id in a pipeline stage not owned by caller |
| LORA_INCOMPATIBLE | 400 | LoRA base model doesn't match the target model/endpoint |
| CHARACTER_INCOMPATIBLE | 400 | characterId is not available on the requested model (no charge) |
| TOO_MANY_LORAS | 400 | More loras[] than the model allows (max 2 for images, max 1 for video) |
| PIPELINE_TOO_MANY_STAGES | 400 | Pipeline exceeds the 5-stage max |
| PIPELINE_INVALID_STAGE_ORDER | 400 | Image stage placed after a video-producing stage |
| CHARACTER_LOCKED | 403 | Character has not been unlocked by this account |
| REGION_BLOCKED | 451 | Feature unavailable in the caller's region (e.g. AI undress in banned US states, or restricted countries) |
| WEBHOOK_URL_INVALID | 400 | Webhook URL failed validation |
| WEBHOOK_LIMIT_REACHED | 409 | Maximum webhooks per account reached |
| WEBHOOK_INACTIVE | 409 | Webhook is disabled — reactivate before test or deliveries |
| GPU_UNAVAILABLE | 503 | All GPU workers throttled — retry shortly |
| IDEMPOTENCY_UNAVAILABLE | 503 | Idempotency check temporarily unavailable — retry |
| AUTH_UNAVAILABLE | 503 | API key check could not complete — your key was not judged; retry after the Retry-After header (never rotate the key for this) |
| TIMEOUT | 503 | Dispatch timed out while the service was busy — retry |
| AUDIO_UNAVAILABLE | 503 | /video with sound or /voice-calls while the voice provider is down — retry later (for /video, or set audioMode to "none") |
| FEATURE_UNAVAILABLE | 503 | Endpoint or model restricted to admin keys (v1/undress, video model wan-21) — use the alternative named in the message |
| STORAGE_ERROR | 500 | Transient storage failure signing a source URL — retry |
| DB_ERROR | 500 | Transient database error — retry |
| INTERNAL_ERROR | 500 | Unexpected server error — retry |
Ready to build?
Create an account, get your API key, and start generating.
Get started free