Building an app
Build on img.pro for image storage and connected-user accounts. Your backend uses an App API key for its own storage, or adds a user selector to work in a connected user’s bucket.
The API Reference describes the same image endpoints for both destinations. If you only need App storage, create an App and follow the Quick Start. The hosted-login steps below are optional.
How it works
- App: your registered product, with a public opaque app id and a dedicated storage bucket. An App-wide API key addresses this bucket when no user selector is sent.
- User: an img.pro account with an opaque user id. Accounts can have no storage, or connect to many Apps.
- Connected-user storage: hosted login connects a user to your App and creates their private bucket within it. Select it using
X-Img-User. The App owner’s own connected-user bucket is separate from the App storage. - API key: a backend secret with
readand/orwritepermissions and App-wide or fixed-bucket scope. Only an App-wide key can select a connected user. It never reaches another App’s storage.
There is no per-user API token to store or refresh. An App key must remain server-side; authenticate your own caller before choosing their stored img.pro user id.
Connected consumers see usage and launch your service from img.pro. They may create read-only keys for their own media; existing write-capable keys continue working. They may also disconnect and delete their connection’s media. Handle 403 user_forbidden with an explicit reconnect flow; silent login will not recreate a deleted connection.
Create your App
Create an App from Apps with its name; you land on its Overview with the public app id. Issue its first key from the Keys page: the plaintext is shown once, so store it in your backend environment. App creation has no plan requirement or count cap. Turn on sign-in from the App’s Users page by adding an App URL and exact callback URLs. Neither is required for storage-only use. For hosted login and user selectors, create a separate key on the Keys page with data access set to This App and its users. Your first key keeps its scope. Manage multiple named keys on the Keys page; creating a key preserves all existing keys.
The App owner manages its settings and App-wide key. Fixed-bucket keys retain their own permissions and scope. Rotating a key invalidates the old key.
Every example on this page uses these hosts:
- img.pro
- Web and hosted login.
- test.api.img.pro
- The REST API.
- test.src.img.pro
- Image CDN (the host in every
url).
Step 1: Sign your user in
Redirect the user's browser to the img.pro hosted-login page:
https://test.img.pro/connect?app=YOUR_APP_ID&redirect_uri=YOUR_CALLBACK&state=YOUR_STATE
In your UI, launch that redirect from the official Continue with img.pro button so users recognize the handoff (full rules under Brand & attribution):
<a class="imgpro-signin" href="https://test.img.pro/connect?app=YOUR_APP_ID&redirect_uri=YOUR_CALLBACK&state=YOUR_STATE">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418"/></svg>
Continue with img.pro
</a>
<style>
.imgpro-signin{display:inline-flex;align-items:center;gap:9px;padding:12px 16px;border-radius:8px;
font:500 14px/1 -apple-system,system-ui,sans-serif;text-decoration:none;
background:#0a0a0a;color:#fff;border:1px solid #0a0a0a;transition:background .15s}
.imgpro-signin:hover{background:#1a1a1a}
.imgpro-signin svg{width:18px;height:18px}
/* On a dark background, swap to the light variant: */
.imgpro-signin--light{background:#fff;color:#0a0a0a;border-color:#e5e5e5}
.imgpro-signin--light:hover{background:#f0f0f0}
</style>
Tight on space? Shorten by dropping the verb, never the brand (see Brand):
<!-- Short: drop the verb, keep the mark -->
<a class="imgpro-signin" href="https://test.img.pro/connect?app=YOUR_APP_ID&redirect_uri=YOUR_CALLBACK&state=YOUR_STATE">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418"/>
</svg>
img.pro
</a>
<!-- Icon-only: SSO rows only; always give it an aria-label -->
<a class="imgpro-signin imgpro-signin--icon" aria-label="Continue with img.pro" href="https://test.img.pro/connect?app=YOUR_APP_ID&redirect_uri=YOUR_CALLBACK&state=YOUR_STATE">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418"/>
</svg>
</a>
<style>/* with the .imgpro-signin base above */
.imgpro-signin--icon{padding:12px;gap:0}</style>
- app required
- Your app id.
- redirect_uri required
- Must exactly match a URI you registered (byte-for-byte; no trailing-slash or query leniency).
- state recommended
- Your opaque CSRF / correlation value, echoed back unmodified. Max 512 chars; over-length is rejected with an error page, never silently truncated.
- prompt optional
noneattempts a silent reconnect (see below).
The user enters their email, gets a one-time code, and confirms an explicit consent step (“Connect your app to img.pro?”, shown with your registered app name). On confirm, img.pro redirects the browser back to your callback (state is present only if you sent one):
YOUR_CALLBACK?code=ONE_TIME_CODE&state=YOUR_STATE
On your callback, verify state matches what you sent, then hand the code to your backend. The code is single-use and expires in ~60 seconds. Exchange it promptly, server-side.
If the user cancels (or the connection can't be granted), img.pro returns them to the same callback with an error and no code:
YOUR_CALLBACK?error=access_denied&state=YOUR_STATE
Check for error before code on your callback and show a “sign-in canceled, try again” state rather than attempting an exchange (this mirrors the OAuth user-deny convention). The return URL is always one you registered, so it's safe to land the user back in your app.
Silent reconnect (prompt=none)
&prompt=none to the login URL to reconnect a returning user with no screen at all: if they have a live img.pro session and have already connected your app with the same current email, img.pro mints a code and redirects straight back (?code=…): no email, no tap. If it can't be done silently (no active session, no prior connection, their email changed, or the connection predates the July 2026 consent hardening; those users re-confirm interactively once, then silent reconnect resumes), you get ?error=interaction_required instead. Just redirect again without prompt=none to show the full flow. A good pattern for “keep me signed in”: try prompt=none first, fall back on interaction_required.Step 2: Exchange the code
From your backend, exchange the code for the verified identity using your App API key:
curl -X POST "https://test.api.img.pro/v1/auth/exchange" \
-H "Authorization: Bearer img_sk_test_…" \
-H "Content-Type: application/json" \
-d '{"code": "ONE_TIME_CODE"}'
{
"object": "auth_context",
"app": { "id": "a8o46yrk", "object": "app", "name": "Photo App" },
"user": { "id": "m3k9ab2c", "object": "user", "email": "jane@example.com", "verified": true }
}
Persist the non-secret user.id for this user: your map from your own user record to their img.pro account. There's nothing secret to store; the App API key you already hold authorizes future calls. A bad, expired, used, or wrong-app code returns 422 invalid_code. Restart from step 1.
Step 3: Act on their images
Your backend now acts on the user with the App API key plus a header naming the user:
Authorization: Bearer img_sk_test_…
X-Img-User: m3k9ab2c # the user to act on
Send X-Img-User: <user.id> to select that connected user’s storage. Omit the header to use the App storage. A blank or malformed selector returns 422 validation_error; an unavailable or unconnected user returns 403 user_forbidden. Neither falls back to the App storage. X-Img-Team is unsupported; fixed-bucket keys reject X-Img-User.
Upload an image (a file, or a URL to import):
curl -X POST "https://test.api.img.pro/v1/images" \
-H "Authorization: Bearer img_sk_test_…" \
-H "X-Img-User: m3k9ab2c" \
-F "file=@photo.jpg" \
-F "caption=Hero shot"
This returns 201 with the Image object. Its url is a CDN link you can drop in an <img> and transform on the fly (resize, convert, white-background cutout). From here, the whole image surface is the same for either destination: list, get, update, delete, batch, and usage are all documented in the API Reference. Send the same App key and add X-Img-User only when selecting a connected user.
page_url viewer page 404s for anyone outside the workspace. Your url and every sizes variant keep serving normally, so embedding is unaffected. Send public: true on the upload (or PATCH /v1/images/{id} later) when you want an img.pro-hosted page for it. JPEG, PNG, GIF, WebP, AVIF, and supported HEVC-based HEIC must fit 20 MB (20,000,000 bytes) and decode synchronously; .heif is only a filename/MIME alias for supported HEVC HEIC. SVG, BMP, and ICO are not accepted. Pass an Idempotency-Key header on uploads to make a retry safe.Step 4: Usage and existing subscriptions
App creation, uploads, storage, and transforms are free during the transition, without plan-based limits. Actual usage remains available from GET /v1/usage. Existing subscriptions continue until changed or canceled; their plan values do not gate current usage.
GET /v1/billing/status is read-only and resolves the same exact bucket as image requests: omit X-Img-User for App storage, or include it for a connected-user bucket.
curl "https://test.api.img.pro/v1/billing/status" \
-H "Authorization: Bearer YOUR_APP_KEY" \
-H "X-Img-User: m3k9ab2c"
{
"object": "billing_status",
"plan": "free",
"limits_enforced": false,
"billing_url": "https://test.img.pro/apps/billing?app=a8o46yrk",
"usage": { "...": "same shape as GET /v1/usage" },
"available_plans": [
{ "object": "plan", "id": "pro", "name": "Pro",
"prices": { "monthly": { "amount_cents": 2900, "currency": "usd" },
"annual": { "amount_cents": 29900, "currency": "usd" } },
"limits": { "monthly_uploads": 10000, "storage_bytes": 107374182400 } }
]
}
billing_url is the co-branded billing card for your App: the one place a user picks, changes or cancels a plan for their bucket in your App. It carries only your App’s uid. Append a registered redirect_uri (the card returns the user there, exactly as hosted login does) and optionally state, plus plan and interval to pre-select a plan. The card verifies an existing img.pro session or runs an inline email code, so no dashboard session is needed. The catalog’s legacy limit values remain for existing billing records and are not enforced; the API performs no billing writes.
Future usage billing is intended for App owners, without an end-user billing step. That billing model is not active during the free transition.
Brand & attribution
Using img.pro as your identity layer comes with a small brand contract. It's what makes the hosted-login handoff feel safe (your users recognize img.pro before they're ever redirected to it, so every app that carries the mark converts the next one better). Co-brand, never white-label: your product is the brand your user knows; the img.pro mark always signals who holds their account.
- The button. Use the snippet above unmodified. Label it Continue with img.pro by default; Sign in with img.pro is allowed only on a surface that's unambiguously a returning-user login. Dark button on light UIs, light on dark; keep the globe and don't recolor it.
- The lockup. When your own UI names the relationship (a “connected account” row), write it app-first: Your App × img.pro.
- Attribution. Show “Account & images secured by img.pro” somewhere persistent (footer or account settings). It's the always-on recognition that pays the handoff back.
- The “✓ Verified” badge is assigned by img.pro (an operator flips it) to apps it has reviewed, and shown on the login screen. Don't reproduce it yourself.
- Naming. Your app name can't contain “img.pro” (rejected at registration) and shouldn't imply img.pro built or endorses your product. It's the account your users sign in with, not a label for your app.
Errors & lifecycle
Errors use the standard error envelope. The codes specific to the App API:
- invalid_code 422
- Exchange code bad / expired / used → restart hosted login.
- user_forbidden 403
- Not your user, or gone / disabled → treat as disconnected (below).
- app_suspended 403
- Your app is not active: suspended by its owner or blocked by an img.pro operator; a full stop until reactivated.
- app_context_unavailable 503
- Current app/user/workspace authority context could not complete in time → honor Retry-After and retry.
A few lifecycle facts:
- No token to refresh. Your backend uses the App API key until it expires or is revoked, subject to current permissions and lifecycle checks.
- A previously-valid user starts returning
403 user_forbidden. The connection is unavailable; stop requests for it and let the user reconnect when eligible. A forbidden response does not disclose whether the account was deleted or disabled. - Keep
img_sk_server-side only. Never ship it in a browser or mobile binary; it's your whole app's credential. Always verifystateon the callback, and exchange the code from your backend.