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:

  1. User buys credits via the checkout widget you embed in your app
  2. You make AI requests through our OpenAI/Anthropic-compatible API using the user's key
  3. 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
Base URL: All API requests go to https://api.opencredits.ai

Quickstart

Get credits working in your app in under 5 minutes.

Don't have a partner key yet? Sign up to become a partner and we'll get you set up.

1. Add the SDK

HTML
<script src="https://opencredits.ai/v1/sdk.js"></script>

2. Initialize

JavaScript
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

JavaScript
// 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:

JavaScript (OpenAI)
// 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!' }],
  }),
});
JavaScript (Anthropic)
// 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!' }],
  }),
});
Tip: The SDK automatically persists the user key in localStorage. For returning users, pass it back via OpenCredits.getUserKey() so they don't have to repurchase.

5. Works with OpenAI SDKs

Python
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!"}],
)
Node.js
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

Python
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!"}],
)
Node.js
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

Node.js (OpenAI provider)
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!',
});
Node.js (Anthropic provider)
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:

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

OptionTypeDescription
publishableKeystringRequired. Your partner publishable key (oc_pk_...)
baseUrlstringOpenCredits URL. Default: 'https://opencredits.ai'
currencystring'usd' or 'eur'. Default: 'usd'
displaystringWhere the OpenCredits checkout appears: 'embedded' (default, an overlay in your page), 'popup', or 'new_tab'. See Display for the trade-offs.
checkoutModestringHow Stripe's payment form is shown, inside whichever display you chose: 'embedded' (default), 'new_tab', or 'manual'
metadataobjectYour 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.
onCreditsAddedfunctionThe 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.
onLoginCompletedfunctionCalled 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)
onCheckoutOpenedfunctionCalled when checkout session starts. Receives { session_id, user_key }
onPurchaseErrorfunctionCalled on payment failure. Receives { error }
onErrorfunctionCalled on any error. Receives { error }
onCompletefunctionCalled when the user dismisses the success screen after purchase. Receives { user_key, balance, credits_added }
onLoggedOutfunctionCalled when user logs out from the checkout widget
onPurchaseCompleted
onPermissionUpdated
functionDeprecated. 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.

OptionTypeDescription
amountnumberPreset 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.

DisplayWhat it isProsCons
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.
popupA 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_tabThe 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.1 and *.localhost always 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.com does not cover http://yourapp.com or https://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 trust localhost gets, 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

ModeBehavior
embeddedStripe checkout renders inside the modal (default, recommended)
new_tabOpens Stripe checkout in a new tab. Modal shows a waiting state.
manualReturns the checkout_url in onCheckoutOpened — you handle opening it yourself.

Full example

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

Request
{
  "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.

Request
{
  "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:

ProviderExample model ID
Anthropicanthropic/claude-sonnet-4-20250514
OpenAIopenai/gpt-4o
Googlegoogle/gemini-2.5-pro
xAIxai/grok-3

See Balance & Models for how to list all available models programmatically.

Streaming: Both endpoints support server-sent events (SSE) for real-time responses.

Server-side requests

You can also make requests from your backend. Store the user key after checkout and attach it when making requests:

Node.js (Express)
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

EventFires whendata
user_key.createdA key is minted for one of your users: email login, sign-in token exchange, or first checkoutuser_key (the plaintext key), key_id, user_id (your app's id for this user, see below), metadata
user_key.revokedA key you were sent stopped workingkey_id, user_id, reason, metadata
credits.addedThe 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
testYou pressed Send test eventpartner_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.

Example delivery
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:

Node.js (Express)
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_at if 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.
Tip: store keys by 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.

Tip: when a request fails with 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.

Response
{
  "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:

statusWhat to show
insufficient_creditsBuy credits
partner_not_permittedBuy credits here, or allow your app on the OpenCredits dashboard
partner_limit_reachedRaise the limit on the OpenCredits dashboard
Tip: Show this number in your UI so users know when to top up. It is what your app can use, not the user's total across every app.

Usage history

GET /v1/credits/history

Returns the user's transaction and usage history. Requires the X-User-Key header.

ParamTypeDescription
limitnumberNumber of entries to return (default 50)
offsetnumberPagination offset (default 0)

List models

GET /v1/models

Returns all available models. No authentication required.

Response
{
  "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.

FieldTypeDescription
publishable_keystringRequired. Your partner publishable key.
modelsstring[]Optional. Filter to specific model IDs.
input_tokensnumberOptional. Assumed input tokens per request (default 2500).
output_tokensnumberOptional. Assumed output tokens per request (default 2500).
Request
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
  }'
Response
{
  "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

StatusCodeDescription
400invalid_requestMalformed request body or missing required fields
401missing_api_keyNo X-User-Key header provided
401invalid_user_keyThe user key is invalid or has been revoked
402insufficient_creditsUser doesn't have enough credits. Open the checkout to top up.
403model_not_allowedYour partner account doesn't have access to this model
404model_not_foundThe requested model ID doesn't exist
429rate_limitedToo many requests. Back off and retry.
502upstream_errorThe AI provider returned an error
Handle 402: When you get 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.