Integrating "Login with mycorra"
This guide is for third-party app developers who want their users to sign in with mycorra and run LLM inference — billed to each user's prepaid mycorra credit balance. mycorra fully abstracts inference: there are no providers, models, or API keys for your app (or the user) to manage. You send a prompt, mycorra runs it and charges the user's credits.
mycorra implements the standard OAuth 2.0 authorization-code grant (RFC 6749, §4.1). If you've integrated "Sign in with GitHub/Google", this is the same shape.
- Base URL below is written as
https://mycorra.example— substitute your deployment (locally,http://localhost:8080). - All timestamps are Unix seconds. Authorization codes live 5 minutes; access tokens live 1 hour.
At a glance
1. Once: register your app → get client_id + client_secret (§1). 2. Per user: send them to /oauth/authorize → they consent → you get a code (§2). 3. Per user: exchange code at /oauth/token → access_token (§3). 4. Per request: POST /api/inference with an OpenAI-style chat body (§4).
The access token is the only credential you need for inference. Keep it server-side. It stays valid as long as you use it and expires only after 14 days without a call; refresh by re-running steps 2–3 when a call returns 401. Users buy credits and pick the model on mycorra's side — your app never handles either.
1. Register your app (one time)
The easiest way: sign in to mycorra and open Developers (footer → "For developers"). Enter an app name, pick the model your app runs, add your redirect URI(s), submit, and copy the client_id + client_secret shown once. Every redirect URI must be an exact URL your app will send later (no wildcards, no path-prefix matching).
Your app's model is fixed at registration (each call is billed at that model's DeepInfra rate). You don't send a model field at inference time — mycorra forces the app's model; any model you send is ignored.
Prefer to automate it? The same registration is a plain POST /clients, authorized by a mycorra session cookie or a provisioning token (Authorization: Bearer …):
curl -sX POST https://mycorra.example/clients \
-b cookies.txt \ # a mycorra session cookie
--data-urlencode 'name=My Cool App' \
--data-urlencode $'redirect_uris=https://mycoolapp.com/auth/callback'
Response — the secret is shown exactly once, store it securely:
{
"client_id": "b1c2...",
"client_secret": "9f8e...",
"redirect_uris": ["https://mycoolapp.com/auth/callback"]
}
2. Send the user to authorize
Redirect the user's browser to /oauth/authorize:
GET https://mycorra.example/oauth/authorize
?response_type=code
&client_id=b1c2...
&redirect_uri=https://mycoolapp.com/auth/callback
&state=<opaque-random-anti-CSRF-value>
| Param | Required | Notes |
|---|---|---|
response_type | yes | must be code |
client_id | yes | from step 1 |
redirect_uri | yes | must exactly match a registered URI |
state | strongly recommended | opaque value you generate; verify it on return to prevent CSRF |
- If the user isn't signed in to mycorra, they're bounced to login and back.
- They see a consent screen naming your app. On Allow, mycorra redirects to:
https://mycoolapp.com/auth/callback?code=<authorization_code>&state=<the-same-state>
Verify state matches what you sent before continuing.
3. Exchange the code for an access token
Server-to-server call (never from the browser — it uses your client secret). Client credentials may go in the body or via HTTP Basic auth.
curl -sX POST https://mycorra.example/oauth/token \
-d grant_type=authorization_code \
-d code=<authorization_code> \
-d redirect_uri=https://mycoolapp.com/auth/callback \
-d client_id=b1c2... \
-d client_secret=9f8e...
Response:
{
"access_token": "eyJ...hmac",
"token_type": "Bearer",
"expires_in": 1209600
}
The code is single-use — a second exchange returns 400 {"error":"invalid_grant"}. Other error codes follow OAuth2: invalid_client, unsupported_grant_type.
4. Call inference with the access token
POST https://mycorra.example/api/inference
Authorization: Bearer <access_token>
Content-Type: application/json
{ "messages": [ {"role": "user", "content": "Say hello in French."} ] }
The body is the OpenAI chat-completions shape — just messages (plus any optional fields like temperature). You do not set model; mycorra forces its own. The response is the OpenAI-compatible completion.
Example
curl -sX POST https://mycorra.example/api/inference \
-H "Authorization: Bearer eyJ...hmac" \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"hello"}]}'
Response
On 200:
{
"id": "chatcmpl-...",
"choices": [{"message": {"role": "assistant", "content": "Bonjour !"}}],
"usage": {"prompt_tokens": 9, "completion_tokens": 3, "total_tokens": 12}
}
Read choices[0].message.content for the answer.
Errors
| Status | Meaning | What to do |
|---|---|---|
200 | completion returned | — |
401 | missing / invalid / expired access token, or the user revoked your app | re-run the authorization flow (steps 2–3) |
400 | your request was rejected (e.g. malformed messages) | fix the request body |
402 | the user is out of credits, or your app hit its per-app spending limit | prompt them to top up, or raise your app's limit under Connected apps |
429 | the service is busy / rate-limited | retry after a short delay (message suggests how long) |
502 | mycorra couldn't reach the model backend | retry with backoff |
503 | inference isn't configured on this instance | contact the operator |
Every non-2xx response is JSON with the same shape — never raw upstream detail, never plain text:
{ "error": { "type": "rate_limited", "message": "The service is busy right now — please try again in about 3 seconds." } }
error.message is always safe to show your user as-is. Branch on error.type for behaviour:
type | Status | Handle it |
|---|---|---|
invalid_request | 400 | fix the request body (don't retry unchanged) |
insufficient_credits | 402 | tell the user to top up at mycorra |
spending_limit | 402 | tell the user to raise this app's limit under Connected apps |
rate_limited | 429 | retry after a short delay (see Retry-After guidance in the message) |
upstream_error | 502 | retry with backoff |
not_configured | 503 | operator hasn't enabled inference; surface a status message |
internal | 500 | retry once, then report |
A 401 (revoked/expired token) has no body type — treat any 401 as "re-run the authorization flow (steps 2–3)".
Example — surface the message, retry only when it's safe:
const r = await fetch(`${MYCORRA}/api/inference`, {
method: "POST",
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ messages }),
});
if (r.status === 401) return reauthorize(); // token gone — restart OAuth
const data = await r.json();
if (!r.ok) {
const { type, message } = data.error;
if (type === "rate_limited" || type === "upstream_error") {
return retryWithBackoff(); // transient
}
return showUser(message); // 402 / 400 / 503 — actionable text
}
return data.choices[0].message.content; // success
Notes
- Billing is automatic. Each successful call debits the user's credit balance (based on tokens used); your app reports nothing. A user with no credits gets
402— surface a "top up at mycorra" prompt. - Per-app spending limit. At consent the user sets a budget your app may spend of their credits. When it's exhausted, calls return
402until they raise it or re-approve. Treat402as "ask the user to check their mycorra balance/limits", not a hard failure. - No approval needed. A registered app works immediately for any user — there is no verification gate. A "verified" badge may be granted by the operator as an optional trust signal, but it never blocks usage.
- Model is fixed per app. You choose it once at registration (from the DeepInfra catalog on the Developers page, or an optional
modelfield onPOST /clients). mycorra forces that model on every call; anymodelyou send at inference time is ignored. - Streaming is not supported. Do not send
"stream": true— responses are a single JSON body so usage can be metered. - Bodies are limited to 1 MiB in, 8 MiB out.
Security notes
- Redirect URIs are matched exactly — this closes the open-redirect hole. Register every callback you use.
- Access tokens are stateless HMAC-signed tokens scoped to
kind=access; a mycorra session cookie can't be replayed as an API token and vice versa. - Never expose your
client_secretin browser/mobile code. If you can't keep a secret (SPA/native), treat this as a confidential-client-only integration and proxy the token exchange through your backend. - No credentials to leak. Your app only holds an access token; it never sees a provider key or the user's payment details. mycorra runs inference and bills credits on the user's behalf.
- Users can revoke your app. From their mycorra "Connected apps" page a user can end your app's session; outstanding access tokens then return
401. Treat a sudden401as "re-run the authorization flow", not a hard error.
Endpoint reference
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /clients | mycorra session | register an OAuth client |
| GET | /oauth/authorize | user session | consent screen, issues a code |
| POST | /oauth/token | client secret | exchange code → access token |
| POST | /api/inference | Bearer access | run inference, billed to credits |
A minimal, self-contained example of the whole flow lives in the end-to-end test: internal/web/web_test.go → TestOAuthLoginWithMycorraEndToEnd.