Pixelforge API reference

Pixelforge generates images from text prompts. Generation is asynchronous: you create a generation, it is queued and processed in the background, and you learn the result from a webhook or by polling. Every request and response body is JSON.

Base URL:

Base URL
https://pixelforge.promptedit.dev/v1
  • Timestamps are ISO 8601 strings in UTC, e.g. 2026-11-04T14:03:11Z.
  • Ids are prefixed by type: gen_ for generations, evt_ for webhook events.
  • Money is always an exact decimal string in US dollars, never a float, e.g. "0.046125000".

Pixelforge is simulated

There is no real model. Images are abstract placeholders at the requested size, captioned with your prompt, and nothing costs real money. Everything else (pricing, limits, timing, failures and webhooks) behaves like a real provider.

Authentication

Authenticate with an API key, sent as a bearer token. Sign up, then create a key in the dashboard. A key is shown once, when you create it, so store it somewhere safe. Revoked keys stop working immediately.

bash
curl https://pixelforge.promptedit.dev/v1/generations/gen_01JC2Z8Q4N6V0M3H7X5T9R2B1K \
  -H "Authorization: Bearer $PIXELFORGE_API_KEY"

Requests without a valid key get 401 with the code invalid_api_key.

Errors

Errors use conventional HTTP status codes and always have the same shape. Validation errors also name the first invalid parameter (param) and list every problem (errors).

json
{
  "error": {
    "type": "invalid_request_error",
    "code": "validation_failed",
    "message": "The selected size is invalid.",
    "param": "size",
    "errors": {
      "size": ["The size must be one of: 1024x1024, 1536x1024, 1024x1536."]
    }
  }
}
CodeStatusMeaning
invalid_api_key401The API key is missing, malformed or revoked.
validation_failed422A parameter is missing or invalid. Unknown parameters are rejected, not ignored. See param and errors.
content_policy_violation400The prompt was rejected. Nothing was created or billed.
not_found404No such resource, or it belongs to another account (you can't tell which).
idempotency_key_reused409The Idempotency-Key was already used with different parameters.
rate_limit_exceeded429Too many requests this minute. Wait for the seconds in the Retry-After header.
concurrency_limit_exceeded429Too many generations queued or processing. Retry-After: 5. Retry once one finishes.
daily_limit_exceeded429The daily generation quota (UTC) is used up.
overloaded503Pixelforge is temporarily overloaded. Retry-After: 2. Nothing was created; retry, ideally with an Idempotency-Key.
date_range_too_long422The usage date range covers too many days.

Rate limits and quotas

Limits apply per account, across all of its API keys.

LimitValueWhen exceeded
Requests60 per minute 429 rate_limit_exceeded, with Retry-After
Concurrent generations3 queued or processing at once 429 concurrency_limit_exceeded, with Retry-After
Generations per day500 per UTC day 429 daily_limit_exceeded

Rejected requests create nothing and cost nothing. Respect the Retry-After header (seconds) before retrying.

Idempotency

Networks fail. To retry POST /v1/generations safely, send an Idempotency-Key header (any unique string up to 255 characters, such as a UUID or your own order id).

  • Same key and same parameters: you get the original generation back with the header Idempotent-Replayed: true. No new generation is created, nothing extra is billed, and the request doesn't count against the concurrency or daily limits.
  • Same key with different parameters: 409 idempotency_key_reused.
  • Keys are scoped to your account and never expire.

Create a generation

POST/v1/generations
ParameterTypeDescription
promptstring, required What to generate. At most 4,000 characters.
sizestring1024x1024 (default), 1536x1024 or 1024x1536.
qualitystringlow, medium (default) or high.
reference_imagesarray of URLs Optional http(s) image URLs to guide the style, at most 4. Pixelforge doesn't download them, but each is billed as 765 input image tokens.
metadataobject Optional string keys to string values (at most 20 keys, values up to 500 characters). Returned on the generation and in every webhook, so you can match them to your own records.

Any other parameter is rejected with 422 validation_failed, so a typo can't silently fall back to a default.

bash
curl https://pixelforge.promptedit.dev/v1/generations \
  -H "Authorization: Bearer $PIXELFORGE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2a9e-3b7d-4e0a-9c55-2d8f1b4e7a10" \
  -d '{
    "prompt": "A lighthouse on a cliff at dawn, oil painting",
    "size": "1536x1024",
    "quality": "high",
    "reference_images": ["https://example.com/style.png"],
    "metadata": { "order_id": "1042" }
  }'

Returns 202 Accepted with the generation object, in the queued status.

json
{
  "id": "gen_01JC2Z8Q4N6V0M3H7X5T9R2B1K",
  "object": "generation",
  "status": "queued",
  "prompt": "A lighthouse on a cliff at dawn, oil painting",
  "size": "1536x1024",
  "quality": "high",
  "reference_images": ["https://example.com/style.png"],
  "metadata": { "order_id": "1042" },
  "image_url": null,
  "image_url_expires_at": null,
  "usage": null,
  "error": null,
  "created_at": "2026-11-04T14:03:11Z",
  "started_at": null,
  "completed_at": null
}

Retrieve a generation

GET/v1/generations/{id}

Returns the generation object. Generations belonging to other accounts return 404 not_found.

List generations

GET/v1/generations

Your generations, newest first, with cursor pagination.

Query parameterDescription
limit1 to 100, default 20.
status Only generations with this status: queued, processing, succeeded or failed.
cursor The next_cursor from the previous page.
bash
curl "https://pixelforge.promptedit.dev/v1/generations?limit=2&status=succeeded" \
  -H "Authorization: Bearer $PIXELFORGE_API_KEY"

{
  "object": "list",
  "data": [ { "id": "gen_…", "object": "generation", … }, … ],
  "has_more": true,
  "next_cursor": "eyJjcmVhdGVkX2F0Ijoi…"
}

The generation object

FieldDescription
idUnique id, prefixed gen_.
objectAlways generation.
statusqueued, processing, succeeded or failed.
prompt, size, quality, reference_images, metadata As requested, with defaults filled in. metadata is always an object (empty if none was sent).
image_url Signed PNG URL once succeeded, otherwise null. See Images.
image_url_expires_at When image_url stops working.
usage Null until succeeded, then input_text_tokens, input_image_tokens, output_tokens, reasoning_tokens and total_tokens.
error Null unless failed, then code and message.
created_at, started_at, completed_at When it was accepted, started processing and finished (succeeded or failed).

Lifecycle

queued → processing → succeeded or failed. A generation typically waits a few seconds in the queue, then processes for 5 to 30 seconds. succeeded and failed are final.

A succeeded generation
{
  "id": "gen_01JC2Z8Q4N6V0M3H7X5T9R2B1K",
  "object": "generation",
  "status": "succeeded",
  "prompt": "A lighthouse on a cliff at dawn, oil painting",
  "size": "1536x1024",
  "quality": "high",
  "reference_images": ["https://example.com/style.png"],
  "metadata": { "order_id": "1042" },
  "image_url": "https://pixelforge.promptedit.dev/images/gen_01JC2Z8Q4N6V0M3H7X5T9R2B1K.png?expires=1762351409&signature=…",
  "image_url_expires_at": "2026-11-05T14:03:29Z",
  "usage": {
    "input_text_tokens": 12,
    "input_image_tokens": 765,
    "output_tokens": 6450,
    "reasoning_tokens": 3184,
    "total_tokens": 10411
  },
  "error": null,
  "created_at": "2026-11-04T14:03:11Z",
  "started_at": "2026-11-04T14:03:13Z",
  "completed_at": "2026-11-04T14:03:29Z"
}

Failed generations are never billed. The error code is generation_failed (the model couldn't produce an image for the prompt) or internal_error (something went wrong on our side; retrying usually works).

A failed generation
"error": {
  "code": "internal_error",
  "message": "Something went wrong on our side while generating. You were not charged; please retry."
}

The API never returns a price

Responses and webhooks report token usage only. Work out the cost yourself from usage and the pricing.

Pricing and token usage

You pay per token, at a different rate for each kind of token. Only succeeded generations are billed.

Token typePrice per 1M tokens
Input text tokens$5.00
Input image tokens$10.00
Output tokens$40.00
Reasoning tokens$12.50

Each generation is billed at the rates in force when it was created. Rates can change, so read them from the pricing endpoint rather than hardcoding them. It needs no authentication.

GET/v1/pricing
bash
curl https://pixelforge.promptedit.dev/v1/pricing

{
  "object": "pricing",
  "currency": "usd",
  "unit": "per_1m_tokens",
  "rates": {
    "input_text_tokens": "5.00",
    "input_image_tokens": "10.00",
    "output_tokens": "40.00",
    "reasoning_tokens": "12.50"
  },
  "tokens": { "characters_per_text_token": 4, … }
}

How tokens are counted

  • Input text tokens: about one token per 4 characters of prompt, so estimate ceil(characters / 4). The real count is within 15% of that estimate in either direction, and is only known once the generation completes.
  • Input image tokens: exactly 765 per reference image.
  • Output tokens: exact, by quality and size:
Quality1024x10241536x10241024x1536
low280420420
medium1,1001,6501,650
high4,3006,4506,450
  • Reasoning tokens: the model thinks before drawing, and how much varies per generation. The count is only known once the generation completes, but always falls within a range set by quality:
QualityMinimumMaximum
low0500
medium2002,000
high1,0008,000

Worked example: the most a generation can cost

A high quality 1536x1024 image with a 400-character prompt and 2 reference images, assuming the most text and reasoning tokens possible:

TokensCount× price per 1MCost
Input text 400 / 4 = 100 estimated, up to +15%115$5.000.000575000
Input image 2 × 7651,530$10.000.015300000
Output high, 1536x1024 (exact)6,450$40.000.258000000
Reasoning maximum for high quality8,000$12.500.100000000
Maximum total0.373875000 ($0.373875)

Costs are exact: one token at $1.00 per 1M is $0.000001, and amounts are never rounded. Floating-point arithmetic will drift; use integers (for example nanodollars) or decimals.

Usage

GET/v1/usage

What Pixelforge billed you, per UTC day. This is the source of truth for your provider costs, so reconcile your own records against it.

  • start_date and end_date: YYYY-MM-DD in UTC, inclusive. Default: the last 30 days, ending today. At most 92 days.
  • A generation is billed on the UTC day it completed, not the day it was created.
  • cost_usd is an exact, unrounded decimal string with nine decimal places.
  • Days without usage are included, with zeros.
bash
curl "https://pixelforge.promptedit.dev/v1/usage?start_date=2026-11-01&end_date=2026-11-02" \
  -H "Authorization: Bearer $PIXELFORGE_API_KEY"

{
  "object": "usage",
  "start_date": "2026-11-01",
  "end_date": "2026-11-02",
  "data": [
    {
      "date": "2026-11-01",
      "generations": 3,
      "input_text_tokens": 41,
      "input_image_tokens": 0,
      "output_tokens": 3300,
      "reasoning_tokens": 2950,
      "cost_usd": "0.169080000"
    },
    {
      "date": "2026-11-02",
      "generations": 0,
      "input_text_tokens": 0,
      "input_image_tokens": 0,
      "output_tokens": 0,
      "reasoning_tokens": 0,
      "cost_usd": "0.000000000"
    }
  ],
  "total": {
    "generations": 3,
    "input_text_tokens": 41,
    "input_image_tokens": 0,
    "output_tokens": 3300,
    "reasoning_tokens": 2950,
    "cost_usd": "0.169080000"
  }
}

Webhooks

Pixelforge sends an HTTP POST to your webhook URL whenever a generation changes status. Set the URL and copy your signing secret in the dashboard.

EventSent when
generation.processingA generation leaves the queue.
generation.succeeded A generation finished, with usage and image_url.
generation.failed A generation failed, with error.
ping You pressed "Send test event" in the dashboard.

The body is an event whose data.object is the full generation object at the moment the event happened.

json
{
  "id": "evt_01JC2Z9B7K2M4P6R8T0V3X5Z7A",
  "object": "event",
  "type": "generation.succeeded",
  "created_at": "2026-11-04T14:03:29Z",
  "data": {
    "object": {
      "id": "gen_01JC2Z8Q4N6V0M3H7X5T9R2B1K",
      "object": "generation",
      "status": "succeeded",
      …the full generation object…
    }
  }
}
Headers
POST /webhooks/pixelforge HTTP/1.1
Content-Type: application/json
User-Agent: Pixelforge-Webhooks/1.0
Pixelforge-Event-Id: evt_01JC2Z9B7K2M4P6R8T0V3X5Z7A
Pixelforge-Event-Type: generation.succeeded
Pixelforge-Delivery-Attempt: 1
Pixelforge-Signature: t=1762265009,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Verifying signatures

Anyone can POST to your URL, so check every request. The Pixelforge-Signature header is t=<unix timestamp>,v1=<signature>, where the signature is the hex HMAC-SHA256 of <timestamp>.<raw request body>, keyed with your signing secret (whsec_…). Compare in constant time, and reject old timestamps to stop replays. Each attempt is signed with a fresh timestamp.

Laravel example
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class PixelforgeWebhookController
{
    public function __invoke(Request $request): Response
    {
        // Verify against the raw body, before any JSON decoding.
        $payload = $request->getContent();
        $signature = [];

        foreach (explode(',', (string) $request->header('Pixelforge-Signature')) as $part) {
            [$key, $value] = array_pad(explode('=', $part, 2), 2, '');
            $signature[$key] = $value;
        }

        $timestamp = (int) ($signature['t'] ?? 0);
        $expected = hash_hmac('sha256', "{$timestamp}.{$payload}", config('services.pixelforge.webhook_secret'));

        if (! hash_equals($expected, $signature['v1'] ?? '') || abs(time() - $timestamp) > 300) {
            abort(400, 'Invalid Pixelforge signature.');
        }

        $event = json_decode($payload, true);

        // Deduplicate by $event['id'] before acting on the event.

        return response()->noContent();
    }
}

Register the route outside the web middleware group (or exclude it from CSRF protection), since Pixelforge has no CSRF token.

Responding and retries

Any 2xx response within 10 seconds counts as delivered. Anything else (another status, a timeout, a connection error) is retried after 10s, 30s, 2m, 10m, 15m: up to 6 attempts in total. Redirects are not followed. Respond quickly and do slow work in a queued job.

Delivery is at least once, in any order

  • The same event can arrive more than once. Deduplicate by the event id.
  • Events can arrive out of order. A retried generation.processing can arrive after generation.succeeded. Go by the generation's status and never move a generation back from succeeded or failed.
  • A webhook can arrive before your POST /v1/generations request has returned.
  • Delivery isn't guaranteed. If every attempt fails, you can still poll GET /v1/generations/{id}.

Local development

Webhook URLs must be publicly reachable http or https URLs. Addresses on private or loopback networks (localhost, 127.0.0.1, 10.x, ...) are rejected. While developing, expose your app with a tunnel such as Expose, ngrok or Cloudflare Tunnel.

The dashboard shows every delivery attempt with the response your endpoint returned, and lets you resend any event.

Images

A succeeded generation's image_url points to a PNG at the requested size. The URL is signed and stops working 24 hours after the generation completed (image_url_expires_at). Download the file and store it yourself if you need it for longer. You can fetch a fresh URL from GET /v1/generations/{id}, but it expires at the same time.

Testing

Put any of these keywords anywhere in a prompt to trigger a behaviour on purpose. They are case-insensitive and still count toward the prompt's text tokens.

KeywordBehaviour
[blocked]Rejected at submission with 400 content_policy_violation. Nothing is created or billed.
[overloaded]Rejected at submission with 503 overloaded. Nothing is created or billed.
[fail]Accepted, then fails while processing (generation.failed). Not billed.
[succeed]Accepted and never fails at random.
[slow]Takes 60 to 90 seconds to process instead of 5 to 30.
[duplicate]Every webhook event for the generation is delivered twice.

Without keywords, Pixelforge still misbehaves now and then, like a real provider: a small share of generations fail at random, a small share of create requests are rejected with 503 overloaded, and some webhook events are deliberately delivered twice.