Building an app

img.pro can be the identity, image storage, and billing layer your app builds on. Your users sign in through img.pro, their images live in img.pro, and they pay img.pro — your app holds one secret and acts on their behalf.

This is a different path from the rest of these docs. The API Reference is for using img.pro with your own API key. This page is for building an app that signs in its users and manages images on their behalf. If you just want to upload your own images, start at Quick Start instead.

The whole integration is four steps:

  1. Sign your user in on an img.pro-hosted page (you never see their password or handle their one-time code).
  2. Exchange the result — your backend trades a one-time code for a verified user.
  3. Act on their images with one machine secret + a header naming the user.
  4. Bill them — read their plan and usage; send them to the img.pro dashboard for any plan change.

There is no OAuth, no consent-scope dance, no per-user tokens, and no password handling on your side.

How it works

A few concepts, used throughout the rest of this page:

  • App — your registered client. It has a public app id (an opaque string like a8o46yrk) and one machine secret.
  • Machine secretimg_sk_live_…. Your app's only stored secret. It lives in your backend, never in a browser, and authenticates every API call your backend makes.
  • User — an img.pro account (identified by email). The same person can use many apps; img.pro is their shared identity. Each user has an opaque user id.
  • Workspace — each user gets a private workspace in your app that holds their images and billing plan, auto-created on their first connect. You don't manage it directly — you address it through the user (X-Img-User), and img.pro keeps it scoped to your app.
  • The sandbox — your app can only ever reach the workspaces it created. There's no way to see another app's data, or a user's data in other apps. The boundary is automatic, which is why there's no consent screen to manage.

Two things happen, in order: hosted login gets you a verified user once (the only browser step), then your backend uses the machine secret to act for them on every call after — there's no per-user token to store or refresh. Your app drives the API; anything a user manages (plan changes, the Stripe portal) lives on the img.pro dashboard, and you link them there.

Register your app

Registration is self-serve for paid img.pro users. On the Apps page you register your app with its name (shown to users on the login page) and your exact redirect URI(s) (where img.pro returns the user after login). You receive your machine secret once — store it in your backend immediately; img.pro keeps only its hash and can't show it again. You also get your public app id.

Register your app

Every example on this page uses these hosts:

img.pro
Web and hosted login.
api.img.pro
The REST API.
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://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://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):

img.pro
html
<!-- Short: drop the verb, keep the mark -->
<a class="imgpro-signin" href="https://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://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, img.pro mints a code and redirects straight back (?code=…) — no email, no tap. If it can't be done silently (no active session, or they've never connected your app), 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 machine secret:

Request
bash
curl -X POST "https://api.img.pro/v1/auth/exchange" \
  -H "Authorization: Bearer img_sk_live_…" \
  -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 machine secret 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 machine secret plus a header naming the user:

text
Authorization: Bearer img_sk_live_…
X-Img-User: m3k9ab2c          # the user to act on

Send X-Img-User: <user.id> on every data call to act on that user (the user.id you saved from the exchange). A data call without it returns 422 validation_error. Every call is sandboxed: a user that isn't yours returns 403 user_forbidden (never a 404 that would reveal whether it exists).

Upload an image (a file, or a URL to import):

Request
bash
curl -X POST "https://api.img.pro/v1/images" \
  -H "Authorization: Bearer img_sk_live_…" \
  -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 as the single-team API: list, get, update, delete, batch, and usage are all documented in the API Reference — just send the machine secret + X-Img-User header instead of an API key.

App API uploads are always public and start safe-for-work. Max upload 70 MB (SVG 10 MB). Pass an Idempotency-Key header on uploads and checkout to make a retry safe.

Step 4 — Billing

Billing attaches to the user's workspace. Your app reads the plan and usage; the user changes plans on the img.pro dashboard. Read billing status:

Request
bash
curl "https://api.img.pro/v1/billing/status" \
  -H "Authorization: Bearer img_sk_live_…" \
  -H "X-Img-User: m3k9ab2c"
Response
json
{
  "object": "billing_status",
  "plan": "free",
  "manage_url": "https://img.pro/billing",
  "usage": { "...": "same shape as GET /v1/usage" },
  "available_plans": [
    { "object": "plan", "id": "pro", "name": "Pro",
      "prices": { "monthly": { "amount_cents": 900, "currency": "usd" },
                  "annual":  { "amount_cents": 9900, "currency": "usd" } },
      "limits": { "monthly_uploads": 1000, "storage_bytes": 10737418240 } }
  ]
}

For any plan change, downgrade, cancel, or the Stripe portal, send the user to manage_url — it opens the img.pro dashboard focused on their account. Treat it as an opaque link: just redirect the user to it. The API does no other billing writes; management lives on the dashboard.

For a smoother first conversion you can start a Stripe Checkout for a free→paid user directly (this returns 201 with a checkout_url to redirect to):

Request
bash
curl -X POST "https://api.img.pro/v1/billing/checkout" \
  -H "Authorization: Bearer img_sk_live_…" \
  -H "X-Img-User: m3k9ab2c" \
  -H "Content-Type: application/json" \
  -d '{"plan": "pro", "interval": "monthly", "return_url": "https://yourapp.com/done"}'

A user that's already paid returns 409 already_subscribed — send them to manage_url instead. The plan (pro/scale/max) and interval (monthly/annual) come straight from available_plans.

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 was suspended by an operator — a full stop.
already_subscribed 409
Checkout on a paid user → use manage_url.
invalid_target_url 422
A return_url that isn’t an absolute http(s) URL.

A few lifecycle facts:

  • No token to refresh. Your backend acts via the machine secret indefinitely.
  • A previously-valid user starts returning 403 user_forbidden. Treat the user as gone (they deleted their img.pro account) — drop your user.id row, and re-run hosted login to reconnect.
  • 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 thing in one file for an LLM or codegen? /llms-full.txt and the OpenAPI spec both include the App API surface.