<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ohan</title>
    <description>The latest articles on DEV Community by ohan (@kavel).</description>
    <link>https://dev.to/kavel</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3550527%2F83e86884-1e59-4bc8-9eee-198438edf04a.png</url>
      <title>DEV Community: ohan</title>
      <link>https://dev.to/kavel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kavel"/>
    <language>en</language>
    <item>
      <title>A free image-generation API with no key and no account — and the three failures that look like success</title>
      <dc:creator>ohan</dc:creator>
      <pubDate>Sun, 06 Sep 2026 10:53:43 +0000</pubDate>
      <link>https://dev.to/kavel/a-free-image-generation-api-with-no-key-and-no-account-and-the-three-failures-that-look-like-f06</link>
      <guid>https://dev.to/kavel/a-free-image-generation-api-with-no-key-and-no-account-and-the-three-failures-that-look-like-f06</guid>
      <description>&lt;h1&gt;
  
  
  A free image-generation API with no key and no account — and the three failures that look like success
&lt;/h1&gt;

&lt;p&gt;Every "free AI image API" tutorial starts the same way: create an account, verify a phone number, copy a key, put it in &lt;code&gt;.env&lt;/code&gt;. This one skips all of that. There is nothing to apply for — you make up an identifier string yourself and send it as a header, and that string &lt;em&gt;is&lt;/em&gt; your account.&lt;/p&gt;

&lt;p&gt;The honest limits, up front: output is 1K and watermarked, and you get a wallet of &lt;strong&gt;15 credits&lt;/strong&gt;. The three things you can spend them on cost &lt;strong&gt;5&lt;/strong&gt;, &lt;strong&gt;15&lt;/strong&gt; and &lt;strong&gt;40&lt;/strong&gt; — so one of them can never run at all, and when it refuses, it answers &lt;code&gt;HTTP 200&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That last part is why this post exists. Every failure mode on this endpoint is shaped like a success.&lt;/p&gt;

&lt;p&gt;Here's the whole client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;kavel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kavel&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;generate&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a paper crane on a windowsill at sunrise, soft light, minimal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No config file, no environment variable, no third-party dependency — standard library only. It returns a CDN URL. The package is on &lt;a href="https://pypi.org/project/kavel/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;; it wraps the anonymous tier of &lt;a href="https://www.kavel.ai/?utm_source=devto&amp;amp;utm_medium=article" rel="noopener noreferrer"&gt;Kavel&lt;/a&gt;, an online image and video studio.&lt;/p&gt;

&lt;p&gt;Both images in this post came out of it while writing. The first is text-to-image. The second is the first one fed back in, with a prompt that only asks for an overcast sky:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F28uzst1e4g89r1jhujwc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F28uzst1e4g89r1jhujwc.webp" alt="Text-to-image output: a paper crane on a windowsill at sunrise" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2s7dp3glzmvwq8wbp9ov.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2s7dp3glzmvwq8wbp9ov.webp" alt="The same image edited: the sky is now overcast, while the crane, the window frame and the scratches on the glass are unchanged" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The folds of the paper, the highlight on the frame, the scratches on the glass — all still there. Only the light changed. That turns out to be a useful thing to know about, and I'll come back to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Without the package: two endpoints
&lt;/h2&gt;

&lt;p&gt;The package is a shell around two HTTP calls, so any language can do this directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Submit&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://www.kavel.ai/api/ai/generate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'x-anon-id: any-unique-string-you-generate'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "provider": "kie",
    "mediaType": "image",
    "model": "kavel-image-v1",
    "scene": "text-to-image",
    "prompt": "a paper crane on a windowsill at sunrise",
    "options": {"aspect_ratio": "1:1"}
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real response, measured 2026-09-06:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2a04f52a-0da3-4629-8a3f-ecc5a507f34f"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"anon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"charged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"remaining"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"grant"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You invent the &lt;code&gt;x-anon-id&lt;/code&gt; yourself. Nothing issues it, nothing validates it, it maps to no account — it is just the key your wallet is stored under.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Poll&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://www.kavel.ai/api/ai/anon-query?taskId=&amp;lt;id&amp;gt;&amp;amp;provider=kie&amp;amp;mediaType=image"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'x-anon-id: the same string'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"images"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"https://cdn.kavel.ai/uploads/kie/image/af6c7cb1-....webp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"watermarked"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nl"&gt;"videos"&lt;/span&gt;&lt;span class="p"&gt;:[]}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those two runs took &lt;strong&gt;60s&lt;/strong&gt; and &lt;strong&gt;75s&lt;/strong&gt; respectively — one sample each, not an average. Poll every 10–15 seconds; a one-second loop buys nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the free tier actually covers
&lt;/h2&gt;

&lt;p&gt;Every row below is read out of a response like the ones above, not copied from a doc:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Credits&lt;/th&gt;
&lt;th&gt;Where the number comes from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Text-to-image&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kavel-image-v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"charged":5,"remaining":10,"grant":15}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Photo edit (image-to-image)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nano-banana-2-lite&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"charged":15,"remaining":0,"grant":15}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video, 480p / 6s&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kavel-video-v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;40&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"wall":true,"cost":40,"grant":15}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anonymous wallet&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;grant&lt;/code&gt; field above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-IP daily ceiling&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;30&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"wall":true,"reason":"anon_ip_daily"}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three consequences worth stating plainly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One edit empties the wallet.&lt;/strong&gt; A 15-credit grant against a 15-credit edit is not a coincidence — it is sized so a signed-out visitor gets exactly one complete edit. Budget accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Video cannot run, and it is not flaky.&lt;/strong&gt; The cheapest configuration I could request — 480p, 6 seconds — costs 40 credits against a 15-credit wallet, and every other setting is priced above it, so there is nothing cheaper to fall back to. Without knowing that, you will spend an hour assuming you got the request shape wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rotating the ID does not get you more.&lt;/strong&gt; I burned through three &lt;code&gt;x-anon-id&lt;/code&gt; values from one IP; after the 30th credit the &lt;em&gt;first&lt;/em&gt; call on a fresh ID was refused, with &lt;code&gt;reason&lt;/code&gt; switching from &lt;code&gt;anon_credits&lt;/code&gt; to &lt;code&gt;anon_ip_daily&lt;/code&gt;. Handle those two separately — the first one means this wallet is done, the second means this IP is done for today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three failures that look like success
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Running out of credit is an HTTP 200
&lt;/h3&gt;

&lt;p&gt;This is the expensive one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"wall"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"anon_credits"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"remaining"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"grant"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Status 200. &lt;code&gt;code&lt;/code&gt; is 0. &lt;code&gt;message&lt;/code&gt; is &lt;code&gt;ok&lt;/code&gt;.&lt;/strong&gt; Code that branches on the status code walks straight past this, reaches for &lt;code&gt;data.id&lt;/code&gt;, and gets &lt;code&gt;undefined&lt;/code&gt; one line later. On my first integration it surfaced as &lt;code&gt;No task id&lt;/code&gt; — an error message pointing at entirely the wrong thing.&lt;/p&gt;

&lt;p&gt;Check the field explicitly, before anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wall&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out of credit: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, this call needed &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. The edit field is &lt;code&gt;image_input&lt;/code&gt;, not &lt;code&gt;image_urls&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;I guessed &lt;code&gt;options.image_urls&lt;/code&gt; and got:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"a reference image is required for this edit"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It says the reference image is missing, and I had passed one — under a name the server never looked at. The correct shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"image_input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://.../photo.jpg"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern here is worth more than the fix: &lt;strong&gt;the error describes the outcome, not the cause&lt;/strong&gt;, so following it leads away from the problem. The Python package wraps this as &lt;code&gt;image_url&lt;/code&gt; and you never see the layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Task status is not monotonic
&lt;/h3&gt;

&lt;p&gt;The sequence I observed was &lt;code&gt;processing → pending → success&lt;/code&gt; — it went &lt;em&gt;backwards&lt;/em&gt; once. So a poll loop written as "anything other than &lt;code&gt;processing&lt;/code&gt; means we're done" will fire early. Treat only &lt;code&gt;success&lt;/code&gt; and &lt;code&gt;failed&lt;/code&gt; as terminal; keep waiting on everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into agents and workflows
&lt;/h2&gt;

&lt;p&gt;Because there is no key to distribute, this fits the places where you want other people to run something without sending them to a signup form first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MCP server&lt;/strong&gt; — ask Claude or Cursor for an image and it runs; nothing to configure. It's listed on &lt;a href="https://lobehub.com/mcp/hanshs474-kavel-mcp" rel="noopener noreferrer"&gt;LobeHub&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;n8n workflow&lt;/strong&gt; — a form trigger, two HTTP Request nodes for submit and poll, and a branch on &lt;code&gt;wall: true&lt;/code&gt; that returns a readable message instead of failing silently. &lt;a href="https://n8n.io/workflows/19009" rel="noopener noreferrer"&gt;The template&lt;/a&gt; imports and runs with no credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apify actor&lt;/strong&gt; — &lt;a href="https://apify.com/kavel/kavel-ai-image-generator" rel="noopener noreferrer"&gt;batch a list of prompts&lt;/a&gt;. One gotcha: credits are metered per &lt;code&gt;x-anon-id&lt;/code&gt;, so &lt;strong&gt;generate a fresh ID per prompt&lt;/strong&gt; or everything after the first one hits the wall.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of the three asks the person using it to register anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to those two images
&lt;/h2&gt;

&lt;p&gt;The prompt for the edit only said: change the sky to overcast, keep the crane and the windowsill exactly as they are. The frame highlight, the scratches on the glass, the folds — all preserved.&lt;/p&gt;

&lt;p&gt;Here's the rule that saves re-runs: &lt;strong&gt;a model preserves what it can still see, and invents what it can't.&lt;/strong&gt; So the leverage in an edit prompt is not describing the change in more detail, it's &lt;strong&gt;naming what must not change&lt;/strong&gt;. Same model, two outcomes: tell it to change the sky and it changes only the sky; hand it a blurred face and ask it to sharpen, and you get a sharp face that belongs to someone else — because the face was never in the input to begin with.&lt;/p&gt;

&lt;p&gt;I've only verified that on this one model, but the cause (a model can only preserve information that survived into the input) isn't specific to it. If you'd rather click than write code, the &lt;a href="https://www.kavel.ai/image/nano-banana-2-lite?utm_source=devto&amp;amp;utm_medium=article" rel="noopener noreferrer"&gt;nano-banana-2-lite page&lt;/a&gt; takes an upload directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, stated up front
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Signed out&lt;/th&gt;
&lt;th&gt;Signed in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Text-to-image&lt;/td&gt;
&lt;td&gt;✅ 1K, watermarked&lt;/td&gt;
&lt;td&gt;✅ no watermark, larger models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Photo edit&lt;/td&gt;
&lt;td&gt;✅ one run on the free grant&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video&lt;/td&gt;
&lt;td&gt;❌ 40 credits vs a 15-credit wallet&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refill&lt;/td&gt;
&lt;td&gt;per-browser, roughly 30 days&lt;/td&gt;
&lt;td&gt;per plan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As said at the top: anonymous output is 1K and watermarked. That's the shape of the free tier, not a defect.&lt;/p&gt;

&lt;p&gt;Signing up is free and starts you at &lt;strong&gt;40 credits&lt;/strong&gt; — eight text-to-image runs, or two edits, at the prices in the table above. There's also a daily check-in that adds more. I'm listing the unit costs so you can work out whether that's enough &lt;em&gt;before&lt;/em&gt; you register rather than after. Removing the watermark and reaching the larger models starts from the same place; &lt;a href="https://www.kavel.ai/pricing?utm_source=devto&amp;amp;utm_medium=article" rel="noopener noreferrer"&gt;pricing is here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Signed-out video on the site runs on a separate browser-side engine rather than this endpoint — the &lt;a href="https://www.kavel.ai/video?utm_source=devto&amp;amp;utm_medium=article" rel="noopener noreferrer"&gt;video tools&lt;/a&gt; work without an account too.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is this endpoint going to disappear?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's the public shape of the anonymous trial path on the site — the same one a signed-out visitor hits when they press Generate. It lives and dies with the free tier rather than being a temporary promo endpoint. The credit numbers will move with costs, but every response carries &lt;code&gt;grant&lt;/code&gt; and &lt;code&gt;charged&lt;/code&gt;, so &lt;strong&gt;read them instead of hardcoding them&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I rotate &lt;code&gt;x-anon-id&lt;/code&gt; on every call?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, nothing validates it. But rotating only escapes the per-browser wallet, not the 30-credit per-IP daily ceiling. Rotation is &lt;em&gt;required&lt;/em&gt; for batch work (without it everything after the first prompt walls), it just isn't a way to get unlimited credit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why isn't video exposed in the Python package?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because signed out it fails 100% of the time. A function that can never succeed is worse than a missing one — people assume they're holding it wrong. It's left out, and the README says why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do failed generations still cost credits?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In my testing the charge lands at submission — it's in the &lt;code&gt;charged&lt;/code&gt; field of the very first response. So get the prompt right the first time, particularly on the edit path, where one attempt is the entire wallet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use the output commercially?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Anonymous output is watermarked, so not directly. Watermark removal and licensing follow the signed-in plans.&lt;/p&gt;

&lt;h2&gt;
  
  
  One thing to take away
&lt;/h2&gt;

&lt;p&gt;If you keep a single line from this: &lt;strong&gt;the failures are shaped like successes.&lt;/strong&gt; Running out of credit returns &lt;code&gt;HTTP 200&lt;/code&gt; with &lt;code&gt;"code":0&lt;/code&gt; and &lt;code&gt;"message":"ok"&lt;/code&gt;, and a single boolean buried in &lt;code&gt;data.wall&lt;/code&gt; is the only thing telling you nothing happened. Write that check first, then write the rest.&lt;/p&gt;

&lt;p&gt;If you'd rather not write any of it, &lt;a href="https://www.kavel.ai/image?utm_source=devto&amp;amp;utm_medium=article" rel="noopener noreferrer"&gt;kavel.ai/image&lt;/a&gt; runs in the browser — no account there either.&lt;/p&gt;

&lt;p&gt;All numbers measured 2026-09-06.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>python</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
