{"generated":1789061711,"pages":[{"p":"getting-started/what-is-portmodels","t":"What is PortModels","d":"A marketplace of focused AI apps sharing one account, one balance, and one set of platform infrastructure.","k":"overview, introduction, platform, marketplace, gallery","h":[["The problem it solves","the-problem-it-solves"],["What you get as a user","what-you-get-as-a-user"],["What you get as a developer","what-you-get-as-a-developer"],["What a PortModel is","what-a-portmodel-is"]],"b":"What is PortModels PortModels is a marketplace for focused AI apps. Every app in the gallery is built by a developer, reviewed before it goes live, and paid for out of the same credit balance — so using a new app never means creating another account or starting another subscription. The problem it solves A lot of genuinely useful AI apps are worth using a few times a month, not every day. That creates a mismatch on both sides: For the people who would use them, each app asks for another sign-up, another card on file, and another recurring charge. The administration costs more than the tool is worth. For the people who build them, a narrow app still needs hosting, authentication, payments, metering, moderation, support, and marketing before it can be sold at all. That work rarely pays for itself on a small idea. PortModels supplies the shared layer around those apps so both sides only deal with the part they care about. What you get as a user One account for every app in the gallery. One credit balance, spent per run, with no subscription. A consistent view of what an app does, what it needs from you, and what it costs before you run it. Central control over which apps are connected to your account and what data they can reach. Start at Your first app. What you get as a developer Models — one OpenAI-compatible API across multiple providers and models. Identity — users authorize your app with the PortModels account they already have, through Connect. Billing — runs are metered and charged to the user's credits; your markup becomes your earnings. Storage — a per-user key/value store and file folder, scoped to your app. Distribution — a gallery of users already browsing and already funded. Start at Publish an app. What a PortModel is A PortModel is a developer-defined AI ap","u":"/docs/getting-started/what-is-portmodels"},{"p":"getting-started/quickstart","t":"Your first app","d":"Create an account, add credits, and run an app from the gallery in a few minutes.","k":"quickstart, first app, sign up, run, tutorial","h":[["Create an account","create-an-account"],["Find an app","find-an-app"],["Add credits","add-credits"],["Run the app","run-the-app"],["Check what you spent","check-what-you-spent"],["Next steps","next-steps"]],"b":"Your first app This walkthrough takes you from a new account to a finished run. Create an account Sign in with your PortModels account. You need one PortModels account for every app in the gallery — apps never ask you to register separately. Find an app Browse the gallery or search for what you want to do. Each listing shows what the app does, who published it, screenshots, and the ratings and comments left by other users. Look at three things before you run anything: Signal Where it is What it tells you Description Top of the listing Whether the app does the specific thing you need Permissions Connect screen What the app can read or write on your account Rating Listing header How well it has worked for other people Add credits Runs are paid for with credits. Open your balance and buy a pack — checkout is handled by Stripe, and your balance is available as soon as payment clears. There is no subscription. Credits do not expire on a monthly cycle, and an app you stop using stops costing you anything. See Credits for how spending is calculated. Run the app Open the app and use it. If it needs access to your account — to run models on your behalf, or to keep your work between sessions — it sends you to a Connect screen first: PortModels shows you which app is asking and exactly which permissions it wants. You approve or decline. On approval the app receives a scoped token. It never sees your password. Check what you spent Your balance page lists each charge with the app that made it. If something looks wrong, the support page explains how to raise it. Next steps Managing connected apps — review and revoke access. Publish an app — if you want to build one.","u":"/docs/getting-started/quickstart"},{"p":"getting-started/accounts","t":"Accounts","d":"How PortModels accounts work, what they hold, and how one account serves every app in the gallery.","k":"account, sign up, login, password, profile","h":[["What an account holds","what-an-account-holds"],["Signing in to an app","signing-in-to-an-app"],["Developer accounts","developer-accounts"],["Deleting your account","deleting-your-account"]],"b":"Accounts One PortModels account gives you access to the whole gallery. Apps do not have their own sign-up, their own password, or their own subscription. What an account holds Identity — your PortModels email address, username, and display name. Credit balance — the shared balance every app draws from. Connected apps — the apps you have authorized, and the scopes each holds. App data — per-app key/value entries and files stored on your behalf. History — the runs made on your account and what each cost. Signing in to an app You never give an app your PortModels password. When an app needs your account it redirects you to a PortModels screen, you approve it there, and the app receives a token limited to the permissions it asked for. That flow is Connect with PortModels. A downloadable app that cannot open a browser redirect uses the device flow instead: it shows a short code, you enter that code on a PortModels page, and the app is authorized once you approve. Developer accounts Publishing requires a developer profile with a publisher namespace — a unique handle that prefixes every app you publish. Your app's identifier is publisher/app-name, so two developers can both publish an app called summarizer without colliding, and a name you own cannot be taken over later. See Publish an app. Deleting your account Contact hello@portmodels.com to delete your account. Deletion removes your profile, your stored app data, and your authorizations. Unused credits are not refundable — see the Terms.","u":"/docs/getting-started/accounts"},{"p":"getting-started/credits","t":"Credits","d":"The single balance every PortModels app charges against, how spending is calculated, and where the money goes.","k":"credits, balance, pricing, cost, spend, wallet","h":[["Why one balance","why-one-balance"],["Buying credits","buying-credits"],["How a run is charged","how-a-run-is-charged"],["Running out","running-out"],["Refunds","refunds"]],"b":"Credits Credits are the unit of account across PortModels. You buy them once and spend them in any app in the gallery. There is no per-app subscription and no separate card on file with each developer. Why one balance A useful app you open twice a month is not worth a monthly plan. Charging per run means an app can be worth publishing at that scale, and worth trying at that scale. One balance also means trying a second app costs you nothing extra in setup. Buying credits Open your balance and choose a pack. Checkout runs on Stripe; PortModels never handles your card details. Your balance is credited when Stripe confirms the payment, so a completed checkout may take a moment to appear. The current rate and the available packs are shown at checkout. How a run is charged Every request an app makes on your behalf is metered: The app calls a model through the PortModels API. PortModels records the tokens (or images) the request actually used. The provider cost for that usage is calculated. The developer's markup is added. The total is deducted from your balance and written to your ledger. Deduction is atomic — a run that cannot be paid for is rejected before it starts rather than leaving your balance negative. Charges follow real usage. A long conversation costs more than a short one with the same app, and a cheaper model costs less than an expensive one for the same prompt. Running out When your balance cannot cover a request, the API returns a payment error and the app should tell you to top up. Nothing is lost: add credits and run again. Developers can read the current balance through the API — see Authentication — so a well-built app warns you before you hit zero. Refunds Credits are prepaid. See the Terms for the refund position and contact hello@portmodels.com if a run","u":"/docs/getting-started/credits"},{"p":"getting-started/glossary","t":"Glossary","d":"The terms used throughout the PortModels docs, defined once.","k":"glossary, terms, vocabulary, definitions","h":[["App","app"],["PortModel","portmodel"],["Credit","credit"],["Connect","connect"],["Scope","scope"],["Access token","access-token"],["API key","api-key"],["Publisher namespace","publisher-namespace"],["Markup","markup"],["App Data Layer","app-data-layer"],["Call log","call-log"]],"b":"Glossary App A product published in the PortModels gallery. Apps are built by third-party developers and reviewed before they go live. PortModel A developer-defined AI application layer: instructions, tools, memory, model routing, safety rules, and monetization settings combined into one named, deployable AI product. \"App\" and \"PortModel\" are used interchangeably in these docs. Credit The platform's unit of account. Users buy credits; apps spend them per run. See Credits. Connect The OAuth 2.0 flow an app uses to get permission to act on a user's account. See Connect overview. Scope A single named permission on a Connect token, such as models.run or kv.write. See Scopes. Access token The credential a connected app uses to call the API on a user's behalf. Prefixed pmc. Each login gets its own token, so several devices can stay connected at once. Tokens currently have no expiry timer; disconnecting the app revokes every token for that user, while connecting again leaves existing active tokens alone. API key A long-lived credential you use to call the API as yourself, prefixed pm. Shown once at creation and stored only as a hash. Publisher namespace The unique handle that prefixes every app you publish, making your client ID publisher/app-name. Markup The margin a developer adds on top of provider cost. The user pays cost plus markup; the markup becomes developer earnings. App Data Layer Per-user storage scoped to a single app: a key/value store and a file folder. See App data. Call log The record PortModels writes for each metered request — model, usage, cost, and outcome. Visible to users on their balance page and to developers in the dashboard.","u":"/docs/getting-started/glossary"},{"p":"apps/finding-apps","t":"Finding apps","d":"How the gallery is organized and what to look at before you run something.","k":"gallery, browse, search, discover, categories","h":[["What a listing shows","what-a-listing-shows"],["Choosing between similar apps","choosing-between-similar-apps"],["Reporting a listing","reporting-a-listing"]],"b":"Finding apps The gallery lists every published app. Each entry carries the same information in the same place, so you can compare two apps without reading two different marketing pages. What a listing shows Name and publisher — who made it, under their publisher namespace. Short description — the one line that appears in the grid. Full description — what the app does, what it needs, and its limits. Screenshots — up to four, showing the real interface. Rating and comments — from users who have run it. Permissions — the scopes the app requests when you connect it. Choosing between similar apps Two apps can do the same job at very different cost, because each developer picks their own models and sets their own markup. If cost matters, run a small job on each and compare the charges on your balance page. Ratings are more useful than descriptions for the question \"does this actually work\". Comments are more useful than ratings for the question \"does it work for my case\". Reporting a listing If an app is not what its listing claims, or behaves maliciously, report it from the listing page or email hello@portmodels.com. Listings can be hidden or removed after approval — see the Terms.","u":"/docs/apps/finding-apps"},{"p":"apps/running-an-app","t":"Running an app","d":"What happens when you open an app, approve access, and are charged for a run.","k":"run, use, launch, authorize, charge","h":[["Opening","opening"],["Approving access","approving-access"],["Being charged","being-charged"],["If a run fails","if-a-run-fails"]],"b":"Running an app Opening Apps are either hosted on the web or downloadable. Both work the same way from your side: you open the app, and the first time it needs your account it sends you through Connect. Approving access The Connect screen is served by PortModels, not by the app. It shows: the app's name and publisher, the exact permissions being requested, what each permission allows. Approve and the app receives a token limited to those permissions. Decline and nothing happens — the app gets no access at all. You can change your mind later at any time; see Connected apps. Being charged Charges happen per request, not per session: Opening an app costs nothing. Each model run costs provider cost plus the developer's markup. Every charge is written to your ledger with the app that made it. If your balance cannot cover a request, the app is told so and the run does not start. Top up and try again. If a run fails A failed request should not be charged for the work that never happened. When a model returns nothing usable, the platform records the outcome as an empty completion rather than billing it as a successful run. If you see a charge for a run that produced nothing, raise it through support with the timestamp.","u":"/docs/apps/running-an-app"},{"p":"apps/connected-apps","t":"Connected apps","d":"Review the apps authorized on your account, see what each can do, and revoke access.","k":"connected, authorized, revoke, permissions, disconnect, security","h":[["What to check","what-to-check"],["Revoking access","revoking-access"],["Re-connecting","re-connecting"]],"b":"Connected apps Every app you have approved appears in your account's connected apps list, with the scopes it holds and when you approved it. What to check Scopes — an app that only generates text should not be holding files.write. See Scopes for what each one allows. Last used — apps you no longer use are worth disconnecting. Publisher — confirm it is the developer you expect. Revoking access Disconnecting an app invalidates its tokens immediately. The next API call it makes fails with an authentication error, and it must send you through Connect again to regain access. Revoking does not delete the data the app stored for you. To remove that as well, see Your data. Re-connecting Connecting the same app again creates a separate token for that login. A second device can stay connected while the first device continues working. Disconnecting the app revokes every token for that app and user; reconnecting afterward creates a new token and does not restore the revoked ones. PortModels will never ask for your password inside an app. If an app shows you its own PortModels login form instead of redirecting you to portmodels.com, do not enter your credentials — report the listing.","u":"/docs/apps/connected-apps"},{"p":"apps/ratings-and-comments","t":"Ratings and comments","d":"How feedback works in the gallery and what the moderation rules are.","k":"rating, review, comment, feedback, moderation","h":[["Leaving feedback","leaving-feedback"],["Moderation","moderation"],["Disputes","disputes"]],"b":"Ratings and comments Ratings and comments are the main signal other users have about whether an app delivers what its listing promises. Leaving feedback Rate an app from its listing page and add a comment if you have something specific to say. The most useful comments describe: what you used the app for, whether the output was usable without rework, roughly what it cost you. Moderation Comments are public and may be moderated or removed. Reviews that are abusive, off-topic, or attempts to promote a competing app do not stay up. Developers can reply but cannot delete honest negative feedback. Disputes If you believe a rating on your own app is fraudulent — a competitor brigading, for example — report it with the specifics rather than replying publicly. See the Terms for the moderation position.","u":"/docs/apps/ratings-and-comments"},{"p":"apps/your-data","t":"Your data","d":"What apps can store on your account, who can read it, and how to remove it.","k":"data, privacy, storage, delete, files, key value","h":[["What is stored","what-is-stored"],["Who can read it","who-can-read-it"],["Controlling it","controlling-it"]],"b":"Your data Apps can store data on your account so your work survives between sessions. That storage is per user and per app. What is stored Key/value entries — small structured values: settings, drafts, saved state. See Key/value store. Files — documents an app writes for you. See File storage. Who can read it Only the app that wrote it, and only while you have granted it a read scope. One app cannot read another app's data on your account, and no app can read another user's data. PortModels staff access is limited to what is needed to operate the service — see the Privacy policy. Controlling it Withhold access — decline kv. or files. scopes at the Connect screen. The app still works for anything that doesn't need storage. Revoke access — disconnect the app; its tokens stop working immediately. Delete the data — ask the app to delete it, or contact hello@portmodels.com to have an app's stored data removed from your account. Deleting your PortModels account removes the data apps stored for you.","u":"/docs/apps/your-data"},{"p":"publishing/publish-an-app","t":"Publish an app","d":"Everything you need to get an app listed in the PortModels gallery.","k":"publish, submit, listing, gallery, developer, review","h":[["Create a developer profile","create-a-developer-profile"],["Submit your app","submit-your-app"],["Review","review"],["Live","live"],["Wire up Connect and the API","wire-up-connect-and-the-api"],["Get paid","get-paid"]],"b":"Publish an app Publishing is free. You keep the revenue your app earns from usage. Create a developer profile Publishing requires a PortModels account with a developer profile. Register if you don't have an account yet. Your developer profile carries a publisher namespace — a unique handle that prefixes every app you publish. An app named summarizer published by acme has the identifier acme/summarizer. The namespace is claimed once and cannot be taken over by anyone else. Submit your app From /publish, provide: Name and tagline — a short, specific description of what the app does. Description — what it does, what input it expects, and what a user gets back. Category — pick the closest match so users can find it while browsing. Type — web app if it runs at a URL, download if users install it. Link URL — where the app actually runs, or where the download starts. Cover image — required, shown on the gallery card and the app page. Screenshots — optional, up to 4, shown on the app page. See Images for the format and size rules. Review New submissions are pending and are not visible in the gallery until an admin reviews them. Review checks that the link works, that the listing accurately describes the app, and that the images are appropriate. Editing an approved app's core details — link, description, category — returns it to pending until it is re-reviewed, because the outbound link is what users trust most. See Review for what reviewers look for. Live Once approved your app appears in the gallery, can be rated, and can receive comments. Comments are visible immediately and moderated after the fact; admins can hide comments that violate the guidelines. Wire up Connect and the API A listing is a link — the app itself still has to authenticate users and call models: Enable Con","u":"/docs/publishing/publish-an-app"},{"p":"publishing/listing-guidelines","t":"Listing guidelines","d":"What makes a listing that passes review and converts browsers into users.","k":"listing, guidelines, description, tagline, quality","h":[["What makes a strong listing","what-makes-a-strong-listing"],["Write for someone deciding in ten seconds","write-for-someone-deciding-in-ten-seconds"],["Things that get a listing rejected","things-that-get-a-listing-rejected"],["Naming","naming"]],"b":"Listing guidelines What makes a strong listing A tagline that says exactly what the app does — avoid vague marketing language. A cover image that shows the app's actual interface or output, not a generic graphic. A link that goes directly to the running app or the download — not a landing page in between. A description that sets accurate expectations, including known limitations. Write for someone deciding in ten seconds Users compare several similar apps in one sitting. Lead with the concrete job your app does, not with what it is built on. \"Turns a meeting recording into a decision list\" beats \"AI-powered productivity assistant\". Cover, in this order: The specific problem it solves. What the user gives it and what they get back. Limits — file sizes, languages, formats it can't handle. Roughly what a typical run costs. Things that get a listing rejected The link is broken, requires a separate account, or leads somewhere unrelated. The description promises capabilities the app does not have. Screenshots are mockups rather than the real interface. The app collects credentials or payment outside PortModels. The listing impersonates another developer or product. Naming Your app name is namespaced under your publisher handle, so you don't need to add your own prefix. Pick a name that describes the app rather than one that apes an existing product.","u":"/docs/publishing/listing-guidelines"},{"p":"publishing/images","t":"Images","d":"Format, size, and composition rules for cover images and screenshots.","k":"images, cover, screenshot, upload, size, format","h":[["Standards","standards"],["Cover image","cover-image"],["Screenshots","screenshots"]],"b":"Images Standards Formats: JPG, PNG, or WebP. Max file size: 5 MB per image. Cover image: recommended around 1200×630 — it is used both as the gallery card and the app page hero. Screenshots: any reasonable aspect ratio, up to 4 per app. Cover image The cover is the only thing most users see before they click. It is cropped to different aspect ratios on the card and the hero, so keep the important content away from the edges and don't put small text near the border. Show the app's real output. A screenshot of the actual result outperforms an abstract graphic every time. Screenshots Use screenshots to answer the questions the description can't: What the interface looks like. What a real input looks like. What a real output looks like. Anything unusual about the workflow. Crop out browser chrome, desktop backgrounds, and personal data. Screenshots containing another person's private information will be rejected. Uploaded files must be what they claim to be. Files that attempt to exploit the upload handling are treated as a Terms violation.","u":"/docs/publishing/images"},{"p":"publishing/review","t":"Review","d":"What happens between submitting an app and seeing it live in the gallery.","k":"review, approval, pending, rejected, moderation","h":[["States","states"],["What reviewers check","what-reviewers-check"],["Edits that trigger re-review","edits-that-trigger-re-review"],["After approval","after-approval"]],"b":"Review Every listing is reviewed by an admin before it appears in the gallery, and again after significant edits. States State Meaning Pending Submitted and waiting for review. Not visible in the gallery. Approved Live in the gallery, ratable and commentable. Rejected Not published. You can fix the issues and resubmit. Hidden Removed from the gallery after approval. What reviewers check The link resolves and leads to the app described. The description matches what the app actually does. Images meet the image standards and are appropriate. The app does not collect credentials or payments outside PortModels. The category is the closest available match. Edits that trigger re-review Changing the link URL, description, or category returns an approved app to pending. The outbound link is the part users trust most, so a changed destination is always re-checked. Cosmetic changes — fixing a typo in the tagline, swapping a screenshot — are lighter-touch, but assume any edit may be reviewed. After approval Approval is not permanent. A listing can be hidden or removed later if the app stops working, changes what it does, or generates a pattern of reports. See the Terms.","u":"/docs/publishing/review"},{"p":"publishing/pricing-and-markup","t":"Pricing and markup","d":"How your app's price is built from provider cost plus your margin, and how to choose it.","k":"pricing, markup, margin, cost, revenue, monetization","h":[["How a charge is built","how-a-charge-is-built"],["Choosing a markup","choosing-a-markup"],["Keeping cost down","keeping-cost-down"],["What is not charged","what-is-not-charged"]],"b":"Pricing and markup There are no subscription tiers to design. Your app earns on usage: every metered request carries provider cost plus your markup, and the markup is your revenue. How a charge is built For each request PortModels records the model used, the tokens or images consumed, the provider cost derived from the model's pricing rules, and the markup applied. The total is deducted from the user's credit balance in one atomic operation. Choosing a markup Consider three things: Value per run. An app that saves someone an hour can carry a markup that a novelty app cannot. Model choice. A cheaper model with better prompting often nets more margin than a premium model at the same end price. Comparability. Users can compare your app with similar ones by running a small job on each and looking at the charge. Pricing far above the field needs to be visibly better. Keeping cost down Trim the context you send. Most apps resend more history than they need. Cache what doesn't change between runs in the key/value store rather than regenerating it. Pick the smallest model that passes your own quality bar, and only escalate for the hard cases. Set sensible maxtokens. Runaway generations cost the user money and cost you goodwill. What is not charged Opening your app costs nothing — only metered API requests do. A request that produces no usable completion is recorded as an empty completion rather than billed as a successful run.","u":"/docs/publishing/pricing-and-markup"},{"p":"publishing/earnings-and-payouts","t":"Earnings and payouts","d":"Where your revenue accrues, what the dashboard shows, and how to withdraw.","k":"earnings, payout, revenue, withdraw, dashboard, balance","h":[["Where earnings come from","where-earnings-come-from"],["The dashboard","the-dashboard"],["Requesting a payout","requesting-a-payout"],["Records","records"]],"b":"Earnings and payouts Where earnings come from Every metered request your app makes accrues the markup portion to your developer balance. Nothing is estimated: earnings are derived from the same call logs used to charge the user. The dashboard Your developer dashboard shows, per app: requests over time, credits spent by users, provider cost, your earnings, errors and empty completions. Use the error and empty-completion counts as a quality signal. A rising empty rate usually means a prompt or model change went wrong, and it costs you earnings before it costs you ratings. Requesting a payout Request a payout from the dashboard once your balance clears the minimum. The request is reviewed and then paid out to the payout details on your developer profile. Payout status is visible in the dashboard from request to completion. Keep your payout details current — a payout blocked on stale details is the most common delay. Records Earnings, payout requests, and completed payouts are all recorded against your developer account, so your dashboard totals reconcile with what you were actually paid.","u":"/docs/publishing/earnings-and-payouts"},{"p":"connect/overview","t":"Overview","d":"What Connect with PortModels gives your app, and which flow to pick.","k":"connect, oauth, authorization, overview, identity, token","h":[["What you get","what-you-get"],["Which flow do I need?","which-flow-do-i-need"],["Isolation","isolation"],["Hosts","hosts"],["Next","next"]],"b":"Connect with PortModels Connect lets your app's users link their PortModels account to your app. Once connected, your backend holds an access token that can — depending on the scopes the user approved — run AI models billed to their credits (with your app's markup as your revenue), store per-user data, and read and write a per-user file folder. It is a standard OAuth 2.0 authorization-code flow. Confidential clients (your backend holds a clientsecret) and public clients (no secret — desktop and CLI apps, via PKCE or the device flow) are both supported. One app connection can have several live access tokens. A login on device B does not sign out device A: each successful authorization or device-flow login gets its own token and keeps the scopes approved for that login. Disconnecting the app from Settings → Connected apps revokes every token for that app and user. What you get Capability Scope Endpoint Who the user is profile.read GET /connect/userinfo The user's email address profile.email GET /connect/userinfo Run models on their credits models.run POST /openai/v1/chat/completions Per-user key/value storage kv.read / kv.write /connect/kv Per-user file folder files.read / files.write /connect/files Which flow do I need? Your app Flow Why Web app with a backend Authorization code Your server can keep a clientsecret Browser-only app, no backend PKCE No secret to leak Desktop app PKCE Can open a browser and receive a redirect CLI, TV, headless Device flow No redirect available at all Isolation KV entries and files are namespaced per (app, user). You can never see data another app stored for the same user, and users can revoke your connection at any time from Settings → Connected apps — the access token dies immediately. Hosts The browser consent page is served at https://ap","u":"/docs/connect/overview"},{"p":"connect/enable-connect","t":"Enable Connect","d":"Configure redirect URIs, scopes, and client type, and get your client ID and secret.","k":"enable, client id, client secret, redirect uri, confidential, public","h":[["Add your redirect URIs","add-your-redirect-uris"],["Tick the scopes your app may request","tick-the-scopes-your-app-may-request"],["Pick your client type","pick-your-client-type"],["Your client ID","your-client-id"]],"b":"Enable Connect In the PortModels console → Apps → your app → Connect with PortModels: Add your redirect URIs Redirect URIs are matched exactly. https is required; http is allowed only for localhost. Device-flow-only public clients don't need any. Exact match means query strings and trailing slashes matter. Register every URI you will actually use, including your development one. Tick the scopes your app may request An app can only ever request scopes enabled here. Enable the minimum you need — users see the list on the consent screen, and an app asking for file access it doesn't use converts worse. See Scopes. Pick your client type Confidential (default) — your app has a backend that can keep a secret. Click Enable — generate client secret and copy the clientsecret (pmcs...). It is shown once. Public — desktop and CLI apps that cannot hold a secret. There is no button to click: saving your redirect URIs and scopes with the public toggle on enables Connect. Authenticate with PKCE or the device flow instead of a secret. Your client ID Your clientid is your app id either way, which is always / : URL-encode the slash as %2F when you put it in an authorize link: Never ship a clientsecret in a desktop binary, a browser bundle, or a public repository. If a secret is exposed, regenerate it in the console — the old one stops working immediately.","u":"/docs/connect/enable-connect"},{"p":"connect/authorization-code","t":"Authorization code flow","d":"The standard server-side flow for confidential clients, from consent screen to access token.","k":"authorization code, oauth, confidential, redirect, state, token exchange","h":[["Send the user to the consent screen","send-the-user-to-the-consent-screen"],["Exchange the code for a token","exchange-the-code-for-a-token"],["Store the token for this login","store-the-token-for-this-login"],["Complete example","complete-example"]],"b":"Authorization code flow For confidential clients — apps with a backend that can hold a clientsecret. Send the user to the consent screen Parameter Required Notes clientid yes Your app id, slash URL-encoded as %2F redirecturi yes Must exactly match a registered URI scope yes Space-separated state recommended Your CSRF token — random per attempt, verified on return The user signs in if needed, sees a consent screen listing exactly what you asked for, and on approval is redirected to: On deny you get ?error=accessdenied&state=... instead. Always verify state matches the value you generated for this attempt before you exchange the code. Skipping it leaves you open to CSRF on the callback. Exchange the code for a token The code is single-use and expires in 10 minutes. Exchange it from your backend — never expose your clientsecret to a browser. Note that clientid in the JSON body is not URL-encoded — the %2F encoding applies only to the query string of the authorize link. Store the token for this login Store accesstoken securely for the corresponding app user, session, or device installation. Every successful code exchange creates an independent token, so connecting on device B leaves a token already held by device A working. Each token keeps the scopes approved in its own grant. Connect tokens currently have no expiry timer. A token stops working when the user disconnects the app, the account becomes unavailable, or a future scope policy gives that token an expiry. Disconnecting from Settings → Connected apps revokes every token for that app and user. If your app intentionally keeps one shared server-side session per user, it may replace its own stored token when a new login completes. That is your storage policy; PortModels does not invalidate the older token. Treat a 401 a","u":"/docs/connect/authorization-code"},{"p":"connect/pkce","t":"PKCE for public clients","d":"The authorization code flow without a client secret, for desktop and browser-only apps.","k":"pkce, public client, code verifier, code challenge, S256, desktop","h":[["Generate a verifier and challenge","generate-a-verifier-and-challenge"],["Redirect with the challenge","redirect-with-the-challenge"],["Exchange with the verifier","exchange-with-the-verifier"],["Desktop apps","desktop-apps"],["Common mistakes","common-mistakes"]],"b":"PKCE for public clients For apps that can't hold a secret — desktop apps, or a browser-only client with no backend. Standard OAuth PKCE, S256 only. Generate a verifier and challenge Generate once per attempt, before redirecting the user: Redirect with the challenge codechallenge is required on the authorize call for public clients — omitting it is rejected before a code is ever minted. Exchange with the verifier No clientsecret. The codeverifier proves you started the flow: The response is the same shape as the confidential flow: Each exchange creates a separate token for that browser or desktop installation. Running PKCE again on another device does not invalidate a token you already issued. Desktop apps A desktop app opens the system browser and listens on a loopback redirect URI: Bind a local HTTP server on a port you registered, e.g. http://localhost:5173/callback. Open the authorize URL in the user's browser. Capture code from the loopback request, exchange it, and shut the local server down. Register the exact loopback URI in the console — matching is exact, so http://localhost:5173/callback and http://127.0.0.1:5173/callback are different URIs. If your app cannot open a browser at all, use the device flow. Common mistakes Symptom Cause 404 at /connect/authorize The browser endpoint is on app.portmodels.com; use api.portmodels.com for token and device API calls invalidgrant at exchange Verifier mismatch, exact redirect mismatch, an expired or already-used code, or a second exchange attempt Rejected at authorize Public client sent no codechallenge Redirect mismatch The URI at exchange differs from the one at authorize Works once, then fails Codes are single-use; generate a fresh verifier per attempt If a fresh code fails on its first exchange after you have indepen","u":"/docs/connect/pkce"},{"p":"connect/device-flow","t":"Device flow","d":"Authorize a CLI or headless app with a short user code and polling — no redirect required.","k":"device flow, cli, headless, user code, polling, device code","h":[["Start a device session","start-a-device-session"],["Show the user the code","show-the-user-the-code"],["Poll for the token","poll-for-the-token"],["Polling loop","polling-loop"]],"b":"Device flow For CLI tools and similar — no redirect at all. The app starts a session, shows the user a short code, and polls until they approve it on another device. Public clients only. Start a device session Show the user the code Print usercode and verificationuri, or open verificationuricomplete directly if you can. The session expires after expiresin seconds. Poll for the token Poll no faster than interval seconds: While waiting, this returns 400 with: Error Meaning What to do authorizationpending The user hasn't approved yet Keep polling slowdown You polled before interval elapsed Back off, then continue accessdenied The user declined Stop; tell the user expiredtoken The device code expired Stop; start a new session Once approved you get 200 with the same token shape as /connect/token. The device login receives its own token and does not invalidate another active login for the same app and user. The user can therefore keep a desktop, CLI, or browser session connected at the same time. Polling loop Store the resulting token in the OS keychain rather than a plaintext dotfile. It grants everything the user approved until they disconnect your app.","u":"/docs/connect/device-flow"},{"p":"connect/scopes","t":"Scopes","d":"The complete list of Connect permissions and what each one grants.","k":"scopes, permissions, profile.read, profile.email, models.run, kv.read, kv.write, files.read, files.write","h":[["The list","the-list"],["Rules","rules"],["Choosing scopes","choosing-scopes"],["Checking what you got","checking-what-you-got"]],"b":"Scopes A scope is a single named permission on a token. Request only what your app actually uses — users see the list on the consent screen. The list Scope Grants profile.read GET /connect/userinfo — user id and username profile.email GET /connect/userinfo — the user's email address models.run OpenAI-compatible API calls billed to the user with your app markup kv.read Read per-user key/value data (scoped to your app) kv.write Write per-user key/value data files.read Read the user's folder for your app files.write Write and delete files in that folder Scopes are space-separated in the scope parameter: Rules An app can only request scopes that are enabled for it in the console. See Enable Connect. Calling an endpoint without the scope for it returns 403. Read and write are separate. kv.read does not imply kv.write. The granted scopes come back on the token response in scope, and are also reported by /connect/userinfo. Check them rather than assuming. Choosing scopes If your app Request Only runs models, keeps nothing models.run Remembers settings between sessions models.run kv.read kv.write Personalizes with the user's name add profile.read Signs the user into your own account system add profile.read profile.email Produces documents the user keeps add files.read files.write Asking for storage scopes an app never uses is a common reason users decline at the consent screen, and a common reason a listing draws complaints. Checking what you got Degrade gracefully when a scope is missing: an app that hard-fails because the user declined files.write is worse than one that keeps working without saved output.","u":"/docs/connect/scopes"},{"p":"connect/using-the-token","t":"Using the token","d":"Every endpoint a Connect access token can reach, with examples.","k":"access token, bearer, userinfo, models, kv, files, requests","h":[["One token per login","one-token-per-login"],["Run models (models.run)","run-models-models-run"],["Who is this user? (profile.read)","who-is-this-user-profile-read"],["Per-user key/value store (kv.read / kv.write)","per-user-key-value-store-kv-read-kv-write"],["Per-user files (files.read / files.write)","per-user-files-files-read-files-write"],["Errors you should handle","errors-you-should-handle"]],"b":"Using the token All calls authenticate the same way: One token per login An authorization-code exchange or device-flow approval issues a token for that login. Multiple devices can stay connected to the same app at once, and each token keeps its own approved scopes. Store tokens by your own user plus session or device installation when your app needs to support those sessions independently. Disconnecting the app from Settings → Connected apps revokes all of its tokens for that user. Reconnecting afterwards creates a new token; it does not restore a token that was revoked. Run models (models.run) Same OpenAI-compatible API as regular API keys: Billing: the user's credits are charged provider cost plus your app's markup rate, and your developer share of the markup accrues to your earnings exactly like calls made inside your app. Your app's allowedmodels list applies. See Chat completions for the full request and response shape. Who is this user? (profile.read) Request profile.email as well when your app needs to associate the account with an email address. The response then includes an email field; email is kept separate from profile.read so apps do not receive contact details by default. Per-user key/value store (kv.read / kv.write) Limits: values up to 64KB, up to 1000 keys per user. See Key/value store. Per-user files (files.read / files.write) Backed by S3-compatible storage under users/{user}/apps/{your app}/.... Files are up to 10MB each, and .. traversal is rejected. See File storage. Errors you should handle Status Meaning 400 invalidgrant on /token — bad, expired, or replayed code, bad redirect, or invalid PKCE codeverifier 400 authorizationpending / slowdown / accessdenied / expiredtoken on /device/token 401 Token invalid, expired, or connection revoked — re-run ","u":"/docs/connect/using-the-token"},{"p":"connect/revocation","t":"Revocation and token lifetime","d":"When a Connect token stops working, and how your app should recover.","k":"revoke, token lifetime, 401, disconnect, reconnect, expiry, devices","h":[["What invalidates a token","what-invalidates-a-token"],["Handling 401","handling-401"],["Handling multiple logins","handling-multiple-logins"],["Revoking from your side","revoking-from-your-side"],["Don't cache failure","don-t-cache-failure"]],"b":"Revocation and token lifetime Each Connect login receives its own access token. Tokens currently have no expiry timer, but they are not permanent credentials. What invalidates a token The user disconnects your app. From Settings → Connected apps the user can revoke the connection; every token for that app and user dies immediately. That token expires under a scope policy. No current scope has a timer, but the API may add one in the future. The account is closed or suspended. All tokens for the unavailable account stop working. Connecting again does not invalidate an older active token. Device A remains connected when device B completes the flow. If you want to remove every login, disconnect the app from Settings → Connected apps. Any of these surfaces as 401 on your next call. Handling 401 Treat 401 as reconnect required, not as a retryable error. Retrying the same token will never succeed. In your UI, a ReconnectRequired should render as a \"Reconnect your PortModels account\" button, not as an error page. The affected token may have been revoked, expired, or tied to an unavailable account; another device's token can still be working. Handling multiple logins If a user runs the connect flow twice — from two browsers, or after reinstalling your desktop app — both tokens can remain active. Store each token keyed by your own user id plus the session or device installation that owns it. Do not overwrite a different active device's token unless that is an explicit choice in your app's session model. Revoking from your side If your app no longer needs access, stop using the token and delete your stored copy. Tell the user they can also remove the connection from Settings → Connected apps, which is the record they see. Don't cache failure A revoked token is a normal state, not ","u":"/docs/connect/revocation"},{"p":"api/authentication","t":"Authentication","d":"API keys and Connect tokens, how they differ, and how to send them.","k":"api key, authentication, bearer, token, pm_, pmc_, authorization header","h":[["Two kinds of credential","two-kinds-of-credential"],["Creating an API key","creating-an-api-key"],["Handling keys safely","handling-keys-safely"],["Base URL","base-url"],["Failure modes","failure-modes"]],"b":"Authentication Every request carries a bearer token: Two kinds of credential Credential Prefix Who it acts as Who pays API key pm You Your credits Connect access token pmc A connected user The user's credits Use an API key for your own scripts, backends, and testing. Use a Connect access token when your published app runs models on a user's behalf — that is what makes the run billable to the user and your markup payable to you. See Connect overview. Creating an API key Create keys from your account settings. The full key is shown once, at creation: PortModels stores only a hash of the key plus a short preview (pm9F3..ZQ2) so you can tell your keys apart in the list. There is no way to recover a lost key — revoke it and create a new one. Handling keys safely Keep keys in environment variables or a secret manager, never in source control. Use a separate key per deployment so one can be revoked without taking down the others. Revoke immediately if a key is exposed; revocation takes effect at once. Never put an API key in browser JavaScript — anyone loading your page can read it. Base URL The OpenAI-compatible surface is mounted under /openai/v1, so a client library that takes a base URL should be pointed at: Failure modes Status Meaning 401 Missing header, malformed header, unknown key, revoked key, or revoked connection 403 Authenticated, but not allowed to do this — for a Connect token, usually a missing scope 429 Rate limited — honor Retry-After A 401 on a Connect token means that token is no longer usable; the app may still have other active device sessions. Send only the affected login back through Connect rather than retrying. See Revocation and token lifetime.","u":"/docs/api/authentication"},{"p":"api/chat-completions","t":"Chat completions","d":"The main endpoint — request shape, streaming, parameter passthrough, and cost reporting.","k":"chat, completions, messages, streaming, sse, model, usage, cost","h":[["Request","request"],["Parameter passthrough","parameter-passthrough"],["Response","response"],["Streaming","streaming"],["Using an OpenAI client library","using-an-openai-client-library"],["On behalf of a user","on-behalf-of-a-user"]],"b":"Chat completions OpenAI-compatible, so existing clients work by changing the base URL and the key. Request Field Required Notes model yes A model key from /v1/models messages yes List of {role, content} objects; both fields are strings stream no true for server-sent events sessionid no Conversation identifier, 1–1,024 bytes; reuse it across turns and use a different value for each conversation provider no With providerkey, pins the request to an enabled provider route providerkey no Encrypted pmbyok:v1 envelope produced by a trusted client; never plaintext Parameter passthrough You can send model parameters such as temperature, maxtokens, topp, stop, responseformat, tools, and reasoning-effort settings. Support depends on the selected model. PortModels request fields include action, model, messages, stream, streamoptions, sessionid, routing, provider, providerkey, appid, connected, and user. For the browser setup flow, threat model, and billing details, see Bring your own provider key. If a request fails after you add an optional parameter, check that the selected model supports it. See Errors for retry and support guidance. Response usage.cost is a PortModels addition: what this request actually cost, in credits, already including the app markup where one applies. Standard OpenAI clients ignore the extra field. Streaming Set \"stream\": true and read server-sent events: A cost event is emitted immediately before the [DONE] terminator, so a streaming client can report spend the same way a non-streaming one does. Using an OpenAI client library On behalf of a user Swap the API key for the user's Connect access token — nothing else changes: The token must carry the models.run scope, the model must be in your app's allowedmodels list, and the charge lands on the user's credit","u":"/docs/api/chat-completions"},{"p":"api/images","t":"Image generation","d":"Generating images through the OpenAI-compatible images endpoint.","k":"images, generation, dall-e, prompt, size, n, b64_json","h":[["Request","request"],["Response","response"],["Notes","notes"]],"b":"Image generation Request Field Required Notes prompt yes What to generate model no Defaults to dall-e-3 n no Number of images size no Provider-supported dimensions, e.g. 1024x1024 Response Providers return either a url or a b64json payload per image; whichever the provider sent is what you get, alongside revisedprompt when the provider supplies one. usage.cost is the credit cost of the generation, markup included. Notes Image pricing is per image and varies sharply between models. Check the cost of one generation before you loop. Hosted image URLs from providers are usually short-lived. If the user needs to keep an image, download it and store it with file storage rather than saving the URL. The prompt field is required; a request without it returns 400.","u":"/docs/api/images"},{"p":"api/models","t":"Models","d":"Listing available models and reading the details of one.","k":"models, list, model key, catalog, owned_by, allowed models","h":[["List models","list-models"],["Get one model","get-one-model"],["Choosing a model","choosing-a-model"],["Allowed models for published apps","allowed-models-for-published-apps"]],"b":"Models List models The id is the model key — the exact string to send as model in a chat completion. ownedby identifies the model's owner. Get one model Model keys often contain a slash. The path accepts it directly, so no encoding is needed here. Choosing a model Don't hard-code a single model forever. The catalogue changes; read the list at startup and fall back gracefully. Match the model to the job. Most app work does not need the largest model available, and the price difference is large. See Pricing and markup. Test cost, not just quality. Run the same realistic prompt through two candidates and compare usage.cost. Allowed models for published apps A published app has an allowedmodels list. When your app runs models with a Connect token, that list applies — a model outside it is rejected even if it appears in /v1/models. Keep the list current as you change which models your app uses.","u":"/docs/api/models"},{"p":"api/usage-and-logs","t":"Usage and logs","d":"What PortModels records for every request, and how to read it back.","k":"usage, logs, call logs, metering, cost, empty completion, analytics","h":[["What is recorded","what-is-recorded"],["Reading cost per request","reading-cost-per-request"],["Where to see aggregates","where-to-see-aggregates"],["Empty completions","empty-completions"],["Practical monitoring","practical-monitoring"]],"b":"Usage and logs Every metered request writes a call log. It is the same record that charges the user, pays your markup, and populates the dashboards — there is no separate estimate anywhere. What is recorded The model and provider used. Token counts, or images generated. Provider cost. Markup applied and the total charged. Outcome: success, error, or empty completion. The app the call came from, when it came through a Connect token. Reading cost per request The cheapest place to see cost is the response itself: Streaming responses emit the same figure as a usage event just before [DONE]. See Chat completions. Where to see aggregates Users see per-charge history on their balance page, labelled with the app that made the call. Developers see per-app request counts, spend, cost, earnings, and error rates in the developer dashboard. See Earnings and payouts. Empty completions A request where the model returns nothing usable is recorded as an empty completion rather than billed as a successful run. Watch this number: a rising empty rate almost always means a prompt, parameter, or model change went wrong, and it costs earnings before it costs ratings. Practical monitoring Track three numbers per app and alert on changes: Metric What a spike means Error rate A provider problem, a bad parameter, or an expired credential Empty completion rate A prompt or model regression Cost per successful run Context growth — usually resending history you don't need","u":"/docs/api/usage-and-logs"},{"p":"api/errors","t":"Errors","d":"The error envelope, every status code the API returns, and how to handle each.","k":"errors, status codes, 400, 401, 403, 429, invalid_request_error, retry","h":[["Envelope","envelope"],["Status codes","status-codes"],["Common messages","common-messages"],["Retry policy","retry-policy"],["Insufficient credits","insufficient-credits"]],"b":"Errors Envelope Errors return a JSON body with a single error object: Validation failures add an errors field with per-field detail: Status codes Status Type Cause Handling 400 invalidrequesterror Missing or malformed field, unsupported parameter, insufficient credits Fix the request; don't retry unchanged 401 invalidrequesterror Missing, malformed, unknown, or revoked credential Re-authenticate; for a Connect token, reconnect 403 invalidrequesterror Access denied — missing scope, or a model outside allowedmodels Request the scope, or change the model 404 notfound Unknown path or model Check the URL and model key 413 — File upload over 10MB Split or compress before uploading 429 ratelimiterror Rate limited Back off; honor Retry-After For persistent errors, contact support with the response's logid, if present, the model key, and the time of the request. Common messages Message Meaning model is required No model in the body messages must be a list messages missing or not an array prompt is required Image generation without a prompt Token is missing the models.run scope Connect token can't run models Invalid or revoked connection token The user disconnected your app Invalid API key Unknown, revoked, or a suspended account Rate limit exceeded Too many requests from this IP Retry policy Situation Retry? 429 Yes — after Retry-After, with exponential backoff Timeout or 5xx Yes — a small number of times, with backoff 400 No — the request is wrong 401 No — get a new credential first 403 No — the permission has to change Insufficient credits When a user's balance can't cover a request, the request is rejected before the model runs — the balance is never left negative. Surface this to the user as \"add credits\", not as a generic failure. See Credits.","u":"/docs/api/errors"},{"p":"api/rate-limits","t":"Rate limits","d":"The request ceiling on the API, the headers to read, and how to stay under it.","k":"rate limit, 429, retry-after, throttling, backoff, quota","h":[["Handling 429","handling-429"],["Staying under the limit","staying-under-the-limit"],["Note on shared IPs","note-on-shared-ips"]],"b":"Rate limits The OpenAI-compatible API is rate limited per client IP at 60 requests per minute on a rolling window. Exceeding it returns: Handling 429 Read Retry-After — it is in seconds. Wait at least that long. Retry with exponential backoff and jitter if it happens again. Never retry a 429 immediately in a tight loop. It extends the window and makes the stall longer for every request behind it. Staying under the limit Queue, don't fan out. A worker pool with a fixed concurrency is far more predictable than firing every request at once. Batch on your side. Combining ten short prompts into one request is usually cheaper as well as fewer requests. Cache. Identical inputs producing identical outputs should not hit the API twice — the key/value store is a reasonable cache for per-user results. Stream long generations. Streaming doesn't reduce request count, but it does stop clients from re-issuing a request they think has hung. Note on shared IPs The limit is keyed on the source IP. A server-side integration behind one egress IP shares one budget across all of its users, so a busy backend should implement its own queue rather than relying on the ceiling being per-user. Separately from rate limits, a request is rejected if the paying account's credit balance can't cover it. See Errors.","u":"/docs/api/rate-limits"},{"p":"app-data/key-value-store","t":"Key/value store","d":"Small per-user JSON values scoped to your app — settings, state, and caches.","k":"kv, key value, storage, preferences, state, json","h":[["Store a value","store-a-value"],["Read a value","read-a-value"],["List keys","list-keys"],["Delete a value","delete-a-value"],["Limits","limits"],["Isolation","isolation"],["Patterns that work well","patterns-that-work-well"]],"b":"Key/value store A per-user JSON store scoped to your app. Use it for settings, saved state, drafts, and small caches — the things that should survive between sessions without you running a database. Requires the kv.read and kv.write scopes. Store a value The body must be a JSON object with a value field. value itself can be any JSON — object, array, string, number, boolean. Read a value A missing key returns 404 with {\"error\": \"key not found\"}. List keys Delete a value Limits Limit Value Key length 1–256 characters Value size 64KB encoded Keys per user, per app 1000 Exceeding the value size returns 400 with value exceeds 64KB limit; exceeding the key count returns 400 with key limit reached (1000 keys per user per app). For anything larger than 64KB, use file storage. Isolation Entries are namespaced per (app, user). Your app cannot see what another app stored for the same user, and cannot see another user's entries. When the user disconnects your app your token stops working, but the data stays until it is deleted. Patterns that work well Namespace your keys. A flat namespace gets crowded at 1000 keys. Prefix by purpose: pref:theme, draft:2026-08-01, cache:models. Read once per session. Load the user's settings at the start of a session and keep them in memory rather than re-reading on every interaction. Use it as a cache. Storing a derived result the user will need again is usually cheaper than regenerating it — see Pricing and markup. Version your shape. Store {\"v\": 2, ...} so an older client can recognize a newer format instead of misreading it. Degrade gracefully when the user declined the storage scopes. An app that still works — just without remembering anything — beats one that refuses to start.","u":"/docs/app-data/key-value-store"},{"p":"app-data/file-storage","t":"File storage","d":"A per-user file folder scoped to your app, including presigned uploads for large files.","k":"files, storage, upload, download, s3, presign, folder","h":[["Upload","upload"],["Download","download"],["List","list"],["Delete","delete"],["Paths","paths"],["Large files: presigned URLs","large-files-presigned-urls"],["Storage quota","storage-quota"],["Cross-app folders","cross-app-folders"]],"b":"File storage Each user gets a private folder per app, backed by S3-compatible storage under: Requires the files.read and files.write scopes. Upload The body is the raw file — not JSON, not multipart: Direct uploads through the API are capped at 10MB. Larger files return 413 — use a presigned upload. Download List prefix is optional and filters within the user's folder for your app. Delete Paths Paths are relative to the user's folder for your app. .. traversal is rejected — you cannot reach another app's folder or another user's data this way. Use / to organize: Large files: presigned URLs For files over the 10MB proxy limit, ask for a time-limited URL and upload directly to storage, bypassing the API server: Then PUT the bytes to the returned URL. Downloads have the same shortcut: Presigned URLs are time-limited. Generate one when you need it rather than storing it. Presigned URLs require a real S3 endpoint. A development server running the in-memory storage backend returns 501 for these routes — use the direct PUT path in development. Storage quota Private-folder writes are metered against the connected user's storage subscription. Once a write would take the user past their quota, the request returns 402 with a storage quota exceeded error. Surface this to the user as \"upgrade or free up space\", not as a generic failure. See App Data Layer plans. Cross-app folders An app can be granted access to another app's folder for the same user through the concrete scopes files.read:{appid} and files.write:{appid}, using: These scopes are only ever grantable between apps owned by the same developer, and the user still has to approve them. Use this when you ship a suite of apps that share output — not as a way around isolation.","u":"/docs/app-data/file-storage"},{"p":"app-data/public-assets","t":"Public app assets","d":"A world-readable bucket for your app's own static files.","k":"public, assets, appdata, icons, static, bucket, cdn","h":[["Reading — public","reading-public"],["Writing — developer only","writing-developer-only"]],"b":"Public app assets Separate from per-user file storage, each app has a public folder for files that belong to the app rather than to any user: icons, screenshots, sample data, and other static assets you want publicly reachable. Reading — public Anyone can read. No token required: List the folder: Writing — developer only Uploads and deletes require you to be authenticated as the app's owning developer: Presigned upload and download URLs exist here too, under /appdata/{appid}/presign-upload/{path} and presign-download/{path}. Uploads are developer-only; the download shortcut is public, like the plain download route. This bucket is world-readable by design. Never put user data, credentials, or anything private here — use file storage for anything that belongs to a user.","u":"/docs/app-data/public-assets"},{"p":"app-data/choosing-storage","t":"Choosing where to store","d":"Which of the three storage surfaces fits which kind of data.","k":"storage, comparison, kv, files, public, choosing, limits","h":[["Quick decisions","quick-decisions"],["What not to assume","what-not-to-assume"]],"b":"Choosing where to store PortModels gives an app three places to put data. They are not interchangeable. Key/value Files Public assets Belongs to A user, in your app A user, in your app Your app Readable by Your app, with kv.read Your app, with files.read Anyone Written by Your app, with kv.write Your app, with files.write You, as the developer Size limit 64KB per value 10MB direct, more via presign Large, via presign Count limit 1000 keys per user Storage quota — Good for Settings, state, small caches Documents, exports, media Icons, screenshots, samples Quick decisions Settings, preferences, session state. Key/value. Small, structured, read on every session. Something the user produced and wants back. Files. A generated report, an export, a transcript. Something big. Files, via a presigned upload. Don't try to base64 a 30MB file into a 64KB key/value entry. Your app's own icon. Public assets. It's the same for every user and it's not private. A cache of an expensive result. Key/value if it's small and per-user; files if it's large. Either way you're saving the user credits. A password or an API key of your own. None of these. Keep your own secrets in your own infrastructure — per-user storage is readable by your app, which means anything you put there is only as safe as your app. What not to assume Storage is not a database. There are no queries, no indexes, and no transactions across keys. Structure your keys so you can find things by name. Data outlives access. Revoking your app stops your token, but does not delete what you stored. Give users a way to clear their data from inside your app. The user may decline. Scopes are optional at the consent screen. Design for the case where you got models.run and nothing else.","u":"/docs/app-data/choosing-storage"},{"p":"account-and-billing/your-account","t":"Your account","d":"One account for browsing, running apps, publishing, and spending credits.","k":"account, profile, email, password, login, session, refresh","h":[["Signing in","signing-in"],["What your profile holds","what-your-profile-holds"],["Multiple roles, one account","multiple-roles-one-account"]],"b":"Your account A single PortModels account covers everything: browsing the gallery, running apps, holding a credit balance, and publishing apps of your own. There is no separate developer account to create — see Becoming a developer. Signing in Sign in with your PortModels account. A successful login returns a website session that your browser keeps for you; the API equivalent is auth.login, which returns a token pair. Action What it does auth.register Creates an account auth.login Returns access and refresh tokens auth.refresh Exchanges a refresh token for a fresh access token auth.logout Ends the session auth.me Returns the current profile Access tokens are short-lived by design. Refresh rather than re-prompting for a password. What your profile holds Your display name and email, your credit balance, the apps you have published, the apps you have connected, and your comment and rating history. Your email is never shown to other users. A connected app receives email only when you approve its separate profile.email scope; it never receives your password. Multiple roles, one account The same account can be a user of other people's apps and the publisher of your own. Credits you spend running an app and credits you earn from your own apps are tracked separately: spending draws down your balance, earnings accumulate as developer earnings and are paid out. See Earnings and payouts.","u":"/docs/account-and-billing/your-account"},{"p":"account-and-billing/buying-credits","t":"Buying credits","d":"How top-ups work, what a credit is worth, and how payment is confirmed.","k":"credits, buy, top up, checkout, stripe, payment, balance, usd","h":[["Topping up","topping-up"],["When credits arrive","when-credits-arrive"],["Checking your balance","checking-your-balance"],["Running out mid-task","running-out-mid-task"]],"b":"Buying credits Credits are the unit of account across PortModels. 1 credit = 1 USD. Every model call — whichever app makes it — is priced in credits and drawn from your balance. Topping up Choose an amount and you are sent to a hosted Stripe Checkout page. Payment details are entered on Stripe's page, not on PortModels. Minimum top-up 1 credit (USD 1.00) Maximum top-up 10,000 credits Currency USD Processor Stripe The API equivalent is credits.checkout, which takes amount, successurl, and cancelurl and returns a checkouturl to send the user to: Redirect URLs must be https, except localhost and 127.0.0.1 during development. When credits arrive Credits are granted when Stripe confirms the payment, not when you are redirected back. The confirmation webhook is the only path that ever adds credits to a balance — so a top-up lands even if you close the browser on the success page, and a cancelled or failed payment never grants anything. If a balance hasn't updated within a minute or two of a successful payment, check Billing history before retrying: a duplicate purchase is easier to avoid than to unwind. Redelivered webhooks are absorbed by the purchase's payment reference, so a single payment can never be credited twice. Checking your balance Your balance is shown in the account area, and returned by credits.balance along with an account snapshot. Running out mid-task A call that would take your balance below zero is rejected before it reaches a provider — you are not charged for it, and the app should tell you your balance is too low rather than failing silently. Top up and retry; nothing else needs resetting.","u":"/docs/account-and-billing/buying-credits"},{"p":"account-and-billing/billing-history","t":"Billing history","d":"Reading your transaction ledger and reconciling charges against usage.","k":"history, transactions, ledger, receipts, invoice, refund, spend","h":[["Transactions","transactions"],["Reconciling a charge against usage","reconciling-a-charge-against-usage"],["Receipts","receipts"],["Disputes and corrections","disputes-and-corrections"]],"b":"Billing history Every change to your balance is a ledger entry. Nothing adjusts a balance without leaving a row. Transactions credits.history returns your entries, newest first, with limit (default 50, max 1000) and offset: Field Meaning type What moved the balance — a purchase, a usage charge, an adjustment amount Credits added or deducted currency The currency actually paid, for purchases paymentref The Stripe checkout session, for purchases status Whether the entry completed Reconciling a charge against usage Transactions tell you that credits left your balance. To see what spent them, use your usage log — every model call records its model, its app, and its cost. See Usage and logs. Working from the two together answers the usual question, \"why did this cost what it cost\": Find the charge in your transaction history. Find the matching calls in your usage log for the same window. Compare the per-call cost against the model's price and your app's markup — see Pricing and markup. Receipts Purchase receipts come from Stripe, to the email on your account. PortModels does not store your card details at any point; the processor holds them. Disputes and corrections If a charge looks wrong, gather the transaction id and the approximate time and contact support. Corrections are made as their own ledger entries rather than by editing history, so the record stays auditable.","u":"/docs/account-and-billing/billing-history"},{"p":"account-and-billing/storage-plans","t":"Storage plans","d":"The free storage allowance, the App Data Layer packages, and how cancellation works.","k":"storage, plans, subscription, quota, app data layer, gigabytes, cancel, upgrade","h":[["The free allowance","the-free-allowance"],["App Data Layer packages","app-data-layer-packages"],["Subscribing","subscribing"],["Cancelling","cancelling"],["When you hit the quota","when-you-hit-the-quota"],["How usage is measured","how-usage-is-measured"]],"b":"Storage plans Files that apps save for you live in your private per-app folders — see File storage. Those bytes count against a storage quota. The free allowance Every account gets 500MB free, with no subscription. Most apps that save settings, notes, and modest exports never come close. App Data Layer packages When you need more, subscribe to a monthly package. Quota is account-wide — shared across every app you connect, not per-app. Tier Storage Per month 5gb 5 GB €2.00 20gb 20 GB €5.00 50gb 50 GB €10.00 100gb 100 GB €18.00 250gb 250 GB €39.00 500gb 500 GB €69.00 1tb 1 TB €119.00 Subscriptions are billed in EUR and are separate from credits — credits pay for model usage, a storage plan pays for capacity. Subscribing storagesubscription.packages lists the tiers and the free allowance; storagesubscription.checkout takes a tier, a successurl, and a cancelurl and returns a hosted Stripe Checkout URL. As with credits, the subscription activates only when Stripe confirms payment. You can hold one active storage subscription at a time. Cancelling storagesubscription.cancel schedules cancellation at the end of the current billing period. Your quota and access continue uninterrupted until then — the period is already paid for — and no refund is issued for the remainder. Changed your mind? storagesubscription.reactivate undoes a pending cancellation any time before it actually lapses. storagesubscription.portal opens the Stripe Billing Portal for payment methods, invoices, and cancellation. When a subscription lapses, your quota drops back to the free allowance. Files already stored are not deleted, but writes that would exceed the lower quota start failing. Export or clear what you need before a plan ends. When you hit the quota A write that would push you past your quota is r","u":"/docs/account-and-billing/storage-plans"},{"p":"account-and-billing/api-keys","t":"API keys","d":"Personal keys for calling the API as yourself, and how to keep them safe.","k":"api key, pm_, token, secret, revoke, rotate, personal","h":[["Creating a key","creating-a-key"],["Using a key","using-a-key"],["Spending","spending"],["Keeping keys safe","keeping-keys-safe"],["Revoking","revoking"]],"b":"API keys An API key lets you call the PortModels API as yourself — from a script, a notebook, or a server you control. It is not the mechanism apps use to act on behalf of other people; that is Connect. Creating a key apikey.create returns a new key beginning with pm: The full key is shown once, at creation. It is stored only as a hash, so nobody — including PortModels support — can retrieve it later. Copy it immediately into wherever it needs to live. apikey.list shows your keys by preview (pmZx3...Qk1) and creation date, so you can tell them apart without revealing them. Using a key See Authentication for how pm keys differ from pmc Connect tokens. Spending Calls made with your key spend your credits. There is no separate budget and no per-key limit, so a runaway loop in a script draws down the same balance you use everywhere else. Watch your usage log when you first automate something. Keeping keys safe Never put a key in client-side code. Anything shipped to a browser or a downloadable app is readable. Keys belong on a server you control. Never commit a key. Use an environment variable and add your env file to .gitignore. One key per purpose. Separate keys for separate scripts mean you can revoke one without breaking the rest. Rotate on suspicion, not on proof. Revoking and reissuing costs a minute; an exposed key costs credits. Revoking apikey.revoke takes a keyid and disables it immediately. Requests using it start failing with 401 right away. If you think a key leaked: revoke it first, then check your usage log for calls you did not make, then issue a replacement.","u":"/docs/account-and-billing/api-keys"},{"p":"account-and-billing/security","t":"Security","d":"What protects your account, what you control, and what to do if something looks wrong.","k":"security, password, phishing, tokens, revoke, breach, safety","h":[["What we hold, and how","what-we-hold-and-how"],["What apps can see","what-apps-can-see"],["Recognising phishing","recognising-phishing"],["If something looks wrong","if-something-looks-wrong"],["Reporting a vulnerability","reporting-a-vulnerability"]],"b":"Security What we hold, and how Your PortModels password is stored and managed by the PortModels console. This website does not collect a second password. API keys are stored hashed too — only a short preview is retained so you can identify them. Card details are never stored by PortModels; the payment processor holds them. What apps can see An app you connect receives a scoped access token. It never receives your password, and it never receives more access than the scopes you approved. An app with models.run cannot read your files; an app with kv.read cannot see another app's data. Review what you have granted under Connected apps, and disconnect anything you no longer use. Recognising phishing PortModels will never ask for your password by email, never ask you to paste an API key into a chat, and never ask you to approve a Connect screen you did not start yourself. Before approving a consent screen, check three things: The app name is the one you meant to use. The scopes match what the app plausibly needs. A wallpaper generator asking for files.read deserves a second look. The URL is on portmodels.com. A consent screen anywhere else is not ours. If something looks wrong Change your PortModels password. This ends other sessions. Revoke your API keys and issue new ones — see API keys. Disconnect apps you do not recognise. Read your history. Compare billing history and your usage log against what you actually ran. Tell us. Contact support with what you found and when. Reporting a vulnerability Found a flaw in PortModels itself? Email hello@portmodels.com with enough detail to reproduce it, and give us a chance to fix it before disclosing publicly. Please don't test against other people's accounts, apps, or data.","u":"/docs/account-and-billing/security"},{"p":"account-and-billing/bring-your-own-provider-key","t":"Bring your own provider key","d":"Use your own provider account while PortModels routes the request without storing a persistent plaintext copy of your key.","k":"BYOK, bring your own key, provider key, encryption, routing, API key, security","h":[["Before you start","before-you-start"],["Configure a key in the web app","configure-a-key-in-the-web-app"],["How the protection works","how-the-protection-works"],["Use BYOK through the API","use-byok-through-the-api"],["What PortModels can and cannot protect","what-portmodels-can-and-cannot-protect"],["Frequently asked questions","frequently-asked-questions"],["Does PortModels store my provider key?","does-portmodels-store-my-provider-key"],["Can PortModels call the provider without seeing the key?","can-portmodels-call-the-provider-without-seeing-the-key"],["Can I choose any provider for any model?","can-i-choose-any-provider-for-any-model"],["What happens if I rotate my provider key?","what-happens-if-i-rotate-my-provider-key"],["Who pays for a BYOK request?","who-pays-for-a-byok-request"]],"b":"Bring your own provider key Bring your own key (BYOK) lets you use your account with an enabled AI provider while still using PortModels as the consistent interface for model requests. You can choose a configured provider in the web app, or send the encrypted BYOK envelope with an API request. PortModels keeps the public model id and provider route separate: model is the logical model key, such as deepseek-ai/DeepSeek-V3. provider is the provider route you choose. providerkey is an encrypted envelope, never a plaintext provider key. For automatic routing, leave the provider choice on Automatic. To pin a request to a provider, first configure an encrypted key for that provider. Before you start You need an active key from a provider that PortModels has enabled for the model you want to use. The provider account pays the provider's own inference bill. PortModels charges its separate BYOK routing fee after a successful request; the current fee is controlled by PortModels and shown in the product. Your provider's terms, limits, and spending controls still apply. Set a limit with the provider if it offers one, and rotate the key at the provider if you ever suspect that it was exposed. Configure a key in the web app Open Settings and choose Provider keys. Select the provider, enter its key, and save it. PortModels encrypts the key in your browser before the key leaves your device. Open Chat and choose the provider key when you want to use that provider. The browser stores only the encrypted envelope for that user and provider. It does not submit or store the plaintext key as a PortModels account value. You can remove the saved envelope from Provider keys when you no longer want that browser to keep it. How the protection works The setup and request flow is: The browser receiv","u":"/docs/account-and-billing/bring-your-own-provider-key"},{"p":"resources/faq","t":"FAQ","d":"Short answers to the questions that come up most often.","k":"faq, questions, answers, help, common","h":[["Accounts and credits","accounts-and-credits"],["Using apps","using-apps"],["Publishing","publishing"],["API and Connect","api-and-connect"]],"b":"FAQ Accounts and credits Is PortModels free to use? Creating an account and browsing the gallery is free. Running apps spends credits, because running apps spends real model capacity. What is a credit worth? 1 credit = 1 USD. Model calls are priced in credits and drawn from your balance. See Buying credits. Do credits expire? No. Your balance sits there until you spend it. Why did an app cost more than I expected? Longer inputs and outputs cost more, and the developer's markup sits on top of the model price. Your usage log shows the per-call breakdown. Do I need a separate balance for each app? No. One account, one balance, every app. Using apps Who wrote the apps in the gallery? Third-party developers. Every listing is reviewed before it goes live, but the app itself is the developer's product. See Your data. What does an app see about me? A scoped token — never your password, and never more than the scopes you approved. Email is only shared when an app requests the separate profile.email permission. How do I stop an app from having access? Disconnect it from Connected apps. Its token stops working immediately. Does disconnecting delete my data? No. The app loses access; what it stored stays until it is deleted. Many apps offer a \"delete my data\" control — use it before disconnecting if you want both. Publishing What does it cost to publish? Nothing. You keep the revenue from your markup — see Earnings and payouts. How long does review take? Listings are reviewed by a person, so it isn't instant. Editing a live listing's link, description, or category sends it back to pending. Can I ship a downloadable app, not a web app? Yes. Use the device flow for anything without a browser redirect — CLIs, desktop apps, TV apps. Can I use my own models or my own API keys? Your app ","u":"/docs/resources/faq"},{"p":"resources/troubleshooting","t":"Troubleshooting","d":"Common failures and what actually causes them.","k":"troubleshooting, errors, debug, 401, 403, 402, 429, problems, fix","h":[["Authentication","authentication"],["Money","money"],["Rate limits and size","rate-limits-and-size"],["Connect flows","connect-flows"],["Development","development"],["Still stuck","still-stuck"]],"b":"Troubleshooting Authentication 401 unauthorized The token is missing, malformed, revoked, or the user disconnected your app. Check the Authorization: Bearer ... header actually reached the server — a missing header and an invalid one look identical from the client side. If a user disconnected, send them back through the authorization flow. 403 forbidden The token is valid but lacks the scope for what you asked. A Connect token needs models.run for model calls, kv.read/kv.write for the key/value store, and files.read/files.write for files. Scopes are fixed at grant time — you cannot widen them without a new authorization. \"It works with my API key but not with a Connect token.\" Different subjects. pm spends your credits; pmc spends the connected user's and is limited by their scopes. Check Authentication. Money 402 on a model call The user's balance is too low. Tell them so plainly and link them to a top-up — don't retry in a loop. 402 storage quota exceeded The user's storage subscription is full. See Storage plans. Credits didn't arrive after a payment. Credits are granted when the payment processor confirms, not on redirect. Wait a moment and check billing history before paying again. Rate limits and size 429 too many requests You crossed 60 requests per minute. Read the retry-after header and back off rather than retrying immediately. If you are fanning out, add a queue. 413 on a file upload Direct uploads are capped at 10MB. Use a presigned upload for anything larger. 400 value exceeds 64KB limit Key/value entries are small by design. Put large payloads in file storage and keep a pointer in the key/value store. Connect flows The consent screen never redirects back. Your redirecturi must match the one registered for the app exactly — scheme, host, port, and path. A t","u":"/docs/resources/troubleshooting"},{"p":"resources/support","t":"Support","d":"Where to ask, and what to include so the answer comes back fast.","k":"support, contact, help, email, bug report, feedback","h":[["Contact","contact"],["What to include","what-to-include"],["Reporting an app","reporting-an-app"],["Reporting a security issue","reporting-a-security-issue"],["Feature requests","feature-requests"]],"b":"Support Contact Email hello@portmodels.com. Before you write, it's worth a minute with Troubleshooting — most errors have a specific, boring cause. What to include The difference between a one-round answer and a five-round thread is usually detail: What you did — the endpoint or the app, and the exact request if there was one. Redact tokens. What happened — the full error body and status code, not a paraphrase. When — a timestamp with a timezone. Logs are searched by time. What you expected instead. Your account email, and the app id if it involves a specific app. Never send an API key, a Connect token, a client secret, or a password. We will never ask for one. If you already pasted a key somewhere, revoke it. Reporting an app If a published app misbehaves — misleading listing, broken link, abusive content, or a scope request that doesn't match what it does — report it from its gallery page or email us. Listings can be hidden or removed after approval. Reporting a security issue See Security. Give us a chance to fix an issue before disclosing it, and don't test against other people's accounts. Feature requests Tell us what you were trying to build and where the platform got in the way. A concrete blocked use case is more actionable than a feature name.","u":"/docs/resources/support"},{"p":"resources/changelog","t":"Changelog","d":"Notable platform changes, newest first.","k":"changelog, releases, updates, history, new","h":[["2026-09","2026-09"],["2026-08","2026-08"],["2026-07","2026-07"],["Earlier","earlier"]],"b":"Changelog Notable platform changes, newest first. Breaking changes are called out explicitly. 2026-09 Multi-device Connect sessions. Each authorization-code exchange and device-flow approval now receives an independent access token. Connecting on another device no longer signs out an existing device; disconnecting the app still revokes every token for that user. 2026-08 Documentation. These docs replaced the single publishing page, with search across every page. Public app assets. Apps can serve their own static files from /appdata — icons, screenshots, and sample data, world-readable, developer-writable. 2026-07 App Data Layer plans. Storage subscriptions now cancel at period end and can be reactivated before they lapse, with a billing portal for payment methods and invoices. See Storage plans. Hosted checkout. Credit top-ups run through hosted Stripe Checkout, and credits are granted only on confirmed payment. Earlier Device flow. Connect gained an RFC 8628 device authorization flow for CLIs, desktop apps, and anything without a browser redirect. See Device flow. PKCE. Public clients can use the authorization code flow with PKCE instead of holding a client secret. Cross-app file scopes. Apps owned by the same developer can share a user's folders through files.read:{appid} and files.write:{appid}, with the user's approval.","u":"/docs/resources/changelog"}]}