v1.0.0

API reference

A small, synchronous API. Send an image, get an optimized one back. Everything on this page is generated from the same document your code generator reads.

OpenAPI document

Point a client generator at this URL, or import it into your API tool of choice. It is public: you do not need a key to read it.

https://ecommerceimageoptimizer.com/v1/openapi.json

Getting a key

Keys are created from your account page. A live key spends your allowance; a test key runs the same image pipeline and spends nothing, so you can build against real output before you commit to anything.

Authentication

Send the key as a bearer token on every request. Keep it on your server: this API sends no CORS headers, because a key in browser JavaScript is a published key.

Authorization: Bearer iok_live_R3kP...

Your first call

Optimize one image and print the URL of the result. The URL is signed and short lived, so fetch it as part of handling the response rather than storing it.

curl -X POST https://ecommerceimageoptimizer.com/v1/images/optimize \
  -H "Authorization: Bearer $IOK_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d "{
        \"image_base64\": \"$(base64 -w0 product.jpg)\",
        \"file_name\": \"product.jpg\",
        \"config\": { \"outputFormat\": \"webp\", \"quality\": 78 }
      }"

Connect an agent (MCP)

The same key works as a Model Context Protocol server, so a coding agent can optimize images without you writing a client. Add it once, with your key as a header.

claude mcp add --transport http ecom-image-optimizer \
  https://ecommerceimageoptimizer.com/api/mcp \
  --header "Authorization: Bearer $IOK_API_KEY"
  • Three tools: optimize_image, list_presets and check_account. A key only sees the tools its scopes allow, so a narrowed key gets a narrowed list.
  • optimize_image takes a public https URL, not a local path. An agent holding a file on disk should POST it to /v1/images/optimize as image_base64 instead, with the same key.
  • The download URL a tool returns lives 60 seconds. An agent should save the file as soon as it receives it.
  • A tool call spends a slot at the MCP endpoint and another at the endpoint behind it, so the effective ceiling is about 60 tool calls a minute against the 120 a key gets.

Limits and cost

  • 500 calls a month are free per account. After that each call spends one credit.
  • Sending the same image again within 24 hours is free. The credit unit is a unique source image, not a request, so retries and reprocessing are never billed twice. The billable field in the response tells you which happened.
  • 120 requests a minute per key. Every response carries the real remaining count, so backing off on X-RateLimit-Remaining works.
  • Images up to 3 MB.

Retrying safely

Send an Idempotency-Key header and a retry cannot charge you twice. For 24 hours the same key returns the same result with a freshly signed URL. The same key with a different body is refused with 422 rather than quietly treated as a new request.

Errors

Every failure is an RFC 9457 problem document. Switch on type, which is stable; title is written for people and may change.

{
  "type": "https://ecommerceimageoptimizer.com/problems/insufficient_credits",
  "title": "The free monthly allowance is spent and this account has no credits left.",
  "status": 402
}

Endpoints

post/v1/accounts

Create an account and get a key, with no credential

The one endpoint here that needs no API key, because an agent arriving with no account has nothing to authenticate with. It creates an account, emails a claim link to the address you supply, and returns a live key you can use immediately.

**Only use an address the person you are acting for can actually read.** The account is theirs, not yours: the emailed link signs them in, and an account nobody claims is deleted automatically along with the key.

An address that already has an account gets 202 and **no key**. That is deliberate and is not an error you can work around: issuing a key for an existing address would be account takeover by email address. Sign in and create one from the account page instead.

Rate limited per address and capped per day across the whole service, and it can be switched off entirely, in which case it answers 403 signup_disabled. If you need a key without this, the Device Authorization Grant is the supported path and has no such limits.

Request body

emailstringrequired
Where the claim link goes. Must accept mail, or nothing is created.
client_idstringrequired
Who is asking. Identifies for the operator, and authenticates nothing.
scopestringoptional
Space delimited. Absent means all four. Same vocabulary as the device grant.

Responses

201
Created. The key is in the body and is never shown again.
202
That address already has an account. No key is issued.
400
Invalid body or unknown scope.
403
Programmatic account creation is switched off.
422
The address would not accept mail, so nothing was created.
429
Per-address window or the daily ceiling.
get/v1/account

Balance, allowance and key details

Read-only and unmetered: asking how much allowance you have never spends any of it. This is the call to make first to check a key works.

A test key sees the account's real balance and real usage, with mode saying which world it is in. Hiding them would make the endpoint useless for checking an integration.

Scope: account:read

Responses

200
The account state.
401
No key, or a key that is not valid.
403
The key lacks the account:read scope.
429
Over the per-key rate limit.
503
The API is temporarily unavailable.
get/v1/presets

List the built-in presets and your own

Built-in presets are shared by every account and cannot be edited. Your own are the ones saved through this endpoint or in the web app: an API key and a signed-in browser session on the same account see the same list.

Scope: presets:read

Responses

200
Every preset available to this account.
401
No key, or a key that is not valid.
403
The key lacks the presets:read scope.
429
Over the per-key rate limit.
503
The API is temporarily unavailable.
post/v1/presets

Save a preset

Send only the fields you care about. The stored preset is your config layered over the defaults, so reading it back gives a complete config rather than a fragment whose meaning depends on which build answered.

Scope: presets:write

Request body

namestringrequired
descriptionstringoptional
tagsstring[]optional
product_typegeneral | fashion | jewelry | electronics | food | furnitureoptional
configPresetConfigrequired
Image settings. Every field is optional and unknown fields are ignored, so a client written against an older version keeps working. GET /v1/presets returns complete configs, which is the easiest way to see every field.

Responses

201
The preset as stored.
400
The body is not valid. errors names the fields.
401
No key, or a key that is not valid.
403
The key lacks the presets:write scope.
409
This account has reached the preset limit.
429
Over the per-key rate limit.
503
The API is temporarily unavailable.
post/v1/images/optimize

Optimize one image

Synchronous. Send base64 image bytes or a public https URL, and get a signed URL to the result. Exactly one of image_base64 and image_url is required; sending both is a 400 rather than a silent choice between them.

image_url is fetched server side, on port 443 only, with redirects refused and the connection pinned to an address that has already been checked, so a URL resolving to a private or link-local address is refused. Every fetch failure returns the same message on purpose: an answer that told a closed port from an open one would be a port scanner.

The URL is valid for 60 seconds, so fetch it as part of handling the response rather than storing it. Sending Idempotency-Key makes a retry safe: for 24 hours the same key returns the same result with a freshly signed URL, and the same key with a different body is 422 rather than a silent second charge.

Configuration is layered: defaults, then preset_id, then config. So preset_id plus config: {"quality": 90} means "that preset, at 90".

Scope: images:write

Headers

Idempotency-Keystringoptional
Any string up to 255 characters. Honoured for 24 hours.

Request body

image_base64stringoptional
Raw base64, or a data URL. Up to 3 MB decoded.
image_urlstringoptional
Public https URL of the source image, port 443, no redirects. Fetched server side, up to 3 MB.
file_namestringoptional
Used for the output name. Defaults to image, or to the last path segment of image_url.
preset_idstringoptional
A built-in preset id, or one of yours from GET /v1/presets.
configPresetConfigoptional
Image settings. Every field is optional and unknown fields are ignored, so a client written against an older version keeps working. GET /v1/presets returns complete configs, which is the easiest way to see every field.

Responses

200
The optimized image.
400
The body is not valid.
401
No key, or a key that is not valid.
402
The free monthly allowance is spent and there are no credits left. A **test** key never gets this: it has its own daily allowance and cannot spend credits, so it gets 429 test_quota_exhausted instead.
403
The key lacks the images:write scope.
404
No preset with that id belongs to this account.
409
A request with this Idempotency-Key is still running.
413
The image is over the size limit for this tier.
422
Either the image could not be processed, or this Idempotency-Key was already used for a different request.
429
Over the per-key rate limit (rate_limited, retry in a minute), or a test key over its daily allowance (test_quota_exhausted, Retry-After is the seconds left until midnight UTC).
500
The image could not be optimized. You were not charged.
503
The API is temporarily unavailable, image processing is paused (unavailable), or the service is at capacity (at_capacity).
post/v1/device/code

Start connecting an agent to a human account

RFC 8628 section 3.1. Unauthenticated, because having no credential yet is the point. Show the returned user_code to your human alongside verification_uri, then poll /v1/token.

verification_uri_complete is a convenience link with the code already in it. Show the code as well even when you use it: the human is asked to compare what is on their screen with what is on yours, and that comparison is what stops a stranger from talking somebody into approving a code they were sent.

Send the body as application/x-www-form-urlencoded. JSON is accepted too.

Responses

200
The two codes, and how to wait.
400
client_id missing or repeated, or a scope outside the four.
429
Too many device authorizations already pending from this client.
503
Temporarily unavailable.
post/v1/token

Poll for the key, and collect it once the human approves

RFC 8628 section 3.4. Poll no faster than the interval you were given.

**While you wait, this returns HTTP 400 with error: authorization_pending.** That is what RFC 6749 section 5.2 specifies for these errors and it does not mean anything is wrong. Only authorization_pending and slow_down mean keep polling; every other error code is terminal and you should stop.

On slow_down, add five seconds to your interval permanently. On a connection failure or a 503, back off exponentially instead: that is a different rule and the two are easy to confuse.

The key you receive does not expire and is revoked by its owner at /account. It arrives in exactly one HTTP response and cannot be retrieved again, so store it before you do anything else.

Responses

200
The human approved. This body is the only place the key exists.
400
authorization_pending or slow_down (keep polling), or access_denied, expired_token, invalid_grant, invalid_request, unsupported_grant_type (stop).
429
Polling far above the interval. Back off.
503
Temporarily unavailable. Back off exponentially, do not treat it as slow_down.