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 read and/or write permissions 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.

Create an App

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:

text
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):

Continue with img.pro
html
<a class="imgpro-signin" href="https://test.img.pro/connect?app=YOUR_APP_ID&amp;redirect_uri=YOUR_CALLBACK&amp;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):

img.pro
html
<!-- 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
none attempts 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):

text
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:

text
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)

Add &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:

Request
bash
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"}'
Response
json
{
  "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:

text
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):

Request
bash
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.

New App buckets are private by default; migrated collections retain their saved visibility default. New uploads start safe-for-work. Private means the 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.

Request
bash
curl "https://test.api.img.pro/v1/billing/status" \
  -H "Authorization: Bearer YOUR_APP_KEY" \
  -H "X-Img-User: m3k9ab2c"
Response
json
{
  "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 verify state on the callback, and exchange the code from your backend.
Want the whole surface in one file for codegen or an agent? The OpenAPI spec includes the App API.