Sign in to create a key. Everything below works the moment you have one.
Omegen API
Generate and describe images from your own code. The API spends the balance of the account the key belongs to, and only that one. An account opened in the Telegram bot and an account opened on the website are separate accounts with separate balances, even when the same person holds both.
Base URL: https://omegen.ai/api/v1
Authentication
Send the key as a bearer token:
Authorization: Bearer omg_xxxxxxxxxxxxxxxxxxxx
X-Api-Key: <key> works too, for clients that cannot set Authorization.
The key is shown once, when it is created, and stored only as a hash. A lost key is replaced, not recovered. Keep it on a server: it spends money, and it belongs in no browser and no repository.
Starting a job
POST /v1/generations
Content-Type: application/json
The response comes back immediately with an id and "status": "processing"; ask for the job again until the status changes. There is no callback, so nothing has to be reachable from the internet on your side.
Half of our jobs finish inside a minute and nine in ten inside five, measured on real traffic rather than estimated. The tail is long enough to matter: poll every 5 to 10 seconds and give up no sooner than 12 minutes, after which an unfinished job is cancelled and refunded on its own.
Generate an image
{ "action": "imagine", "prompt": "a red apple on a marble table --ar 16:9" }
Midjourney parameters go in the prompt itself, the same as anywhere else: --ar, --no, --s, --style, --sref, --oref and the rest. One job returns four images.
A prompt the model would refuse on sight is refused here first, with 422 invalid_prompt and a reason, before anything is charged:
reason | What it means |
|---|---|
link_only | The prompt is a picture and nothing else. A picture is a reference; add words saying what to make from it. |
no_words | Parameters, punctuation or nothing describable. Say what the picture is. |
too_short | Under 2 characters. |
too_long | Over 5900 characters. The model refuses anything over 6000, and we leave room for the version parameter we add. |
The list grows as more refusals are seen on the live service. A prompt that passes is not promised to succeed, only that we have no reason to say otherwise.
Describe an image
{ "action": "describe", "image_url": "https://example.com/photo.jpg" }
Or send the bytes instead, as base64, with or without a data URI header:
{ "action": "describe", "image_base64": "iVBORw0KGgo..." }
Either way the picture is copied onto our host first, so the job does not depend on your URL still being reachable when it runs. Maximum 12 MB of request body.
The result is four prompts describing the picture, in result.prompts. A describe returns no files.
Blend several images
{ "action": "blend", "image_urls": ["https://example.com/a.jpg", "https://example.com/b.jpg"] }
Two to five images, merged into one result. image_base64 takes a list here rather than a single string, and only one of the two fields at a time.
dimensions is square, portrait or landscape, and defaults to square.
The pictures are shrunk before they are sent on. Blend takes style and subject from a reference and nothing finer, and five full-size images were refused by the model's gateway where the same five smaller went through in half the time. Prefer image_urls: five base64 images can approach the 12 MB body limit.
Edit a picture
{ "action": "edit", "prompt": "make the sky green", "image_url": "https://example.com/photo.jpg" }
edit changes what is in the picture. retexture takes the same fields and changes how it looks while keeping the shapes.
To change one area rather than the whole picture, send a PNG with that area erased to transparency. The editor treats the hole as the part to fill.
Neither offers follow-up actions afterwards: the editor produces a different family of job, and the endpoints that upscale or vary a grid have never heard of one.
Repaint an area
{ "action": "vary_region", "parent_id": 9045, "prompt": "a brass lamp", "mask_base64": "iVBORw0KGgo..." }
The parent must be a finished single image, which in practice means an upscale. The mask is a PNG the same size as that picture, black everywhere except where the paint should go, which is white. Not a transparent hole: this is a different endpoint from the editor and wants a different thing.
The parent has a lifetime on the model's side that nobody downstream is told. When it has passed, the job comes back failed with job expired and the money returns. Trying is the only way to find out.
Act on a result
Anything an image unlocks is asked for the same way, naming the job it acts on:
{ "action": "upscale", "parent_id": 9045, "index": 3 }
result.actions on a finished job lists what it accepts and with which index — [1,2,3,4] for the four tiles of a grid, [0] for an action on the whole picture. Actions are upscale, variation, low_variation, high_variation, zoom_out_1_5, zoom_out_2, pan_left, pan_right, pan_up, pan_down and reroll. vary_region is reported beside them and is asked for differently, above: it needs a mask as well as the job.
GET /v1/actions returns the same list with prices, read from the price list the service bills from.
Each of them can be done once per tile. Asking twice is refused rather than charged for: the first result still stands.
Retrying safely
A generation is charged before the provider is called, so a request you sent but never saw the answer to is a request you may have paid for. Send an Idempotency-Key and a repeat of it is recognised instead of bought again:
POST /v1/generations
Idempotency-Key: 9f2c1b7a-order-4711
Any string up to 255 characters, unique per request on your side. A UUID or your own job id both work. Keys are remembered for 24 hours and are scoped to the key that sent them, so two of your servers can number requests independently.
What you get back depends on what the first attempt did:
| Situation | Answer |
|---|---|
| First time | 201 and the new job, as usual |
| The same key again, work finished | 200, the header Idempotent-Replayed: true, and the job as it stands now |
| The same key again, first attempt still running | 409 idempotency_in_progress. Wait a moment and ask again |
| The same key, different body | 422 idempotency_key_reused |
| First attempt failed before creating anything | The key is free. Fix the request and send it again with the same key |
The replay is deliberately the current state of the job, not a recording of the first response. By the time you retry, a job that was processing has usually finished, and that is the answer you came back for.
The header is optional and nothing else changes when you leave it out.
Reading a job
GET /v1/generations/{id}
{
"id": 9072,
"action": "upscale",
"status": "completed",
"prompt": "a red apple on a marble table --ar 16:9",
"price_usd": "0.020000",
"refunded": false,
"created_at": "2026-08-30T09:37:18Z",
"result": {
"files": [
{ "url": "https://omegen.ai/media/2026-08-30/9072/1.png", "type": "image" }
],
"prompts": [],
"actions": { "pan_left": [0], "reroll": [0] }
},
"error": null
}
status is one of pending, processing, completed, failed or expired. Only completed carries a result.
Every file says what kind it is: image, video, audio, or unknown when we cannot tell. Branch on type rather than on the file extension — image models are what exist today, and the field is there so the code you write now still works when they are not the only ones.
Image links expire. Files are kept for 30 days and then deleted. Download what you want to keep as soon as the job finishes.
Listing jobs
GET /v1/generations?limit=20&before_id=9000
Everything this key has run, newest first. Paged by id rather than by offset, because rows are only ever added and an id to page before is stable in a way an offset is not.
status narrows it, and takes a comma-separated list:
GET /v1/generations?status=pending,processing
This is the endpoint to poll. Watching five jobs one at a time is five requests every few seconds; asking for the open ones is one, whatever the number of jobs. An unknown status is refused rather than ignored, so a typo in a filter cannot leave you watching an empty list and believing it.
Statuses are pending, processing, completed, failed and expired.
Money
GET /v1/balance returns what is left.
GET /v1/usage is the one to reconcile against:
GET /v1/usage?from=2026-08-01&to=2026-08-31
{
"from": "2026-08-01",
"to": "2026-08-31",
"scope": "key",
"balance_usd": "12.500000",
"totals": {
"jobs": 142,
"refunded_jobs": 3,
"charged_usd": "9.940000",
"refunded_usd": "0.210000"
},
"by_action": [{ "action": "imagine", "jobs": 100, "charged_usd": "7.000000" }],
"by_day": [{ "day": "2026-08-01", "jobs": 12, "charged_usd": "0.840000" }]
}
Both dates are UTC and inclusive, the window defaults to the last 30 days and may not exceed 366. scope is key, which is the default, or account for every key on the account together.
Refunds are reported beside the charges rather than netted off them. A total that quietly nets them is one nobody can reconcile against the requests they actually made.
GET /v1/actions returns the current price list. Read it rather than hard-coding prices: they are edited from the admin panel, and this endpoint quotes the number your key is actually charged.
A job is charged when it is accepted and refunded automatically if it fails or times out. A refunded job reports "refunded": true, and the money is back before you see it.
Failures
Every response is JSON, including the failures:
{ "error": { "code": "insufficient_balance", "message": "Not enough balance for this request." } }
Branch on code, not on the message.
| Status | code | What to do |
|---|---|---|
| 401 | missing_key, invalid_key | Check the header. |
| 403 | key_revoked | The key was revoked. Ask for a new one. |
| 403 | account_blocked | The account is blocked. |
| 402 | insufficient_balance | Stop retrying and top up. required_usd and balance_usd say by how much. |
| 409 | too_many_active | Wait for a job to finish. limit says how many run at once. |
| 422 | invalid_request, missing_field, invalid_field, unknown_action, image_unreadable | The request needs changing. Retrying it will not help. |
| 422 | invalid_prompt | The prompt cannot work. reason says why, and nothing was charged. |
| 429 | rate_limited | Slow down. X-RateLimit-Remaining is on every response. |
| 502 | provider_error | The job never started and nothing was charged. Safe to retry. |
| 503 | maintenance | The service is being updated. Retry in a few minutes. |
| 500 | internal_error | Our fault. Safe to retry. |
A job that fails after it started is not an error response: it comes back as a job with "status": "failed", an error object saying what happened, and the money already returned.
A whole run, in shell
KEY=omg_xxxxxxxxxxxxxxxxxxxx
ID=$(curl -s -X POST https://omegen.ai/api/v1/generations \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"action":"describe","image_url":"https://example.com/photo.jpg"}' \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')
until curl -s -H "Authorization: Bearer $KEY" \
https://omegen.ai/api/v1/generations/$ID \
| tee /tmp/job.json | grep -q '"status": "\(completed\|failed\|expired\)"'; do
sleep 5
done
cat /tmp/job.json