---
name: opencredits
description: Integrate OpenCredits into a web app — embed a checkout widget for users to buy AI credits, make AI requests via OpenAI/Anthropic-compatible API, and handle billing. Use when the user asks to add OpenCredits, AI credits, AI billing, or pay-per-use AI to their app.
metadata:
  author: opencredits
  version: "1.0"
---

# OpenCredits Integration

OpenCredits lets users buy AI credits inside your app. Your app uses those credits to make AI requests. You earn a referral share on every request.

**Base URL:** `https://api.opencredits.ai`
**SDK:** `https://opencredits.ai/v1/sdk.js`
**Docs:** https://opencredits.ai/docs

## Prerequisites

You need a **publishable key** (`oc_pk_...`). If the user hasn't provided one, ask for it.

## Step 1: Add the SDK

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

## Step 2: Initialize

Call once on page load. `publishableKey` is required. The checkout completes only on pages whose origin the partner registered in the dashboard (Sites that open your checkout); `localhost` always works, so if a deployed page shows "This site can't open this checkout", register that origin.

```javascript
OpenCredits.init({
  publishableKey: 'oc_pk_...',
  currency: 'usd', // or 'eur'
  metadata: { user_id: 'usr_123' }, // optional: your own ids, echoed in webhook events

  onCreditsAdded(data) {
    // data.user_key — API key (oc_sk_...)
    // data.balance — what your app can spend for this user now
    console.log('User key:', data.user_key);
  },

  onComplete(data) {
    // User closed checkout after successful purchase
  },

  onError(data) {
    console.error('Purchase error:', data);
  }
});
```

## Step 3: Open checkout

```javascript
// Let user pick an amount
OpenCredits.open();

// Or preset the amount (in dollars/euros)
OpenCredits.open({ amount: 10 });
```

Typically triggered by a button click.

## Step 4: Returning users

The SDK persists the user key in `localStorage` automatically.

```javascript
const userKey = OpenCredits.getUserKey();
if (userKey) {
  // User has credits — make API calls
} else {
  // Show "Buy credits" button
}
```

## Step 5: Make AI requests

The API is OpenAI and Anthropic-compatible. Use the user key for auth.

### Browser (fetch)

```javascript
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({
    model: 'anthropic/claude-sonnet-4-20250514',
    messages: [{ role: 'user', content: prompt }],
  }),
});
const data = await response.json();
const reply = data.choices[0].message.content;
```

### OpenAI SDK (Python)

```python
from openai import OpenAI

client = OpenAI(
    base_url="https://api.opencredits.ai/v1",
    api_key=user_key,
)
response = client.chat.completions.create(
    model="anthropic/claude-sonnet-4-20250514",
    messages=[{"role": "user", "content": prompt}],
)
```

### OpenAI SDK (Node.js)

```javascript
import OpenAI from 'openai';

const client = new OpenAI({
  baseURL: 'https://api.opencredits.ai/v1',
  apiKey: userKey,
});
const response = await client.chat.completions.create({
  model: 'anthropic/claude-sonnet-4-20250514',
  messages: [{ role: 'user', content: prompt }],
});
```

### Anthropic SDK (Python)

```python
import anthropic

client = anthropic.Anthropic(
    base_url="https://api.opencredits.ai",  # no /v1
    api_key=user_key,
)
message = client.messages.create(
    model="anthropic/claude-sonnet-4-20250514",
    max_tokens=1024,
    messages=[{"role": "user", "content": prompt}],
)
```

### Anthropic SDK (Node.js)

```javascript
import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic({
  baseURL: 'https://api.opencredits.ai',  // no /v1
  apiKey: userKey,
});
const message = await client.messages.create({
  model: 'anthropic/claude-sonnet-4-20250514',
  max_tokens: 1024,
  messages: [{ role: 'user', content: prompt }],
});
```

## Step 6: Handle insufficient credits

```javascript
if (response.status === 402) {
  OpenCredits.open(); // prompt user to top up
  return;
}
```

## Step 7: Check balance (optional)

```javascript
const res = await fetch('https://api.opencredits.ai/v1/credits/balance', {
  headers: { 'X-User-Key': userKey },
});
// balance = what YOUR app can spend for this user (their credits, capped by
// what they allowed your app), not their total. status is 'ok' or the 402
// code a request would get: insufficient_credits | partner_not_permitted |
// partner_limit_reached
const { balance, minimum_required, status } = await res.json();
```

## Users with credits from other apps

Your app can only spend what the user allows: credits bought inside your app, plus any limit they set for it. On `402 partner_not_permitted` or `402 partner_limit_reached`, open the checkout — the user is prompted to buy here or to allow your app — and listen for `onCreditsAdded` (`data.type === 'permission'`). Do not treat those as "no credits".

## Available models

Call `GET https://api.opencredits.ai/v1/models` for the full list. Common models:

- `anthropic/claude-sonnet-4-20250514` — Claude Sonnet 4
- `anthropic/claude-haiku-4-5-20251001` — Claude Haiku 3.5
- `openai/gpt-4o` — GPT-4o
- `openai/gpt-4o-mini` — GPT-4o Mini
- `google/gemini-2.0-flash` — Gemini 2.0 Flash

## SDK events

| Callback | When |
|---|---|
| `onCreditsAdded(data)` | **Handle this one.** User made credits available to your app: a purchase (`data.type === 'purchase'`) or allowing credits bought elsewhere (`'permission'`). `data.balance` = what your app can spend now; `data.user_key` = the key to use |
| `onComplete(data)` | User closes modal after purchase |
| `onCheckoutOpened(data)` | Stripe checkout initiated |
| `onLoginCompleted(data)` | User logs in via email; `data.balance` = what your app can spend for them |
| `onLoggedOut(data)` | User logs out |
| `onError(data)` | Purchase fails |
| `onPurchaseCompleted(data)`, `onPermissionUpdated(data)` | Deprecated. The two cases of `onCreditsAdded` as separate callbacks, same fields minus `data.type`; still fire, with one console warning. Handle `onCreditsAdded` instead |

## Webhooks (optional, server-side)

If the backend must learn about keys and purchases without trusting the browser, configure a webhook in the partner dashboard (`/partners` → Webhooks): public HTTPS endpoint, then copy the signing secret it reveals.

Events, each a JSON `{ id, type, created_at, data }`:

- `user_key.created` — `data.user_key` is the plaintext key (the only server-side channel that carries it), plus `key_id`, `user_id` (an `au_…` id we mint per app for this user — never our account id or their email), `metadata`
- `user_key.revoked` — `key_id`, `user_id` (the same `au_…` id), `reason` (`user_revoked` | `session_revoked` | `evicted`), `metadata`: delete that key
- `credits.added` — the user made credits available to your app (server-side twin of `onCreditsAdded`). `type` (`purchase` | `permission`), `user_id` (the `au_…` id), `metadata`, `balance`, `credits_added`, plus `transaction_id` (purchase) or `granted_credits` (permission). A permission fires on every save, even one that adds nothing (`credits_added: 0`). No email, no amount paid

`data.user_id` is an `au_…` id we mint per app for the user (stable per app, unrelated across apps, never our account id or their email); `data.metadata` is whatever the app passed to `OpenCredits.init({ metadata })`. Use either to map events to your own users.

Verify every delivery before parsing it. Header `X-OpenCredits-Signature: t=<unix seconds>,v1=<hex>`; `v1` is HMAC-SHA-256 with the secret over `<t>.<raw body>`:

```javascript
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 || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return res.sendStatus(400);
  const expected = createHmac('sha256', process.env.OC_WEBHOOK_SECRET).update(`${m[1]}.${raw}`).digest('hex');
  if (!timingSafeEqual(Buffer.from(m[2]), Buffer.from(expected))) return res.sendStatus(400);
  const event = JSON.parse(raw);
  // handle event.type; deduplicate on event.id; respond fast
  res.sendStatus(200);
});
```

Delivery is at-least-once (3 attempts over ~10 s, 5 s timeout each); the SDK callbacks fire in the browser regardless. Full guide: https://opencredits.ai/docs#webhooks

## Key details

- SDK URL: `https://opencredits.ai/v1/sdk.js`
- `display: 'embedded' | 'popup' | 'new_tab'` — where the OpenCredits checkout appears (default the overlay). Popup / tab are first-party: a user signed in to OpenCredits is recognised in any **website** on open (the checkout appears already signed in and `onLoginCompleted` fires with a key for this app). An app that is not a website (a webview, registered by a scheme wildcard) always asks for one Sign in click instead. `open()` must then run inside the user's click, and the app's page must not send `Cross-Origin-Opener-Policy: same-origin` (use `same-origin-allow-popups`; otherwise the SDK falls back to the overlay). Independent of `checkoutMode`, which is how Stripe's form is shown
- Callback to handle: `onCreditsAdded(data)` — `data.type` is `'purchase'` or `'permission'`; store `data.user_key`, spend up to `data.balance`
- API base URL: `https://api.opencredits.ai`
- Anthropic SDKs: base_url without `/v1` (SDK adds it)
- OpenAI SDKs: base_url with `/v1`
- User keys: `oc_sk_...`, stored in localStorage automatically
- Pricing: $1 = 100 credits, deducted by token usage
- Errors: `401` invalid key, `402` insufficient credits, `429` rate limited
- Webhooks: `X-OpenCredits-Signature: t=<unix>,v1=<hmac_sha256(secret, t + '.' + rawBody)>`, verified against the raw body — https://opencredits.ai/docs#webhooks
