cred.diy API documentation
Getting Started
Get up and running with cred.diy in minutes. Build credit systems, referral programs, and monetization for your applications.
Quick Start Steps
- 1Create an account (email verification required) and set up your first project
- 2Create credit types (e.g., "tokens", "points", "credits") to define your currency
- 3Set up pricing tiers to let users purchase credits
- 4Connect Stripe via Stripe Connect to receive payments directly
- 5Generate an API key (with optional expiration) and start making API calls
💡 After creating your project, a Getting Started Checklist appears on the dashboard to guide you through each step.
Dashboard Features
Keyboard Shortcuts
Press ⌘K (or Ctrl+K) anywhere in the dashboard to open the command palette for quick navigation.
Security Notes
- • Never expose your API key in client-side code — always use environment variables
- • Email verification is required before you can sign in
- • Set expiration dates on API keys and rotate them regularly
- • Login is rate-limited to prevent brute-force attacks (5 attempts per 60 seconds)
SDK Installation
The CRED SDK makes it easy to integrate credit systems into your TypeScript/JavaScript applications. Install the SDK and start managing credits in minutes.
Installation
npm install @cred/sdk
# or
yarn add @cred/sdk
# or
pnpm add @cred/sdkInitialize the Client
Create a CRED client instance with your API key and project ID.
import { CredClient } from '@cred/sdk';
// Initialize the client
const cred = new CredClient({
apiKey: process.env.CRED_API_KEY,
projectId: 'your_project_id',
// Which of your apps these calls come from. Sent as x-app-origin and used
// for signup attribution. In the browser it defaults to the page origin;
// on a server, set it — otherwise attribution stays blank.
appOrigin: 'https://app1.example.com'
});SDK Features
Issue Credits
// Issue credits to a user
const result = await cred.credits.issue({
user_id: 'user_123',
amount: 100,
reason: 'Welcome bonus'
});
if (result.success) {
console.log('New balance:', result.data.balance);
}Deduct Credits
// Deduct credits from a user
const result = await cred.credits.deduct({
user_id: 'user_123',
amount: 10,
reason: 'API usage'
});
if (!result.success) {
console.error('Error:', result.error?.message);
}Check Balance
// Get user's credit balance
const balance = await cred.credits.getBalance({
user_id: 'user_123'
});
console.log('Balance:', balance.data?.balance);Manage Pricing Tiers
// List all pricing tiers
const tiers = await cred.pricing.list();
tiers.data?.forEach(tier => {
console.log(`${tier.name}: ${tier.credits} credits for $${tier.price}`);
});
// Create a new pricing tier
const newTier = await cred.pricing.create({
name: 'Starter Pack',
description: 'Perfect for getting started',
price: 9.99,
credits: 100,
currency: 'USD'
});Create Checkout Sessions
// Create a checkout session
const session = await cred.checkout.create({
pricing_tier_id: 'tier_123',
user_id: 'user_456',
success_url: 'https://myapp.com/success',
cancel_url: 'https://myapp.com/cancel'
});
// Redirect user to checkout
if (session.success) {
window.location.href = session.data.checkout_url;
}User Management
// Create or get a user
const user = await cred.users.createOrGet({
external_id: 'auth0|123456',
email: 'user@example.com',
name: 'John Doe'
});
// Get user balances
const balances = await cred.users.getBalances('auth0|123456');TypeScript Support
The SDK is fully typed. Import types like CreditBalance,PricingTier,EndUser, and more from@cred/sdk.
Authentication
All API requests require authentication using your API key. Generate API keys from the API Keys page in your dashboard.
API Key Format
API keys are prefixed with sk_live_ or sk_test_ for identification. Keep your API keys secure and rotate them regularly.
Request Headers
x-api-key: sk_live_your_api_keyAPI Key Management
- • Create multiple API keys for different environments
- • Set expiration dates (30 days to 1 year) for enhanced security
- • Expired keys are highlighted and stop working automatically
- • Revoke keys instantly if compromised
- • Track last usage time for each key
Rate Limiting
API requests are limited to 100 requests per minute per API key. Rate limit information is included in response headers:
- •
X-RateLimit-Limit— Maximum requests per window (100) - •
X-RateLimit-Remaining— Requests remaining
If you exceed the limit, a 429 Too Many Requests response is returned. Implement exponential backoff in your client.
Platform Credits (CRD)
Each API call costs 1 platform credit (CRD). Your CRD balance is displayed in the dashboard header and is independent of your project credit types. If your CRD balance is depleted, API calls return 402 Payment Required. Top up from the Billing page.
Managing Credits
The credits API allows you to issue, deduct, and track credits for your users. Credits are tracked per user and per credit type.
Issue Credits
Add credits to a user's account. Users are automatically created if they don't exist.
// Issue credits to a user
// Note: user_email is recommended for wallet identity
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/issue',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({
user_id: 'user_123',
credit_type_id: 'your_credit_type_id',
amount: 100,
reason: 'Welcome bonus',
user_email: 'user@example.com', // Recommended! Same email = same wallet
user_name: 'John Doe'
})
}
);
const result = await response.json();
// {
// success: true,
// transaction_id: '...',
// user_id: 'user_123',
// credit_type_id: '...',
// amount_issued: 100,
// new_balance: 100
// }Deduct Credits
Remove credits from a user's account when they consume your service.
// Deduct credits from a user
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/deduct',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({
user_id: 'user_123',
credit_type_id: 'your_credit_type_id',
amount: 25,
reason: 'API usage'
})
}
);
const result = await response.json();
// {
// success: true,
// transaction_id: '...',
// user_id: 'user_123',
// credit_type_id: '...',
// amount_deducted: 25,
// new_balance: 75
// }Check Balance
Query a user's current credit balance across all credit types.
// Get user's credit balance
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/balance?user_id=user_123',
{
headers: {
'x-api-key': 'sk_live_your_api_key'
}
}
);
const result = await response.json();
// {
// user_id: 'user_123',
// balances: [{
// credit_type_id: '...',
// credit_type_name: 'Tokens',
// credit_type_symbol: 'TKN',
// balance: 75
// }]
// }Safe Retries
Send an Idempotency-Key header (or an idempotency_key body field) on /issue, /deduct and /refund. If a request times out, retry it with the same key: when the first attempt went through, you get its result back with idempotent_replay: true and nothing moves twice.
- • Use one key per operation, such as your job or order id (up to 255 characters)
- • Keys are scoped to your project and never expire
- • Reusing a key for a different request returns
409 IDEMPOTENCY_KEY_REUSED
Email-Based Identity
When issuing credits, always include the user_email parameter. This ensures users have one wallet across all your apps within the same project.
- • Same email = same wallet, regardless of user_id
- • Credits are automatically shared across your apps
- • Users can access balances from any app using their email
Best Practices
- • Always check balance before performing expensive operations
- • Use
reference_idto link transactions to your internal records - • Handle insufficient balance errors gracefully in your UI
- • Use webhooks to sync credit changes in real-time
End Users
Manage end users in your credit system. Users are identified by email (preferred) or external_id. The same email always maps to the same wallet within your project.
Create or Get User
Create a new user or retrieve an existing user by email. If the email already exists, the existing user is returned.
// Create or get a user
// Note: If a user with this email already exists, returns existing user's wallet
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/users',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({
external_id: 'user_123',
email: 'user@example.com', // Primary identifier - one email = one wallet
name: 'John Doe',
metadata: { plan: 'pro' }
})
}
);
const result = await response.json();
// { user: { id: '...', external_id: 'user_123', email: 'user@example.com', ... }, created: true }Bulk Sync Users
Sync up to 1000 users at once from your existing database. Great for initial setup.
// Sync all your existing users to CRED (up to 1000 per request)
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/users/bulk',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({
users: [
{ external_id: 'user_1', email: 'user1@example.com', name: 'User One' },
{ external_id: 'user_2', email: 'user2@example.com', name: 'User Two' },
{ external_id: 'user_3', email: 'user3@example.com', name: 'User Three' }
]
})
}
);
const result = await response.json();
// { success: true, created: 2, updated: 1 }List Users
Retrieve all users with pagination support.
// List all users with pagination
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/users?limit=100&offset=0',
{
headers: {
'x-api-key': 'sk_live_your_api_key'
}
}
);
const result = await response.json();
// { users: [...], total: 150, limit: 100, offset: 0 }Update User
Update user details like name or metadata.
// Update a user
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/users',
{
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({
external_id: 'user_123',
name: 'Jane Doe',
metadata: { plan: 'enterprise' }
})
}
);
const result = await response.json();
// { user: { id: '...', external_id: 'user_123', name: 'Jane Doe', ... } }User Identity Model
Users are identified primarily by email within your project:
- One email = one wallet across all your apps
- The
external_idis your internal reference - Credits issued to the same email are consolidated
- Users can access their balance from any app using their email
Signup Attribution
Track which app, landing page, or marketing channel each end user came from — and target promotions per app. Every end user carries three optional attribution fields.
The three fields
signup_url— full URL the user signed up from (e.g.https://app1.example.com/signup?ref=twitter).signup_host— normalized lowercase hostname. Auto-derived by cred fromsignup_urlif omitted; you can override by sending it explicitly.signup_credit_type_id— the credit type the user signed up for (useful for cross-app / multi-product attribution).
Sending the app origin
You have three ways to tell cred which app a call came from, checked in this order:
signup_url(orsignup_host) in the request body — best for server-to-server calls where you know the exact signup page.- An
x-app-origin: https://yourapp.comrequest header — set this once in your HTTP client. The official SDK sends it automatically when you passappOriginto the client. - The browser's
Origin/Refererheader — used automatically for calls made from a browser.
Optional (but strongly recommended) on POST /users, POST /users/bulk, /issue and /deduct — these calls always succeed without it, they just leave attribution blank.
First-touch semantics
Once set on an end user, these fields are never overwritten by later calls. Safe to send on every request — cred latches the first non-null value. Cred has no way to know which of your apps a user came from, so your app must send signup_url and signup_credit_type_id. Blank attribution is backfilled, not frozen: because cred latches the first non-null value, a user created without attribution is filled in by their next call that carries an app origin. Only users who never call again stay blank — backfill those from the dashboard.
Endpoints that accept attribution
Send the three fields as top-level body parameters on:
POST /usersandPOST /users/bulkPOST /issueandPOST /deduct(stamps the user on first-touch if they don't exist yet)POST /hosted-checkout(also stored on the resulting purchase)- Presale checkout
// Create a user with signup attribution
// signup_host is auto-derived from signup_url — no need to send it
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/users',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({
external_id: 'user_123',
email: 'user@example.com',
signup_url: 'https://app1.example.com/signup?ref=twitter',
signup_credit_type_id: 'ct_abc123'
})
}
);Filter users by attribution
GET /users accepts two additional filter query params: signup_host and signup_credit_type_id.
// Filter users by attribution
// signup_host — normalized hostname (e.g. app1.example.com)
// signup_credit_type_id — the credit type the user signed up for
const res = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/users?signup_host=app1.example.com&limit=100',
{ headers: { 'x-api-key': 'sk_live_your_api_key' } }
);GET /signup-analytics
User counts, purchase counts, and revenue grouped by signup_host. Pass days=0 for all-time (default: 30).
// GET /signup-analytics?days=30
// User counts, purchase counts, and revenue grouped by signup_host.
// Pass days=0 for all-time (default: 30).
const res = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/signup-analytics?days=30',
{ headers: { 'x-api-key': 'sk_live_your_api_key' } }
);
// {
// "breakdown": [
// {
// "signup_host": "app1.example.com",
// "signup_credit_type_id": "ct_abc",
// "signup_credit_type_name": "GEMS",
// "user_count": 1250,
// "purchase_count": 87,
// "revenue": 4321.50
// }
// ]
// }Pricing Tiers
Create pricing packages for your users to purchase credits. Pricing tiers define how many credits users get for a specific price. Prices are automatically normalized to 2 decimal places in responses and validated against Stripe's $0.50 minimum charge.
List Pricing Tiers
Retrieve all pricing tiers configured for your project.
// List all pricing tiers for your project
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/pricing-tiers',
{
headers: {
'x-api-key': 'sk_live_your_api_key'
}
}
);
const result = await response.json();
// { pricing_tiers: [{ id: '...', name: 'Starter', price: 9.99, credits: 100, ... }] }Create Pricing Tier
Programmatically create a new pricing tier. Use the returned ID for checkout sessions.
// Create a new pricing tier programmatically
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/pricing-tiers',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({
name: 'Pro Pack',
description: 'Best value for power users',
price: 29.99,
credits: 500,
currency: 'USD',
billing_type: 'one_time', // or 'subscription'
is_popular: true,
// Optional: restrict this tier to specific credit types.
// Omit or pass [] to make the tier available for ALL credit types in the project.
credit_type_ids: ['ct_abc123']
})
}
);
const result = await response.json();
// {
// success: true,
// pricing_tier: {
// id: 'tier_abc123', // Use this ID for checkout
// name: 'Pro Pack',
// price: 29.99,
// credits: 500,
// ...
// }
// }Pricing Tier Fields
| Field | Type | Description |
|---|---|---|
| name | string | Display name (e.g., "Starter Pack") |
| price | number | Price in currency (≥ $0.50 or $0.00 for free). Returned as a 2-decimal string. |
| credits | number | Number of credits included |
| billing_type | string | "one_time" or "subscription" |
| is_popular | boolean | Highlight as recommended |
| credit_type_ids | string[] | Credit types this tier can be purchased for. Empty array = applies to all active credit types in the project. |
Price Validation Rules
- Minimum price is $0.50 (Stripe requirement) or exactly $0.00 for free tiers
- Price-per-credit ratio must not exceed $1.00
- Prices are stored at 4-decimal precision and returned as 2-decimal strings
- Dashboard auto-formats prices to 2 decimal places on input
Pro Tip
Create pricing tiers in the dashboard for easy management, then use the API to fetch them for display in your app. Use the is_popular flag to highlight your best-value tier.
Credit Swaps
Enable users to exchange between different credit types atomically. Configure swap pairs with exchange rates and fees to create flexible credit economies.
List Swap Pairs
Retrieve available swap pairs for your project.
// List available swap pairs
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/swap-pairs',
{
headers: {
'x-api-key': 'sk_live_your_api_key'
}
}
);
const result = await response.json();
// { swap_pairs: [{ id: '...', source_credit_type_name: 'Gold', target_credit_type_name: 'Silver', exchange_rate: 10, ... }] }Execute Swap
Perform an atomic swap between two credit types.
// Execute a credit swap
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/swap',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({
user_id: 'user_123',
swap_pair_id: 'swap_abc123',
source_amount: 100
})
}
);
const result = await response.json();
// {
// success: true,
// transaction_id: 'swap_tx_xyz789',
// source_amount: 100,
// target_amount: 975,
// exchange_rate: 10,
// fee_amount: 25,
// source_new_balance: 400,
// target_new_balance: 1975
// }Swap Pair Fields
| Field | Type | Description |
|---|---|---|
| exchange_rate | number | How many target credits per source credit |
| fee_percent | number | Fee deducted from output (e.g., 2.5) |
| min_swap_amount | number | Minimum source credits per swap |
| max_swap_amount | number | Maximum source credits per swap |
| is_bidirectional | boolean | Can swap in both directions |
Exchange Rate Calculation
The formula is: target = source × rate × (1 - fee%/100)
Example: 100 Gold at 10:1 with 2.5% fee = 100 × 10 × 0.975 = 975 Silver
Escrow / Credit Holds
Reserve credits from a user's balance into escrow, then either release them back to the holder or capture (settle) them — optionally crediting a different end user for peer-to-peer and marketplace flows. The lifecycle is hold → release | capture; a partial capture automatically releases the uncaptured remainder back to the holder.
Create a Hold
Move credits out of a user's spendable balance and into a held state. Held credits no longer count toward their balance until released or captured.
// Reserve credits into escrow (hold)
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/escrow/hold',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({
user_email: 'buyer@example.com',
credit_type_id: 'ct_abc123',
amount: 100,
reference_id: 'order_789'
})
}
);
const result = await response.json();
// { success: true, hold_id: 'hold_abc123', status: 'held', amount: 100, new_balance: 150 }Capture or Release
Capture settles the hold — optionally to a different user. Release cancels the hold and returns the full amount to the original holder.
// Capture (settle) a hold — optionally to a different user (marketplace/P2P).
// Any uncaptured remainder is released back to the holder.
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/escrow/capture',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({
hold_id: 'hold_abc123',
to_user_email: 'seller@example.com'
// amount: 60 // optional — partial capture, remainder released
})
}
);
const result = await response.json();
// {
// success: true,
// hold_id: 'hold_abc123',
// status: 'captured',
// captured_amount: 100,
// remainder_released: 0,
// to_user_id: 'seller_456',
// recipient_new_balance: 100,
// holder_new_balance: 150
// }
// To cancel instead, POST /escrow/release with { hold_id }.Escrow API Endpoints
| Endpoint | Description |
|---|---|
| POST /escrow/hold | Reserve credits into escrow |
| POST /escrow/release | Cancel a hold, returning credits to the holder |
| POST /escrow/capture | Settle a hold, optionally to another user |
| GET /escrow | List holds (filter by status, reference_id, user) |
Marketplace Settlement Flow
- Buyer places an order — hold their credits with POST /escrow/hold
- Order is fulfilled — capture to the seller with POST /escrow/capture
- Order is cancelled — release back to the buyer with POST /escrow/release
- Partial fulfillment — capture part; the remainder is auto-released
Wallets & On-Chain Credits
A credit type can be deployed as an ERC-20, and issuing credits then mints tokens to the user who earned them. That needs an address, so every end user can carry one. Attach it yourself in a single call, or have the holder prove it by signature — both are enough to receive a mint, and the difference is recorded on the user.
Attach an Address
wallet_address is accepted on POST /users, PATCH /users and POST /users/bulk. It is stored lowercase and marked asserted — you vouched for it, nothing was proved. Pass null to detach.
// Attach an address to a user. One call, no signature — you are
// vouching for it. Use this when your app already ran its own
// wallet-connect flow and knows the address is theirs.
await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/users',
{
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({
user_email: 'user@example.com',
wallet_address: '0x1234567890abcdef1234567890abcdef12345678'
})
}
);
// The same field works on POST /users and POST /users/bulk, so a wallet
// can be attached at signup rather than in a second round trip.Prove an Address
The server picks a nonce, the holder signs a message containing it, and the server recovers the signer. Because the server chose the nonce, a signature harvested from another site or an earlier session cannot be replayed here. On success wallet_verified_at is set.
// Or prove it. Two calls: ask for a message, send back the signature.
// 1. Get a challenge. The message names YOUR project, not Cred — it is
// what the user reads in their wallet popup, so show it unmodified.
const challenge = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/wallet/challenge',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({
user_email: 'user@example.com',
address: await signer.getAddress()
})
}
).then(r => r.json());
// { nonce: '9f8e…', address: '0x1234…', message: 'Your App wants to…',
// expires_at: '2026-08-17T12:10:00.000Z' }
// 2. Sign it and send it back.
const signature = await signer.signMessage(challenge.message);
const result = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/wallet/verify',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({ nonce: challenge.nonce, signature })
}
).then(r => r.json());
// { verified: true, user: { wallet_address: '0x1234…',
// wallet_verified_at: '2026-08-17T12:01:04.221Z' } }
// A challenge is single-use and lasts ten minutes. A rejected signature
// consumes it too — retry means requesting a fresh one.Mint on Issue
When a credit type has minting enabled, POST /issue returns an on_chain block alongside the balance. Every reason a mint does not happen is reported rather than silently skipped.
// With a wallet on file and an on-chain credit type, issuing credits
// also mints tokens. The response tells you what happened on-chain.
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/issue',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({
user_id: 'user_123',
user_email: 'user@example.com',
credit_type_id: 'your_credit_type_id',
amount: 100,
reason: 'Welcome bonus',
// Optional: mint this one issuance elsewhere, ignoring the
// address on the user.
wallet_address: '0xabcdef1234567890abcdef1234567890abcdef12'
})
}
);
const data = await response.json();
// {
// balance: 100,
// transaction_id: 'txn_abc123',
// on_chain: {
// status: 'enqueued',
// mint_id: 'mint_abc',
// transaction_hash: '0x9f8e…'
// }
// }
// The ledger is the system of record and the chain follows it. A mint that
// cannot be sent NEVER fails the issuance — you get status 'skipped' and a
// reason instead:
// "no wallet address for this end user — set end_users.wallet_address"
if (data.on_chain?.status === 'skipped') {
console.warn('Credits issued, no token minted:', data.on_chain.reason);
}Read the Contract
GET /credit-types publishes the contract address, chain id and decimals, so your app can link to an explorer or convert amounts without hardcoding anything.
// Read which credit types are on-chain, and where they live.
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/credit-types',
{ headers: { 'x-api-key': 'sk_live_your_api_key' } }
);
// {
// "credit_types": [{
// "id": "ct_abc",
// "name": "Game Coins",
// "symbol": "GC",
// "exchange_rate": 100,
// "is_on_chain": true,
// "blockchain": "base-sepolia",
// "contract_address": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
// "chain_id": 84532,
// "token_decimals": 18,
// "mint_on_issue": true,
// "deployment_type": "managed"
// }]
// }
// Enough to build an explorer link, convert a credit amount to atomic
// units, or offer "add this token to your wallet".Wallet API Endpoints
| Endpoint | Description |
|---|---|
| POST /wallet/challenge | Issue a single-use message for the holder to sign |
| POST /wallet/verify | Recover the signer and record the proved address |
| GET /wallet | Read the address on file and whether it was proved |
| DELETE /wallet | Detach the address — balances are untouched |
Deposits — tokens coming back
A credit type can collect deposits at an address, and tokens sent there credit the depositor's ledger balance. This is the only path that runs chain → ledger, so the guards are strict: only the configured token into the configured address, only after the confirmations you choose, never a mint from the zero address, and idempotent on (chain_id, transaction_hash, log_index).
The paying address must already be attached to a user. Tokens from an unrecognised address are recorded as unattributed and held rather than credited — read them with GET /deposits and resolve them by attaching the address.
Deposits and mint-on-issue are mutually exclusive, enforced in the database. Minting mirrors an issuance rather than moving it, so a holder issued 100 credits also holds 100 tokens — depositing those back would credit them twice, and again, and again.
What the API will not do
Deploying a token, enabling minting on a credit type, and setting the address that x402 payments settle to, and configuring deposits all stay in the dashboard, behind your operator login. Each of them either spends gas or decides where money lands, and an API key is a credential your app servers hold — not one that should be able to move either.
Deploy the token once from Dashboard → Credit Types, then everything above works from your app with an API key.
Agent Payments (x402)
An AI agent has no card, no checkout session and no patience for a redirect. x402 is the open standard that turns HTTP's long-unused 402 Payment Required into a price quote: the server answers with what it costs, the caller signs a stablecoin transfer, retries the same request, and gets served. Cred speaks x402 version 2 in both directions.
The handshake
- Client makes a normal request.
- Server answers 402 with a PAYMENT-REQUIRED header — base64 JSON naming the scheme, network, asset, amount and recipient.
- Client signs it and retries with a PAYMENT-SIGNATURE header.
- Server verifies and settles through a facilitator, then returns the result plus a PAYMENT-RESPONSE receipt.
Pay to keep calling the API
When your CRD platform balance runs out, credits-api no longer just fails — it quotes a top-up. An autonomous caller can buy credit and complete the request it was already making, without a human opening the dashboard.
// Any credits-api call can now be paid for inline. When your CRD runs
// out the API answers 402 with a price instead of simply failing.
const call = () => fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/balance?user_id=user_123',
{ headers: { 'x-api-key': 'sk_live_your_api_key' } }
);
let response = await call();
if (response.status === 402) {
const offer = JSON.parse(atob(response.headers.get('PAYMENT-REQUIRED')));
// offer.accepts[0] -> { scheme, network, amount, asset, payTo, extra }
const payload = await wallet.signX402(offer.accepts[0]);
// Retry the *same* request with the signed payment attached.
response = await fetch(response.url, {
headers: {
'x-api-key': 'sk_live_your_api_key',
'PAYMENT-SIGNATURE': btoa(JSON.stringify(payload))
}
});
// 200 OK — credits were topped up and the original request ran.
// PAYMENT-RESPONSE header carries the on-chain settlement receipt.
}Sell your credits to agents
Turn on x402 under Dashboard → Agent Payments, give a receiving wallet and a price per credit, and any agent can buy your credits with stablecoin. Payments settle straight to your wallet — Cred quotes, verifies and issues, but never custodies the funds. Buyers with no Cred account are identified by the wallet they paid from.
// 1. Discover what Cred accepts — no credentials needed.
const res = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/x402/supported' +
'?project_id=your_project_id'
);
const support = await res.json();
// {
// x402Version: 2,
// enabled: true,
// kinds: [{ x402Version: 2, scheme: 'exact', network: 'eip155:84532' }],
// asset: { address: '0x036CbD...', name: 'USDC', decimals: 6 },
// facilitator: 'https://x402.org/facilitator',
// platform_topup: { credits: 100, symbol: 'CRD', price_usd: 1, amount: '1000000' },
// project: {
// accepts_x402: true,
// purchase_url: '.../credits-api/x402/purchase',
// credit_type: { id: '...', name: 'Game Coins', symbol: 'GC' },
// price_per_credit_usd: 0.01,
// min_credits: 1,
// max_credits: 10000
// }
// }Buying, end to end
// 2. Ask to buy. With no payment attached you get a 402 carrying the price.
const url =
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/x402/purchase';
const order = { project_id: 'your_project_id', credits: 500 };
const quote = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(order)
});
// quote.status === 402
const paymentRequired = JSON.parse(atob(quote.headers.get('PAYMENT-REQUIRED')));
// 3. Sign the chosen requirements with your wallet (EIP-3009 for USDC), then
// retry the same request with the signed payload.
const paid = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'PAYMENT-SIGNATURE': btoa(JSON.stringify(signedPaymentPayload))
},
body: JSON.stringify(order)
});
const receipt = await paid.json();
// {
// success: true,
// credits_purchased: 500,
// new_balance: 500,
// buyer_id: 'x402:0xAgentWallet...',
// payer: '0xAgentWallet...',
// price_paid: 5,
// currency: 'USDC',
// transaction: '0xabc...'
// }
// The settlement receipt is also on the PAYMENT-RESPONSE response header.x402 Endpoints
| Endpoint | Description |
|---|---|
| GET /x402/supported | Public. What Cred accepts, and a project's credit price list |
| POST /x402/purchase | Public. Buy a project's credits by paying in stablecoin |
| GET /x402/receipts | Payments taken for your project, with transaction hashes |
| any endpoint, on 402 | Attach PAYMENT-SIGNATURE to top up CRD and run the request |
Deployment configuration
x402 stays switched off until X402_PAY_TO names a receiving wallet — an advertised price with nowhere to pay it would strand every agent that tried. Everything else has a Base Sepolia default.
| Variable | Default |
|---|---|
| X402_PAY_TO | required — treasury wallet for CRD top-ups |
| X402_NETWORK | eip155:84532 |
| X402_ASSET | 0x036CbD…CF7e (USDC) |
| X402_FACILITATOR_URL | https://x402.org/facilitator |
| X402_CRD_PRICE_USD | 0.01 per CRD |
| X402_CRD_TOPUP_CREDITS | 100 CRD per top-up |
| X402_ENABLED | set to false to turn off without unsetting the wallet |
The defaults target Base Sepolia testnet through the public facilitator. Before pointing X402_NETWORK at a mainnet chain, move X402_FACILITATOR_URL to a production facilitator — the x402 spec explicitly says the public one is for development and testnets.
Settling in a different asset
By default every project quotes the platform's asset. Override it per project in Dashboard → Agent Payments: token contract, decimals, network, and the token's EIP-712 domain name and version. GET /x402/supported?project_id=… then publishes your asset alongside the platform's, so an agent learns which token to hold before it signs anything.
The asset and its decimals are one setting. An override without decimals is ignored rather than half-applied — quoting a new contract with the old contract's decimals is a payment wrong by orders of magnitude.
Stablecoins only. The quote is your price per credit times the number of credits, expressed in the asset's units, with no price source — which assumes one unit is one dollar. True of a USD stablecoin, false of anything that floats.
Through MCP
The Cred MCP server exposes the same thing to agents as tools: x402_supported to discover prices and buy_credits_x402 to purchase. Any other tool that hits a spent CRD balance returns the x402 payment requirements as its structured error content, so a paying client can sign and retry. Neither tool needs an API key — the payment is the credential.
Payment Rails
Cred has three ways to take money, and they differ in one thing that matters more than fees: who the merchant of record is. That decides who a processor terminates when something goes wrong, and it is why not every business can use every rail.
| Rail | Merchant of record | Who can use it | Pays end users out? |
|---|---|---|---|
| Stripe Connectplatform account | Cred | Permitted categories only | Yes |
| Your own keysyour Stripe account | You | Any category | Yes |
| Stablecoinx402, settles to your wallet | No processor involved | Any category | No |
If Stripe won't take your business
Card processors decline a long list of lawful businesses — adult content & services, gambling & real-money gaming, cannabis, cbd & hemp, firearms, ammunition & weapons, and others. Those verticals cannot use Cred's platform Stripe account, because under Connect it is Cred's account that answers for them and one restricted merchant would cost every other operator on the platform their payments.
Everything else in Cred works exactly the same for you: credit types, pricing tiers, wallets, escrow, the API, webhooks. You have two rails:
- Bring your own processor keys. Add a secret key under Dashboard → Payments → Your Own Keys and you become the merchant of record. Hosted checkout, refunds and payouts all keep working. Whether your processor permits your business is between you and them — read their restricted-business rules before you build on it.
- Take stablecoin over x402. No card network, no underwriting, no chargebacks. Set a receiving address and a price per credit under Dashboard → Agent Payments and any buyer — a person with a browser wallet or an autonomous agent — can pay you directly. Funds settle to an address you control; Cred never custodies them.
Which categories sit on which side is set out in the Acceptable Use Policy, and declared per project under Dashboard → Payments → Business & compliance.
The one thing stablecoin can't do
Cash redemption — paying an end user real money for their credits — runs on Stripe Connect Express transfers. There is no stablecoin equivalent in Cred today, so if your credit type is redeemable and you are on the crypto rail, buyers can purchase but cannot cash out. Sell-only credit types are unaffected. If redemption matters to you, the own-keys rail is the one to plan around.
Where each sale shows up
Every rail writes the same ledger. A purchase row carries payment_provider naming the rail that settled it (stripe or x402), so revenue, purchase history and analytics read the same whichever way your customers paid. Crypto payments additionally carry on-chain transaction hashes — see GET /x402/receipts under Agent Payments.
Hosted Checkout
Let your users purchase credits through a Stripe-powered checkout. Set up pricing tiers in the dashboard and generate checkout sessions via API. Payments go directly to your connected Stripe account.
Create Checkout Session
Generate a checkout URL and redirect your users to complete their purchase.
// Create a hosted checkout session for credit purchases
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/hosted-checkout',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({
user_id: 'user_123',
user_email: 'user@example.com',
pricing_tier_id: 'your_pricing_tier_id',
success_url: 'https://yourapp.com/success',
cancel_url: 'https://yourapp.com/cancel',
// billing: 'recurring' // Explicitly opt in to monthly billing
})
}
);
const result = await response.json();
// {
// url: 'https://checkout.stripe.com/...',
// session_id: '...',
// purchase_id: '...'
// }
// Redirect user to result.url to complete purchaseCheckout Parameters
| Parameter | Type | Description |
|---|---|---|
| user_id | string | Your end-user's external identifier |
| user_email | string | User's email for receipt |
| pricing_tier_id | string | ID of the pricing tier to purchase |
| success_url | string | Redirect URL after successful payment |
| cancel_url | string | Redirect URL if user cancels |
Stripe Connect
Connect your Stripe account to receive payments directly. Set up Stripe Connect from the Payments page in your dashboard. A small platform fee is applied to each transaction.
Subscriptions
Charge customers monthly and add credits after every paid invoice. A checkout becomes recurring only when the request explicitly sends billing: "recurring".
https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/hosted-checkoutStart a monthly subscription. The pricing tier must support recurring billing. Omitting billing always creates a one-time checkout.
Body parameters
user_idstringrequiredYour unique identifier for the customerpricing_tier_idstringrequiredA pricing tier with recurring or both billing enabledbillingstringrequiredSet to "recurring"user_emailstringCustomer emailsuccess_urlstringHTTPS redirect after checkoutcancel_urlstringHTTPS redirect when checkout is canceled
Example request
curl -X POST "https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/hosted-checkout" \
-H "x-api-key: sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"user_id": "user_123",
"user_email": "user@example.com",
"pricing_tier_id": "tier_monthly",
"billing": "recurring"
}'Response
{
"url": "https://checkout.stripe.com/...",
"session_id": "cs_...",
"purchase_id": "purchase_..."
}/subscriptionsList subscriptions by customer or status. status=active includes active, trialing, and past_due subscriptions.
Query parameters
user_idstringFilter by external user IDemailstringFilter by customer emailstatusstringactive or an exact Stripe subscription status
Example request
curl -X GET "https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/subscriptions?email=user@example.com&status=active" \
-H "x-api-key: sk_live_your_api_key"Response
{
"subscriptions": [{
"id": "subscription_...",
"stripe_subscription_id": "sub_...",
"user_id": "user_123",
"user_email": "user@example.com",
"credits_per_period": 1000,
"price": "10.00",
"currency": "USD",
"interval": "month",
"status": "active",
"current_period_end": "2026-11-02T19:00:00Z",
"cancel_at_period_end": false
}]
}/subscriptions/cancelCancel one subscription by ID, or every live subscription for a customer by user ID or email. Cancellation defaults to the end of the paid period and is idempotent.
Body parameters
subscription_idstringCred subscription ID or Stripe sub_ IDuser_idstringCancel all live subscriptions for this useremailstringCancel all live subscriptions for this emailat_period_endbooleanDefault true; false stops immediately without a refund
Example request
curl -X POST "https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/subscriptions/cancel" \
-H "x-api-key: sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "subscription_id": "sub_123", "at_period_end": true }'Response
{
"success": true,
"at_period_end": true,
"subscriptions": [{ "id": "subscription_...", "status": "active", "cancel_at_period_end": true }]
}/subscriptions/portalCreate a short-lived Stripe-hosted link for updating a payment method or viewing invoices. Create it when the customer clicks; do not store it. Plan changes and cancellation are disabled.
Body parameters
subscription_idstringCred subscription ID or Stripe sub_ IDuser_idstringUse this customer's most recent subscriptionemailstringUse this customer's most recent subscriptionreturn_urlstringHTTPS page to return toflowstring"payment_method_update" (default) or "overview"
Example request
curl -X POST "https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/subscriptions/portal" \
-H "x-api-key: sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "email": "user@example.com", "flow": "payment_method_update" }'Response
{
"url": "https://billing.stripe.com/p/session/...",
"subscription": { "id": "subscription_...", "status": "past_due" }
}Renewals and lifecycle events
Credits are issued once per paid invoice, including the first invoice and every renewal. Own-keys renewals and cancellations can take up to about ten minutes to appear; Stripe Connect events arrive immediately.
subscription.created— first checkout completedsubscription.renewed— invoice paid and credits addedsubscription.cancel_scheduled— cancellation set for period endsubscription.canceled— subscription endedsubscription.payment_failed— payment failed; create a portal link when the customer chooses to fix it
Saved Cards & Auto Top-Up
Keep a buyer's card from a one-time checkout, then buy packs on it later without the buyer present — for example when their balance runs low. A card is saved only when the checkout request sends save_payment_method: true. One card per user; saving again replaces it.
https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/hosted-checkoutOne-time checkout that also saves the card for off-session charges. Not allowed with billing: recurring.
Body parameters
user_idstringrequiredYour unique identifier for the customerpricing_tier_idstringrequiredThe pack to buy nowsave_payment_methodbooleanrequiredSet to true to keep the carduser_emailstringCustomer email
Example request
curl -X POST "https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/hosted-checkout" \
-H "x-api-key: sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "user_id": "user_123", "pricing_tier_id": "tier_10k", "save_payment_method": true }'Response
{ "url": "https://checkout.stripe.com/...", "session_id": "cs_...", "purchase_id": "..." }/payment-methodThe user's saved card, or null. Stripe ids are never returned.
Query parameters
user_idstringrequiredExternal user ID
Example request
curl "https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/payment-method?user_id=user_123" \
-H "x-api-key: sk_live_your_api_key"Response
{ "payment_method": { "brand": "visa", "last4": "4242", "exp_month": 12, "exp_year": 2030, "created_at": "...", "updated_at": "..." } }/payment-methodDetach the saved card in Stripe and forget it. Send user_id in the JSON body.
Body parameters
user_idstringrequiredExternal user ID
Example request
curl -X DELETE "https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/payment-method" \
-H "x-api-key: sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "user_id": "user_123" }'Response
{ "deleted": true }
// no card saved:
{ "deleted": false, "code": "no_saved_payment_method" }/charge-savedBuy a one-time pack on the saved card, off-session. Requires an idempotency key: a retry with the same key returns the first attempt's outcome and never charges twice. Credits are issued before a 200 returns (a credits.issued webhook with billing: auto_topup also fires). requires_action (3-D Secure) cannot be completed without the buyer, so it is a 402 — send them through checkout instead.
Body parameters
user_idstringrequiredExternal user ID with a saved cardpricing_tier_idstringrequiredA one_time or both pricing tieridempotency_keystringrequiredOr the Idempotency-Key header; max 255 charscredit_type_idstringWhen the tier applies to several credit types
Example request
curl -X POST "https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/charge-saved" \
-H "x-api-key: sk_live_your_api_key" \
-H "Idempotency-Key: autotopup-user_123-0001" \
-H "Content-Type: application/json" \
-d '{ "user_id": "user_123", "pricing_tier_id": "tier_10k" }'Response
// 200 — charged and credited
{ "status": "succeeded", "purchase_id": "...", "payment_intent_id": "pi_...", "credits": 10000, "new_balance": 12500 }
// 202 — settling; credits arrive by webhook
{ "status": "processing", "purchase_id": "...", "payment_intent_id": "pi_..." }
// 402 — declined or needs the buyer
{ "status": "failed", "purchase_id": "...", "code": "card_declined", "decline_code": "insufficient_funds", "message": "...", "requires_action": false }
// 404 — { "error": "...", "code": "no_saved_payment_method" }
// 409 — "IDEMPOTENCY_KEY_REUSED" (other user) or "saved_payment_method_unusable" (card on a Stripe account the project no longer uses)
// 502 — "charge_not_started": retry with the SAME keyRefunds
Reverse a credit purchase or transaction. The originally issued credits are clawed back and, for Stripe-backed purchases, an optional money refund is issued. Refunds are idempotent — each source transaction can be refunded only once. To retry a timed-out refund safely (including partial refunds), send an Idempotency-Key header: a retry with the same key returns the original refund.
Refund a Purchase
Identify what to refund with exactly one of purchase_id, stripe_payment_intent_id, transaction_id, or reference_id. Pass amount for a partial refund.
// Refund a purchase (claws back credits + optional Stripe money refund)
// Identify the source with ONE of: purchase_id, stripe_payment_intent_id,
// transaction_id, or reference_id. Refunds are idempotent.
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/refund',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({
purchase_id: 'pur_abc123',
// amount: 50, // optional — partial refund
// refund_payment: true, // optional — also refund the Stripe payment (default true)
reason: 'Customer requested refund'
})
}
);
const result = await response.json();
// {
// success: true,
// refund_transaction_id: 'tx_refund_123',
// refunded_credits: 100,
// partial: false,
// new_balance: 200,
// credits_clawed_back: true,
// payment_refunded: true,
// stripe_refund_id: 're_1Nv9...',
// source: 'purchase'
// }Refund API Endpoint
| Endpoint | Description |
|---|---|
| POST /refund | Reverse a purchase/transaction; optional Stripe money refund |
Common Errors
// 404 - No matching purchase/transaction found
{ "error": "Purchase not found" }
// 409 - This source was already refunded (idempotency guard)
{ "error": "Already refunded" }
// 400 - Requested amount is larger than the original
{ "error": "Refund exceeds original amount" }
// 402 - Stripe money refund failed; no credits were changed
{ "error": "Stripe refund failed", "stripe_error": "..." }Pre-Sales
Sell credits before your product launches. Create pre-sale campaigns to gauge interest, fund development, and build your user base early.
Create Pre-Sale Checkout
Generate a checkout session for pre-sale purchases. Credits will be tracked and can be distributed when your product launches.
// Create a presale checkout session
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/presale-checkout',
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
presale_id: 'your_presale_id',
email: 'buyer@example.com',
name: 'John Doe',
success_url: 'https://yourapp.com/presale-success',
cancel_url: 'https://yourapp.com/presale-cancel'
})
}
);
const result = await response.json();
// {
// url: 'https://checkout.stripe.com/...',
// session_id: '...',
// order_id: '...'
// }Pre-Sale Features
- • Public pages: Each pre-sale gets a shareable public page
- • Launch dates: Set expected launch dates for transparency
- • Order tracking: Monitor all pre-sale orders in the dashboard
- • Email collection: Collect buyer emails for updates
- • Credit allocation: Link pre-sales to specific credit types and pricing
Referral Program
Create a referral program for your users to earn credits by inviting others. Configure rewards for both referrers and referees programmatically via the API.
Configure Referral Settings
Set up your referral program by configuring how many credits referrers and referees receive.
// Configure referral program settings
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/referral-settings',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({
is_active: true,
referrer_credits: 100,
referee_credits: 50,
credit_type_id: 'your_credit_type_id'
})
}
);
const result = await response.json();
// {
// success: true,
// settings: {
// id: '...',
// is_active: true,
// referrer_credits: 100,
// referee_credits: 50,
// credit_type_id: '...',
// credit_type_name: 'Tokens'
// }
// }Generate Referral Code
Generate a unique referral code for a user. If the user already has a code, it returns the existing one.
// Generate a referral code for a user
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/referrals/generate-code',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({
user_id: 'user_123'
})
}
);
const result = await response.json();
// {
// success: true,
// user_id: 'user_123',
// referral_code: 'ABC12XYZ'
// }Apply Referral Code
When a new user signs up with a referral code, apply it to award credits to both parties.
// Apply a referral code for a new user
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/referrals/apply',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_your_api_key'
},
body: JSON.stringify({
user_id: 'new_user_456',
referral_code: 'ABC12XYZ'
})
}
);
const result = await response.json();
// {
// success: true,
// referral_id: '...',
// referrer_credits_issued: 100,
// referee_credits_issued: 50
// }List Referrals
Retrieve all referrals for your project with optional filtering by status.
// List all referrals for your project
const response = await fetch(
'https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/referrals?status=completed&limit=50',
{
headers: {
'x-api-key': 'sk_live_your_api_key'
}
}
);
const result = await response.json();
// {
// referrals: [{ id: '...', referral_code: 'ABC12XYZ', status: 'completed', ... }],
// total: 25,
// limit: 50,
// offset: 0
// }Referral API Endpoints
| Endpoint | Description |
|---|---|
| GET /referral-settings | Get current referral program settings |
| POST /referral-settings | Create or update referral settings |
| POST /referrals/generate-code | Generate a referral code for a user |
| POST /referrals/apply | Apply a referral code (issues credits) |
| GET /referrals | List all referrals with filtering |
Integration Flow
- Configure your referral settings (credits for referrer/referee)
- Generate referral codes for existing users
- Display the code in your app for users to share
- When new users sign up, apply the referral code
- Credits are automatically issued to both parties
Webhooks
Receive real-time notifications when events happen in your cred.diy project. Configure webhook endpoints from the Webhooks page in your dashboard.
Webhook Events
credits.issuedCredits were added to a user's accountcredits.deductedCredits were removed from a user's accountcredits.refundedA credit transaction was reversedbalance.lowUser's balance dropped below thresholdpurchase.completedA credit purchase was completedpurchase.failedA credit purchase payment failedswap.completedA credit swap was successfully executedreferral.completedA referral was successfully processedsubscription.createdA recurring checkout created a subscriptionsubscription.renewedA paid renewal added creditssubscription.cancel_scheduledA subscription will end after its paid periodsubscription.canceledA subscription endedsubscription.payment_failedA renewal payment failed and needs customer actionPayload Envelope
Every delivery includes a unique event id (sent as both id and event_id), the event name as event and type, and a created_at timestamp, alongside the event-specific fields. The same values are mirrored in the X-Cred-Event-Id and X-Cred-Timestamp headers. Use the event id to make your handler idempotent — transient failures (network errors, 5xx, 429) are retried up to three times.
Handling Webhooks
// Set up webhook listener (Express.js example)
const crypto = require('crypto');
app.post('/webhooks/cred', (req, res) => {
// Verify signature
const signature = req.headers['x-cred-signature'];
const expectedSig = crypto
.createHmac('sha256', 'your_webhook_secret')
.update(JSON.stringify(req.body))
.digest('hex');
if (signature !== expectedSig) {
return res.status(401).send('Invalid signature');
}
const event = req.body;
switch (req.headers['x-cred-event']) {
case 'credits.issued':
console.log('Credits issued:', event);
break;
case 'credits.deducted':
console.log('Credits deducted:', event);
break;
case 'purchase.completed':
console.log('Purchase completed:', event);
break;
}
res.status(200).send('OK');
});Testing Webhooks
Use the Test button next to each webhook endpoint in the dashboard to send a test.ping event. This verifies your endpoint is reachable and responding with a 200 status code before going live.
The test payload includes a X-Cred-Signature header so you can also verify your signature validation logic.
Webhook Security
- • Each endpoint has a unique secret generated at creation time
- • Verify the
X-Cred-Signatureheader using HMAC-SHA256 - • Failed deliveries are visible in the delivery log with full payload/response details
- • Always respond with
200 OKto acknowledge receipt
MCP Server (AI Agents)
cred.diy exposes a Model Context Protocol (MCP) server so AI agents like Claude, Cursor, Windsurf, and others can manage credits, users, pricing, swaps, and referrals on your behalf using natural language.
MCP Endpoint
https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/cred-mcpTransport: Streamable HTTP — compatible with any MCP client that supports the HTTP transport.
Authentication
Pass your API key and project ID as HTTP headers when connecting. These are set once at the connection level — you don't need to provide them on every tool call.
x-api-keyYour cred.diy API key (e.g. sk_live_...)x-project-idYour project UUID from the dashboardAlternatively, each tool also accepts optional api_key and project_id parameters to override the connection-level credentials.
Setup: Claude Desktop
Add the following to your claude_desktop_config.json:
{
"mcpServers": {
"cred": {
"url": "https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/cred-mcp",
"headers": {
"x-api-key": "sk_live_your_api_key",
"x-project-id": "your_project_id"
}
}
}
}Setup: Cursor / Windsurf
Add an MCP server in your IDE settings:
// In Cursor Settings → MCP Servers → Add Server
// Name: cred
// Type: Streamable HTTP
// URL: https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/cred-mcp
// Headers:
// x-api-key: sk_live_your_api_key
// x-project-id: your_project_idAvailable Tools
The MCP server exposes 27 tools covering the full cred.diy API surface:
Available MCP Tools:
─────────────────────────────────────────
Credits
• issue_credits — Issue credits to a user
• deduct_credits — Deduct credits from a user
• get_balance — Get a user's credit balance
• get_transactions — List transaction history
Users
• create_user — Create or get a user
• get_users — List all users
• update_user — Update user profile
• delete_user — Delete a user
• bulk_create_users — Bulk sync users (up to 1000)
Credit Types
• list_credit_types — List all credit types
Pricing
• list_pricing_tiers — List pricing tiers
• create_pricing_tier — Create a pricing tier
Swaps
• list_swap_pairs — List swap pairs
• execute_swap — Execute a credit swap
Cash Redemption
• onboard_payout_account — Start Stripe Connect onboarding for a user
• redeem_credits — Cash out credits to a connected Stripe account
Referrals
• get_referral_settings — Get referral config
• save_referral_settings — Update referral config
• generate_referral_code — Generate a referral code
• apply_referral — Apply a referral code
• list_referrals — List all referrals
Wallets
• set_user_wallet — Attach an asserted wallet
• get_user_wallet — Read a user's wallet
• request_wallet_challenge — Request a proof message
• verify_wallet_signature — Verify wallet ownership
x402
• x402_supported — Discover accepted assets and prices
• buy_credits_x402 — Buy credits with stablecoinExample: AI Agent Conversation
Once connected, your AI assistant can manage credits through natural language:
User: "Issue 500 welcome credits to alice@example.com"
AI Agent calls → issue_credits({
user_email: "alice@example.com",
user_id: "alice",
amount: 500,
credit_type_id: "ct_abc123",
reason: "Welcome bonus"
})
→ ✅ Credits issued. New balance: 500Tips
- •The MCP server uses the same API keys as the REST API — no separate credentials needed.
- •All tools return structured JSON, making it easy for AI agents to process and present results.
- •Rate limits (100 req/min per key) apply the same as the REST API.
- •Use the "Copy Docs for AI" button on this page to give your AI assistant full API context.
API Reference
Complete reference for all API endpoints. Base URL: https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api
Quick Start
1. Generate an API key from the API Keys page (set an expiration date for security).
2. Include your API key in the x-api-key header.
3. Make requests to the endpoints below.
Authentication
All requests require an API key in the header. Rate limit: 100 requests/minute per key.
curl -H "x-api-key: sk_live_your_api_key" https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/balance?user_id=user_123/balanceGet the credit balance for a user. Supports email-first lookup for unified wallets.
Query parameters
emailstringUser's email (preferred, takes priority over user_id)user_idstringYour unique identifier for the user (required if email not provided)credit_type_idstringFilter to specific credit type
Example request
# Preferred: use email for cross-app wallet lookup
curl -X GET "https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/balance?email=user@example.com" \
-H "x-api-key: sk_live_your_api_key"
# Alternative: use user_id
curl -X GET "https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/balance?user_id=user_123" \
-H "x-api-key: sk_live_your_api_key"Response
{
"user_id": "user_123",
"email": "user@example.com",
"balances": [{
"credit_type_id": "ct_abc123",
"credit_type_name": "Tokens",
"credit_type_symbol": "TKN",
"balance": 75
}]
}Webhooks
Configure webhooks to receive real-time notifications when credit events occur. Use the Test button in the dashboard to verify your endpoints.
Available Events
credits.issuedcredits.deductedcredits.refundedpurchase.completedpurchase.failedswap.completedreferral.completedbalance.lowsubscription.createdsubscription.renewedsubscription.cancel_scheduledsubscription.canceledsubscription.payment_failed
Verify the X-Cred-Signature header using HMAC-SHA256 with your webhook secret.
Error Codes
| Code | Description |
|---|---|
| 400 | Bad Request — invalid parameters or insufficient user credits (code: INSUFFICIENT_CREDITS) |
| 401 | Unauthorized — invalid or missing API key |
| 402 | Payment Required — insufficient platform credits (CRD) |
| 404 | Not Found — user or resource not found |
| 409 | Conflict — already processed (e.g. a refund that was already issued) |
| 429 | Rate Limit Exceeded — 100 requests/minute per key |
| 500 | Internal Server Error |
Your first credit is one request away.
Create a project, copy an API key, and issue a credit from your server.
