OpenCredits Developer Docs
Let users buy AI credits in your app, use them for AI requests, and earn a referral share on every one.
How it works
OpenCredits gives your users a single credit balance that works across AI models (Claude, GPT, Gemini, and more). Here's the flow:
- User buys credits via the checkout widget you embed in your app
- You make AI requests through our OpenAI/Anthropic-compatible API using the user's key
- Credits are deducted based on token price, plus a referral share that goes to you
What you get
- A publishable key (
oc_pk_...) to identify your app in the checkout widget - User keys (
oc_sk_...) returned after purchase — use these to make API calls on behalf of your users - An OpenAI and Anthropic-compatible API that accepts the user key as auth
https://api.opencredits.ai
Quickstart
Get credits working in your app in under 5 minutes.
1. Add the SDK
<script src="https://opencredits.ai/v1/sdk.js"></script>
2. Initialize
OpenCredits.init({
publishableKey: 'oc_pk_your_key_here',
currency: 'usd',
// Called when the user makes credits available to your app:
// a purchase, or allowing credits bought elsewhere (data.type)
onCreditsAdded(data) {
console.log('User key:', data.user_key);
console.log('Balance:', data.balance);
}
});
Then register the origin your app runs on (for example https://yourapp.com) in the partner dashboard under Sites that open your checkout. The checkout completes only on pages from origins you listed; localhost always works, so nothing is needed while you develop. See Origins.
3. Open the checkout
// Let user pick an amount
OpenCredits.open();
// Or preset the amount (in dollars/euros)
OpenCredits.open({ amount: 10 });
4. Make API calls
Once you have the user's key, make AI requests with it. The API is OpenAI and Anthropic-compatible — just change the base URL and auth header:
// OpenAI-compatible endpoint
const response = await fetch('https://api.opencredits.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-User-Key': userKey, // oc_sk_... from checkout
},
body: JSON.stringify({
model: 'anthropic/claude-sonnet-4-20250514',
messages: [{ role: 'user', content: 'Hello!' }],
}),
});
// Anthropic-compatible endpoint
const response = await fetch('https://api.opencredits.ai/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-User-Key': userKey, // oc_sk_... from checkout
},
body: JSON.stringify({
model: 'anthropic/claude-sonnet-4-20250514',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Hello!' }],
}),
});
localStorage. For returning users, pass it back via OpenCredits.getUserKey() so they don't have to repurchase.
5. Works with OpenAI SDKs
from openai import OpenAI
client = OpenAI(
base_url="https://api.opencredits.ai/v1",
api_key=user_key, # oc_sk_... from checkout
)
response = client.chat.completions.create(
model="anthropic/claude-sonnet-4-20250514",
messages=[{"role": "user", "content": "Hello!"}],
)
import OpenAI from 'openai';
const client = new OpenAI({
baseURL: 'https://api.opencredits.ai/v1',
apiKey: userKey, // oc_sk_... from checkout
});
const response = await client.chat.completions.create({
model: 'anthropic/claude-sonnet-4-20250514',
messages: [{ role: 'user', content: 'Hello!' }],
});
6. Works with Anthropic SDKs
import anthropic
client = anthropic.Anthropic(
base_url="https://api.opencredits.ai",
api_key=user_key, # oc_sk_... from checkout
)
message = client.messages.create(
model="anthropic/claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}],
)
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic({
baseURL: 'https://api.opencredits.ai',
apiKey: userKey, // oc_sk_... from checkout
});
const message = await client.messages.create({
model: 'anthropic/claude-sonnet-4-20250514',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Hello!' }],
});
7. Works with Vercel AI SDK
import { generateText } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
const openai = createOpenAI({
baseURL: 'https://api.opencredits.ai/v1',
apiKey: userKey, // oc_sk_... from checkout
});
const { text } = await generateText({
model: openai('anthropic/claude-sonnet-4-20250514'),
prompt: 'Hello!',
});
import { generateText } from 'ai';
import { createAnthropic } from '@ai-sdk/anthropic';
const anthropic = createAnthropic({
baseURL: 'https://api.opencredits.ai/v1',
apiKey: userKey, // oc_sk_... from checkout
});
const { text } = await generateText({
model: anthropic('anthropic/claude-sonnet-4-20250514'),
prompt: 'Hello!',
});
8. Works with Claude Code
Point Claude Code at OpenCredits by setting environment variables:
export ANTHROPIC_BASE_URL=https://api.opencredits.ai
export ANTHROPIC_API_KEY=oc_sk_... # user key from checkout
claude
Checkout SDK
The SDK opens a branded checkout modal in your app. It handles amount selection, Stripe payment, account login, and returns the user key when done.
OpenCredits.init(options)
Initialize the SDK. Call this once on page load.
| Option | Type | Description |
|---|---|---|
publishableKey | string | Required. Your partner publishable key (oc_pk_...) |
baseUrl | string | OpenCredits URL. Default: 'https://opencredits.ai' |
currency | string | 'usd' or 'eur'. Default: 'usd' |
display | string | Where the OpenCredits checkout appears: 'embedded' (default, an overlay in your page), 'popup', or 'new_tab'. See Display for the trade-offs. |
checkoutMode | string | How Stripe's payment form is shown, inside whichever display you chose: 'embedded' (default), 'new_tab', or 'manual' |
metadata | object | Your own identifiers for this user, e.g. { user_id: 'usr_123' }. A flat object of strings, numbers or booleans, up to 500 characters serialized. Echoed in every webhook event about this user's keys and purchases. |
onCreditsAdded | function | The one to handle. Called whenever the user makes credits available to your app: a purchase here, or allowing credits they bought elsewhere. Receives { type, user_key, balance, credits_added } — type is 'purchase' or 'permission', balance is what your app can spend now, credits_added is how much that grew. Your app earns per request either way, so the reaction is the same: store the key, retry. |
onLoginCompleted | function | Called when user signs in. Receives { user_key, balance } — balance is what your app can spend for this user (0 if it could not be read) |
onCheckoutOpened | function | Called when checkout session starts. Receives { session_id, user_key } |
onPurchaseError | function | Called on payment failure. Receives { error } |
onError | function | Called on any error. Receives { error } |
onComplete | function | Called when the user dismisses the success screen after purchase. Receives { user_key, balance, credits_added } |
onLoggedOut | function | Called when user logs out from the checkout widget |
onPurchaseCompletedonPermissionUpdated | function | Deprecated. The two cases of onCreditsAdded as separate callbacks, same fields minus type. They still fire (the SDK logs one console warning when either is set); handle onCreditsAdded instead. |
OpenCredits.open(options?)
Open the checkout modal.
| Option | Type | Description |
|---|---|---|
amount | number | Preset dollar/euro amount. If omitted, user picks from $5 / $10 / $25 / $50 / $100 or enters a custom amount. |
OpenCredits.close()
Programmatically close the checkout modal.
OpenCredits.getUserKey()
Returns the current user key (oc_sk_...) or null if no user is logged in. The SDK persists this in localStorage automatically.
Display
Where the OpenCredits checkout itself appears. Your callbacks and the user's account are the same in all three; what differs is the browser context, and with it how a returning user is recognised.
| Display | What it is | Pros | Cons |
|---|---|---|---|
embedded (default) | An overlay in your page with the checkout in an iframe. | Feels native to your app. Never blocked. Works everywhere, including inside other overlays. | The iframe is third-party on your domain, so browsers keep its storage separate. A user your app has a key for is recognised; a user new to your app signs in again even if they are signed in to OpenCredits elsewhere. The "Manage apps" link opens the dashboard signed out. |
popup | A small window on opencredits.ai, opened by OpenCredits.open(). | First-party: a user signed in to OpenCredits is recognised in any website on open — the checkout appears already signed in, no Sign in click — and dashboard links open signed in. An app that is not a website (a webview, registered by a scheme wildcard) is the exception: it always asks for one Sign in click, see Origins below. Stripe's form still renders inside it. | open() must run inside the user's click or the browser blocks it (the SDK then falls back to the overlay). If your page sends Cross-Origin-Opener-Policy: same-origin, the browser cuts the link to the window; the SDK reads your page's headers when it initialises and uses the overlay instead, with a console warning — use same-origin-allow-popups to keep the popup. A second open() while a payment is in progress brings the window forward; a window idling on the picker is re-opened with the new options. Phones and some desktops show it as a tab. The user can close it; you get onComplete if a purchase had finished. |
new_tab | The checkout in a full tab. | Everything the popup gives, plus room, and the natural shape on phones. | The user leaves your page until they are done. Same click rule as the popup. |
checkoutMode is independent: it chooses how Stripe's payment form appears inside whichever display you picked.
Origins
Your publishable key is public — it is in your page source — so the key alone cannot say which site is yours. The checkout therefore completes only on pages whose origin (scheme and host, e.g. https://yourapp.com) you registered in the partner dashboard under Sites that open your checkout. Anywhere else it shows a refusal naming the origin, and shares nothing with the page around it: no key, no balance, no events.
localhost,127.0.0.1and*.localhostalways work, on any port, so development needs no setup.- Applying with a bare site address registers it for you; add staging or a second domain in the dashboard.
- The dashboard also lists where your key was opened recently, registered or not, with one-click registration — and shows you if someone else's site is using it.
- A registered origin is exact:
https://yourapp.comdoes not coverhttp://yourapp.comorhttps://app.yourapp.com. - An app that is not a website (a VS Code webview, an Electron shell) is registered by its scheme wildcard, e.g.
vscode-webview://*; the refusal page names the wildcard to add. A wildcard admits every app on the user's device using that scheme (another extension's webview, another Capacitor app), the same trustlocalhostgets, and such a host is never signed in without a click — not when the checkout opens and not when the user presses Buy: register one only if you ship such an app. - The "asked about recently" list is unverified: anyone who knows your publishable key can make an origin appear there. Register only sites you run.
Checkout modes
| Mode | Behavior |
|---|---|
embedded | Stripe checkout renders inside the modal (default, recommended) |
new_tab | Opens Stripe checkout in a new tab. Modal shows a waiting state. |
manual | Returns the checkout_url in onCheckoutOpened — you handle opening it yourself. |
Full example
<script src="https://opencredits.ai/v1/sdk.js"></script>
<script>
OpenCredits.init({
publishableKey: 'oc_pk_your_key',
onCreditsAdded(data) {
// Store the key server-side for this user
fetch('/api/save-credits-key', {
method: 'POST',
body: JSON.stringify({ userKey: data.user_key }),
});
},
});
document.getElementById('buy-btn').addEventListener('click', () => {
OpenCredits.open();
});
</script>
API
Make AI requests on behalf of your users. The API is compatible with both OpenAI and Anthropic SDKs — just change the base URL and use the user's key for auth.
Authentication
Pass the user key in the X-User-Key header. When using the OpenAI SDK, this maps to the apiKey field.
OpenAI-compatible endpoint
POST /v1/chat/completions
Accepts the standard OpenAI chat completions request format. Supports streaming, tools, vision, and all other OpenAI features.
{
"model": "openai/gpt-4o",
"messages": [
{ "role": "user", "content": "Explain quantum computing" }
],
"stream": true
}
Anthropic-compatible endpoint
POST /v1/messages
Accepts the standard Anthropic messages request format. Supports streaming, thinking, vision, and all other Anthropic features.
{
"model": "anthropic/claude-sonnet-4-20250514",
"max_tokens": 1024,
"messages": [
{ "role": "user", "content": "Explain quantum computing" }
]
}
Model IDs
Models are namespaced by provider. Use the full ID in your requests:
| Provider | Example model ID |
|---|---|
| Anthropic | anthropic/claude-sonnet-4-20250514 |
| OpenAI | openai/gpt-4o |
google/gemini-2.5-pro | |
| xAI | xai/grok-3 |
See Balance & Models for how to list all available models programmatically.
Server-side requests
You can also make requests from your backend. Store the user key after checkout and attach it when making requests:
app.post('/api/chat', async (req, res) => {
const userKey = getUserKeyFromSession(req); // your auth logic
const response = await fetch('https://api.opencredits.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-User-Key': userKey,
},
body: JSON.stringify(req.body),
});
res.status(response.status).send(await response.json());
});
Webhooks
Webhooks deliver user keys and purchases to your backend, signed with a secret only you hold. They are the server-side alternative to catching user_key from the SDK callbacks: use them when your backend needs to know about a key or a purchase without trusting the browser.
Configure them in the partner dashboard under Webhooks: set a public HTTPS endpoint, copy the signing secret, and use Send test event to verify your handler end to end.
Events
| Event | Fires when | data |
|---|---|---|
user_key.created | A key is minted for one of your users: email login, sign-in token exchange, or first checkout | user_key (the plaintext key), key_id, user_id (your app's id for this user, see below), metadata |
user_key.revoked | A key you were sent stopped working | key_id, user_id, reason, metadata |
credits.added | The user made credits available to your app: a purchase here (type: 'purchase', once per Stripe session, after the credits were granted) or a permission they set from the checkout prompt, the post-purchase opt-in, or their dashboard (type: 'permission'). The server-side twin of onCreditsAdded. A permission fires on every save, even one that adds nothing (credits_added: 0) | type, user_id, metadata, balance (what your app can spend for them now), credits_added, plus transaction_id on a purchase or granted_credits on a permission |
test | You pressed Send test event | partner_id, message |
reason on a revocation is user_revoked (from their OpenCredits dashboard), session_revoked (they logged that device out) or evicted (a user holds up to 10 live keys per app; an eleventh pushes out the most idle one). Delete that key from your storage. The user's other keys keep working, and a new created event never invalidates keys you already hold.
POST /webhooks/opencredits
X-OpenCredits-Event: user_key.created
X-OpenCredits-Signature: t=1783015200,v1=5f0c…
{
"id": "evt_5f0c…",
"type": "user_key.created",
"created_at": "2026-07-09T18:00:00.000Z",
"data": {
"user_key": "oc_sk_…",
"key_id": "…",
"user_id": "au_…",
"metadata": { "user_id": "usr_123" }
}
}
user_id is your app's id for the user: an au_… value minted per app, stable for this user in your app and unrelated to their id in any other app. OpenCredits never sends its own account id or the user's email, in any event or on any partner page. Attach your own identifiers as metadata to map events to your records.
Identifying your users
Pass metadata to OpenCredits.init with your own identifiers. It rides through login and checkout and comes back in every event, so your handler can map a key or a purchase to your user without a lookup:
OpenCredits.init({
publishableKey: 'oc_pk_...',
metadata: { user_id: 'usr_123', plan: 'pro' },
});
A flat object of strings, numbers or booleans, up to 500 characters serialized. It is opaque to OpenCredits and only ever returned to you. If you call the API directly instead of through the SDK, the same field is accepted by POST /v1/auth/verify-code, POST /auth/token and POST /v1/credits/checkout.
Verifying signatures
Every delivery carries X-OpenCredits-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA-256 with your signing secret over <t>.<raw body>. Verify against the raw request body, before any JSON parsing, and reject stale timestamps:
import { createHmac, timingSafeEqual } from 'node:crypto';
app.post('/webhooks/opencredits', express.raw({ type: '*/*' }), (req, res) => {
const raw = req.body.toString('utf8');
const m = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(req.get('X-OpenCredits-Signature') || '');
if (!m) return res.sendStatus(400);
const [, t, sig] = m;
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400); // 5 min tolerance
const expected = createHmac('sha256', process.env.OC_WEBHOOK_SECRET).update(`${t}.${raw}`).digest('hex');
if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return res.sendStatus(400);
const event = JSON.parse(raw);
// store or delete the key, record the purchase — then respond fast
res.sendStatus(200);
});
Delivery
- At least once, best effort. Three attempts: immediately, after 2 s, after 8 s, each with a 5 s timeout. Respond 2xx quickly and do the work asynchronously. Any other 4xx except 408 and 429 stops retries for that event.
- Deduplicate by
id. Retries reuse the event id. - Order is not guaranteed across events. Use
created_atif it matters. - Additive. The SDK callbacks still fire in the browser whether or not the delivery succeeds, so a missed webhook never breaks a user.
- Redirects are never followed, and the endpoint must be a public HTTPS host.
- Deliveries pause while your partner account is disabled and stop if you clear the endpoint URL. Your signing secret is retained either way.
key_id, not by value, and delete on user_key.revoked. Disabling your account suspends keys rather than revoking them, so no event fires for that: they resume when you are re-enabled.
Users with credits from other apps
Credits belong to the user, but your app can only spend what the user allows it to. Credits bought inside your app are always spendable there. A balance the user bought in another app is not, until they set a limit for your app — which the checkout offers them the moment they sign in with an existing balance, and again after any purchase.
402 partner_not_permitted or 402 partner_limit_reached, open the checkout: the user sees the prompt and either buys credits here or allows your app to use their balance. onCreditsAdded fires with type: 'permission' when they do. Never treat those two as "no credits".
Balance & Models
Check balance
GET /v1/credits/balance
Returns the credits your app can spend for this user right now — their money, capped by what they've allowed your app to use. Requires the X-User-Key header.
{
"balance": 270,
"currency": "credits",
"minimum_required": 10,
"status": "ok"
}
When balance is below minimum_required, status says why, using the same codes the API returns as 402 errors:
status | What to show |
|---|---|
insufficient_credits | Buy credits |
partner_not_permitted | Buy credits here, or allow your app on the OpenCredits dashboard |
partner_limit_reached | Raise the limit on the OpenCredits dashboard |
Usage history
GET /v1/credits/history
Returns the user's transaction and usage history. Requires the X-User-Key header.
| Param | Type | Description |
|---|---|---|
limit | number | Number of entries to return (default 50) |
offset | number | Pagination offset (default 0) |
List models
GET /v1/models
Returns all available models. No authentication required.
{
"data": [
{
"id": "anthropic/claude-sonnet-4-20250514",
"name": "Claude Sonnet 4",
"type": "language",
"context_window": 200000,
"max_tokens": 8192
},
...
]
}
Credit pricing estimates
POST /v1/credits/pricing
Returns models with estimated credits per request. Useful for showing users "how many requests will my credits buy?" Results are filtered to your allowed models and include your partner commission. Also available as GET with query params.
| Field | Type | Description |
|---|---|---|
publishable_key | string | Required. Your partner publishable key. |
models | string[] | Optional. Filter to specific model IDs. |
input_tokens | number | Optional. Assumed input tokens per request (default 2500). |
output_tokens | number | Optional. Assumed output tokens per request (default 2500). |
curl -X POST https://api.opencredits.ai/v1/credits/pricing \
-H "Content-Type: application/json" \
-d '{
"publishable_key": "oc_pk_...",
"models": ["anthropic/claude-opus-4.6", "openai/gpt-4o"],
"input_tokens": 1000,
"output_tokens": 500
}'
{
"token_assumption": { "input": 2500, "output": 2500 },
"models": [
{
"id": "anthropic/claude-sonnet-4-20250514",
"name": "Claude Sonnet 4",
"credits_per_request": 1.44,
},
{
"id": "openai/gpt-4o",
"name": "GPT-4o",
"credits_per_request": 0.94,
},
...
]
}
Errors
The API returns errors in the format matching the endpoint you're using (OpenAI or Anthropic format).
OpenAI format
Returned from /v1/chat/completions:
{
"error": {
"message": "Insufficient credits. Please top up your balance.",
"type": "insufficient_credits",
"param": null,
"code": "402"
}
}
Anthropic format
Returned from /v1/messages:
{
"type": "error",
"error": {
"type": "insufficient_credits",
"message": "Insufficient credits. Please top up your balance."
}
}
Error codes
| Status | Code | Description |
|---|---|---|
| 400 | invalid_request | Malformed request body or missing required fields |
| 401 | missing_api_key | No X-User-Key header provided |
| 401 | invalid_user_key | The user key is invalid or has been revoked |
| 402 | insufficient_credits | User doesn't have enough credits. Open the checkout to top up. |
| 403 | model_not_allowed | Your partner account doesn't have access to this model |
| 404 | model_not_found | The requested model ID doesn't exist |
| 429 | rate_limited | Too many requests. Back off and retry. |
| 502 | upstream_error | The AI provider returned an error |
insufficient_credits, prompt the user to buy more credits by calling OpenCredits.open().
Referrals
You earn a referral share on every AI request your users make through OpenCredits. This is automatic — no extra integration needed.
How it works
Each AI request deducts credits from the user based on token price, plus a referral share that goes to you. Your referral rate is set on your partner account (e.g. 10%). You can see per-request breakdowns in your usage logs.
Example
With a 10% referral rate and a request that costs $5 in provider fees:
| Amount | |
|---|---|
| Provider cost for request | $5.00 |
| Your referral share (10%) | $0.50 |
| Total charged to user | $5.50 |
The more your users use AI features in your app, the more you earn. At scale this adds up — 1,000 requests/month at this rate = $500/month in referral revenue.
By bringing users to OpenCredits, you're rewarded every time they use AI — better features mean more usage and more revenue for you.