Build an App

Build on img.pro for image storage and sign-in for your users. Your backend uses an API key for your App’s own storage, and an App-wide key with a user selector to work in a connected user’s storage.

On this page

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 img.pro sign-in steps below are optional.

How it works

  • App: your registered product, with a public opaque App id and its own storage, the App storage. An App-wide key works there 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: img.pro sign-in connects a user to your App and creates their own storage within it, kept apart from everyone else’s. Select it using X-Img-User. If you sign in to your own App, your storage there is separate from the App storage.
  • API key: a backend secret with read and/or write permissions whose reach is App-wide or App storage only (scope app or bucket). Only an App-wide key can select a connected user. No key reaches another App’s storage.

There is no per-user API token to store or refresh. An API key must remain server-side; authenticate your own caller before choosing their stored img.pro user id.

Users who connect to your App see its usage and open it from img.pro. They can create read-only keys for their own images (write keys they already have keep working), and they can disconnect, which deletes their images in your App. While your App is paused, blocked or unavailable, they see it as Paused: they can’t open it or create keys, and they can still rename and revoke their keys, disconnect, and manage or cancel a plan they bought from you. Handle 403 user_forbidden by sending the user through img.pro sign-in again; a silent reconnect never restores a deleted connection. While a disconnect is still under way, the sign-in and billing screens tell the user their connection is still being removed and link to where they finish it; prompt=none answers interaction_required.

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 API 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 Connect page by adding an App URL and exact callback URLs. Neither is required for storage-only use. For img.pro sign-in and user selectors, create a separate key on the API Keys page with data access set to App-wide. Your first key keeps its reach. Manage multiple named keys on the API Keys page; creating a key preserves all existing keys.

The App’s owner and its admins manage its settings and create its App-wide keys (Team roles). They can revoke any key, and only a key’s creator changes it. A key stops working when its creator is no longer an admin. App-storage keys keep their own permissions and reach. Creating a key never invalidates another: rotate by creating a new key, deploying it, then revoking the old one.

Create an App

Every example on this page uses these hosts:

img.pro
The console and img.pro sign-in.
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 img.pro sign-in:

URL
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&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:#0033ff;color:#fff;border:1px solid #0033ff;transition:background .15s,border-color .15s}
.imgpro-signin:hover{background:#002ee6;border-color:#002ee6}
.imgpro-signin svg{width:18px;height:18px}
</style>

Tight on space, or want your own words? Drop the verb, or put your own label on the button, such as Sign up free. Never drop the globe: it carries img.pro’s mark (see Brand):

Sign up free img.pro
html
<!-- Your own label: the globe stays, the aria-label names img.pro -->
<a class="imgpro-signin" aria-label="Sign up free with img.pro" 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>
  Sign up free
</a>

<!-- 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 and types the 6-digit code img.pro sends them. Right above the button, img.pro says what connecting grants, “Your App gets your email, and your images are stored in your img.pro account.”, and the code step’s button reads “Continue to your App”. Typing the code is the consent: img.pro connects the user and redirects the browser straight back to your callback, on a first connection and for a returning user alike (state is present only if you sent one):

URL
YOUR_CALLBACK?code=ONE_TIME_CODE&state=YOUR_STATE

A user who is already signed in to img.pro types nothing, so they confirm once instead: “Finish connecting to your App” with Connect and continue to your App, or “Welcome back” with Continue to your App when they are already connected. A session alone never makes a new connection (a silent reconnect, below, only reaffirms one that already exists for the same email). The same page appears if connecting fails on img.pro’s side right after the code, or if the user sends a code again after it was used (Back, or a reload) while still signed in with that email. Either way, the redirect is the same.

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:

URL
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.

While your App is paused, img.pro’s page says it isn’t available right now, and its Back to your App button returns the user to your callback with temporarily_unavailable instead, and no code:

URL
YOUR_CALLBACK?error=temporarily_unavailable&state=YOUR_STATE

Show a “try again later” state for it, not a canceled sign-in: the user did nothing to stop it.

Once your App has its plan’s users limit connected (50 on Free, 500 on Pro), a new user’s first sign-in stops on img.pro’s page, which says your App can’t take new sign-ins right now. Its one button takes the user back to your App: img.pro returns them to your callback with error=access_denied, as Cancel does. Everyone already connected keeps signing in, silent reconnects included. See Usage and limits.

Silent reconnect (prompt=none)

Add &prompt=none to the sign-in URL to reconnect a returning user with no screen at all: if they have a live img.pro session and have already connected to 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 earlier connection, their email changed, or they connected before img.pro added the consent step, in which case they confirm once and silent reconnects work after that), 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-wide key (it needs read):

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 App-wide key you already hold authorizes future calls. A bad, expired or used code, or one for another App, returns 422 invalid_code. Restart from step 1.

Step 3: Act on their images

Your backend now acts on the user with its App-wide key plus a header naming the user:

http
Authorization: Bearer img_sk_live_…
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. A key that is not App-wide rejects X-Img-User.

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 for either destination: list, get, update, delete, batch, and usage are all documented in the API reference. Send the same App-wide key and add X-Img-User only when selecting a connected user.

Every image loads from its url and sizes for anyone holding the link, so keep links to a user’s images inside your App. Image details and metadata need your key. New uploads start safe-for-work. JPEG, PNG, GIF, WebP, AVIF, and supported HEVC-based HEIC must fit 20 MB (20,000,000 bytes) and decode while the request is open; .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: Sell plans to your users

Selling plans is in early access. Write to support@img.pro to turn it on for your App. Once it is on, you create plans on the Pricing tab of your App’s Connect page, add monthly or yearly prices, put them on sale and open new subscriptions. img.pro runs checkout, trials, invoices and cancellations with Stripe.

A plan’s name, description and benefits are your words on img.pro’s own pages: the plan card your users choose on, and the plan’s name after your App’s on Stripe’s checkout, receipts and invoices. Write them in any language. Pricing refuses words that name img.pro or call the plan official, verified or the like, and a plan name that reads that way after your App’s name. A plan’s name can’t hold such a word at all; a description or benefit can use one for a feature (“Verified background removal”) (Brand).

List the plans on sale with GET /v1/billing/plans and an App-wide key that has read. Your key selects your App, so the request needs no user and no App.

Request
bash
curl "https://api.img.pro/v1/billing/plans" \
  -H "Authorization: Bearer YOUR_APP_KEY"
Response
json
{
  "object": "list",
  "app": { "id": "a8o46yrk", "object": "app", "name": "Example App" },
  "purchasing_enabled": true,
  "data": [
    {
      "id": "opaque-plan-id",
      "object": "plan",
      "key": "personal",
      "name": "Personal",
      "description": "For personal projects",
      "benefits": ["One board"],
      "display_order": 1,
      "prices": [
        { "id": "opaque-monthly-price-id", "object": "price", "amount": 1000, "currency": "usd", "interval": "month", "trial_days": 7 },
        { "id": "opaque-yearly-price-id", "object": "price", "amount": 10000, "currency": "usd", "interval": "year", "trial_days": 7 }
      ]
    }
  ],
  "pagination": { "has_more": false, "next_cursor": null, "next_url": null }
}

Each Plan carries its prices on sale, with amounts in the currency’s smallest unit (cents for usd). key is the plan key you chose, the value your App reads to decide what a subscriber gets. Plans come in the Display order you set in Pricing (display_order), then by name, the order the hosted card shows them. limit is 1–100 (default 50); follow pagination.next_url until has_more is false. A limit outside 1–100, a cursor this list didn’t give you, either one sent twice, or an App parameter (your key already names the App) returns 422 validation_error, with details naming the parameter. When purchasing_enabled is false, hide your buy buttons: subscribers can still manage their plan.

Check what one user has with GET /v1/billing/status and X-Img-User, or with that user’s own key and no header. Each 403 forbidden says what is missing. Changed September 29, 2026: it no longer answers with an img.pro plan and usage, with or without X-Img-User. With a user, plan, usage, limits_enforced and available_plans are gone: read access and subscription. Your App’s img.pro plan and usage are at GET /v1/usage.

Request
bash
curl "https://api.img.pro/v1/billing/status" \
  -H "Authorization: Bearer YOUR_APP_KEY" \
  -H "X-Img-User: m3k9ab2c"
Response
json
{
  "object": "billing_status",
  "app": { "id": "a8o46yrk", "object": "app", "name": "Example App" },
  "purchasing_enabled": true,
  "trial_eligible": false,
  "has_subscribed": true,
  "access": { "plan_key": "personal", "status": "active", "expires_at": "2026-11-26T00:00:00.000Z" },
  "subscription": {
    "id": "opaque-subscription-id",
    "object": "subscription",
    "plan_key": "personal",
    "price_id": "opaque-monthly-price-id",
    "status": "active",
    "trial_end": null,
    "current_period_end": "2026-11-26T00:00:00.000Z",
    "cancel_at_period_end": false,
    "cancel_at": null,
    "pending_plan_key": null,
    "pending_effective_at": null
  },
  "billing_url": "https://img.pro/apps/billing?app=a8o46yrk"
}

access.plan_key is the plan the user has now: your App reads it to decide what they get. Access is inactive, trialing, active or grace (a payment failed and its grace days are still running), and expires_at is when the current period, trial or grace ends: read status again after it. Someone who never subscribed reads inactive, a null plan_key and a null subscription. subscription is the plan they pay for, for display: its status is Stripe’s, so decide access from access; cancel_at_period_end or cancel_at says it is set to end, and pending_plan_key with pending_effective_at a change booked for the renewal. Dates are ISO UTC strings or null. Offer a trial only when trial_eligible is true and the price has trial_days: a user gets one trial per App, even after cancelling and reconnecting. While img.pro confirms a renewal, status returns 503 server_error with Retry-After: 30 and a wait action: keep the access you last read for the user and retry, so a moment of doubt never reads as no access.

Send users to billing_url with a registered exact redirect_uri, optional state, and an optional Price id as price_id. With a price you sell now, a user with no current plan goes straight on to Stripe’s checkout once signed in, where they confirm; backing out of it returns to your redirect_uri with state and without billing. Otherwise the card shows its plans, with that price chosen where it is one of them: without a price, for a subscriber, while new subscriptions are closed or your App is paused, and for a price not on sale. Send only users already connected to your App: the card signs them in with a 6-digit code, and an email that isn’t connected to your App is asked for the one they use there. While it asks for the email, the card shows the price you passed as its plan, with any trial as who gets it (“Personal, 7-day free trial for new subscribers, then $10 a month”). The card never creates an img.pro account. It needs no cookie, so it works in an in-app browser. The hosted card handles checkout, plan changes (each confirmed first with what it costs today and from when), cancelling, payment details and invoices, a payment that waits (Confirm payment when the bank must confirm it, Pay invoice beside Update payment method for a declined card), and withdrawal (below). A link it can’t use says so in one plain sentence, with a code for the part that failed in the page’s markup (data-fault-code, not shown to the user): missing_app, app_unavailable, missing_redirect_uri, redirect_uri_not_registered or state_too_long. The way back to your App carries state, and billing=success after a completed checkout. A return query is navigation only: always re-read status before you grant anything. Closing new subscriptions in Pricing never stops a subscriber from managing theirs.

Trials require a payment method and renew automatically. A switch keeps the original trial end. Upgrades take effect once paid; downgrades at renewal. A cancelled plan keeps access to the end of its term; a withdrawal (below) ends it at once. A failed renewal keeps access for a few days of grace, then becomes inactive.

What your subscribers see and receive, so you can write your own billing copy from it. img.pro processes the payment for your App, and img.pro’s Terms cover renewal, trials, cancelling and refunds.

  • Checkout names the plan “{App} {Plan}” (“Muse Personal”), shows the price and any trial, asks for as much of the billing address as tax needs (usually the country, and a postal code where tax needs one) instead of the whole address, and says by its pay button “Billed by img.pro (Moshi Inc.) for {App}. Renews every month until you cancel in {App}.” (“every year” for a yearly price). Above it is a required checkbox, the subscriber’s request that the plan start at once and what they keep: “Start my {App} plan, including any trial, now. I can still withdraw within 14 days and get a full refund.”, its “withdraw” a link to img.pro’s Terms. It may show the price in the subscriber’s local currency. After it the hosted card confirms the plan, says “A confirmation is on its way to {email}.”, and its Continue button returns to your App; if Stripe’s word is still on its way, it says “We’re confirming your plan” and shows the plan a few seconds later.
  • Withdrawing. Every subscriber, wherever they live, can withdraw from your App’s plan within 14 days of subscribing, without a reason, and gets a full refund: every paid invoice, whole. The hosted card shows “Withdraw and get a full refund” for 20 days and 1 hour after they subscribe (never less than the law’s 14 days), and they can also withdraw by email or post, which img.pro’s support records. The plan ends at once, its open invoices are voided, and the refunds go back to the payment method. Once img.pro has settled it, usually as the subscriber presses Confirm withdrawal, GET /v1/billing/status reads canceled in subscription.status and inactive in access.status, exactly as an immediate cancel, with has_subscribed true and trial_eligible false: there is no new status to handle. While Stripe is out of reach it can still read the plan for a few minutes until img.pro settles it, so re-read status as always. The terms are img.pro’s (“Withdrawal.” in its Terms).
  • Stripe’s page, for payment details, invoices and cancelling, lists the plan under your App’s name (“{App} {Plan}”). A cancellation there takes effect at the end of the period, and plans change only on the hosted card.
  • Receipts, invoices and card statements name your App: the plan as Checkout names it, and “IMG.PRO* MUSE” on a card statement. Other payment methods, such as a bank payment through Link, show img.pro alone, as img.pro’s Terms say.
  • Stripe’s emails, in img.pro’s branding: receipts, a failed payment, a payment that needs the bank’s confirmation, an expiring card, a renewal coming up and a cancellation. No trial reminder: img.pro’s trial-ending email (below) is the one. None about a plan change.
  • img.pro’s confirmation emails, each naming your App, to every subscriber whatever their country: “Your {App} {Plan} plan” right after checkout (“Your {App} trial has started” for a plan that starts with a trial; the plan, the price, the trial’s first charge, how to cancel, the 14 days to withdraw for a full refund, the withdrawal terms with the withdrawal form, and that they asked for the plan to start right away); and, when they withdraw, “Your {App} withdrawal” (the statement, when they sent it, and what happened to the money), then “Your {App} refund is on its way” if more goes back later.
  • img.pro’s trial-ending email “Your {App} trial ends {day}” (“Your Muse trial ends Sep 21”: the earliest day the trial ends anywhere), 3 days before a trial ends (at once for a shorter trial, never after the subscriber cancels it), with the end once in UTC, what the plan costs after it (“$10 a month”, or “the price shown at checkout” where Stripe bills the subscriber in their own currency), “If you cancel before then, you won’t be charged.” and a link to the hosted card.
  • Refunds are asked for at support@img.pro, which checks with you before refunding anything the Terms don’t already cover (a withdrawal, above, is covered).

Your App’s own usage and its limits are at GET /v1/usage. Free: up to 5,000 images and 50 users per App. Pro: up to 50,000 images and 500 users per App, $20 a month. Unlimited: no limits, $100 a month. The App’s owner and admins manage its img.pro plan in Billing.

Team roles

The people who help run your App are its members. Whoever created it is its Owner, and everyone else is invited as a Viewer, an Editor or an Admin; the Owner and Admins can change a member’s role later. What each role can do:

What a member can doViewerEditorAdminOwner
See every image in the App, and its users
Upload imagesNo
Edit and delete any imageNo
Create keys, and revoke any keyNoNo
Invite and remove members, and change their rolesNoNo
Rename the App, set up sign-in, and pause or resume itNoNo
Manage the App's img.pro plan: payment details, invoices and cancellingNoNo
Start or upgrade the App's img.pro planNoNoNo
Change the plans the App sellsNoNoNo
Delete the AppNoNoNo
Leave the AppNo

Viewers and Editors see your App’s users once its sign-in is on. Only an App set up to sell plans has plans to change (Step 4).

Brand & attribution

Using img.pro as your sign-in comes with a small brand contract. It’s what makes the img.pro sign-in 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, apart from its label. 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’s sign-in. Your own words, such as Sign up free, are allowed too: the globe stays before them, carrying img.pro’s mark. We recommend an aria-label on the link that adds “with img.pro” to your words. It is img.pro blue on every background, light or dark, as on img.pro itself; 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 added by img.pro to Apps it has reviewed, and shown on the sign-in screen. Don’t reproduce it yourself.
  • Naming. Your App’s name can’t contain “img.pro” (refused when you create the App) and shouldn’t imply img.pro built or endorses your product. Your plans’ names, descriptions and benefits can’t name img.pro or call a plan official, verified or the like, and neither can a plan’s name after your App’s, as checkout shows it (refused when you save the plan or rename the App). img.pro is 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
The exchange code is bad, expired or used → send the user through img.pro sign-in again.
user_forbidden 403
This user isn’t connected to your App now → treat them as disconnected (below).
app_suspended 403
Your App is paused (by its owner or an admin) or blocked: a full stop for App-wide keys, every request, until it is resumed or unblocked. Its Overview on img.pro says which, and how to go on.
app_blocked 403
Your App is blocked: every write from an App-storage key is refused until the block is lifted, and those keys keep reading. App-wide keys get app_suspended instead. A user's own key is not your App's, and keeps working as it does while your App is paused. Write to support@img.pro.
app_context_unavailable 503
img.pro couldn’t check your key’s App and user in time → honor Retry-After and retry.

A few lifecycle facts:

  • No token to refresh. Your backend uses its App-wide key until it expires or is revoked, while your App isn’t paused or blocked and the key keeps its permissions.
  • A user who worked before starts returning 403 user_forbidden. They are no longer connected to your App; stop requests for them and let them connect again through img.pro sign-in. The response never says whether their account was deleted or disabled.
  • At the plan’s limits. Once your App holds its plan’s images limit (5,000 on Free, 50,000 on Pro), its storage and every connected user’s together, a new upload returns 403 quota_exceeded and stores nothing; once its users limit is connected (50 on Free, 500 on Pro), a new user’s first sign-in is refused. What is stored keeps serving and everyone connected keeps signing in. Unlimited has no limits.
  • 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.