Developer Documentation
The complete guide to integrating Microz microtransaction settlement into your application.
Overview
Microz is a patent-pending microtransaction settlement APIthat lets you charge end-users sub-dollar amounts — as low as $0.005 per action — without incurring per-transaction card processing fees. Instead of hitting the card network on every charge, Microz accumulates micro-charges in a ledger and settles them as a single card charge when the balance reaches a threshold.
How it works
- User registers— Your frontend collects a card via Stripe.js (using Microz's Stripe publishable key). You send the resulting
pm_xxxtoken to Microz to register the user. - You charge per action— On every billable event (AI inference, API call, content generation, etc.), your backend calls
POST /v1/charge. This is a ledger entry — no card network communication, responds in <50ms. - Microz settles automatically— When the user's accumulated balance hits $5.00 (threshold trigger) or Day 6arrives (policy window trigger) — whichever comes first — Microz charges the card once and distributes payouts to merchants via Stripe Connect.
The billing model
Microz operates as a Payment Facilitator (PayFac). End-user cards are tokenized under Microz's own Stripe account. When settlement occurs, Microz charges the card and distributes merchant payouts via Stripe Connect. The statement descriptor reads MICROZ.NET.
You define a cost-per-unit (COGS)for each app you register. Microz uses this to calculate the dynamic floor — the minimum balance required to settle without losing money on processing fees. This means you never pay more in card fees than you collect.
Microcents
All monetary values in the API use microcents for sub-cent precision:
| Amount | Microcents |
|---|---|
| $1.00 | 1,000,000 |
| $0.25 | 250,000 |
| $0.01 (1 cent) | 10,000 |
| $0.005 (half-cent) | 50,000 |
| $5.00 (default threshold) | 5,000,000 |
Formula: microcents = dollars * 1,000,000. One cent = 10,000 microcents.
Getting Started
1. Sign up and get API keys
Create an account at microz.net/signup. From your dashboard, copy your API key. Keys are prefixed with mz_live_ for production and mz_test_ for sandbox environments.
MICROZ_API_KEY=mz_live_your_secret_key_here
2. Create an app with COGS
In your dashboard, create an app and define the cost-per-unit (COGS) in microcents. This represents your actual cost for each billable unit (e.g., what you pay OpenAI per inference). Microz uses this to calculate the dynamic settlement floor.
curl -X POST https://api.microz.net/v1/apps \
-H "X-API-Key: mz_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "My AI App",
"slug": "my-ai-app",
"cogs_per_unit_microcents": 30000,
"price_per_unit_microcents": 50000,
"description": "GPT-4 powered writing assistant"
}'The slug is what you pass in every charge request to attribute the charge to the correct app. The price_per_unit_microcents is what the user pays per unit; the cogs_per_unit_microcents is your actual cost.
3. Integrate
The integration has two parts: a frontend piece (Stripe.js card collection) and a backend piece (registering users, charging, handling webhooks). Both are covered in detail below.
Architecture
Understanding the system topology helps you design the right integration.
Your Frontend Your Backend Microz API Stripe
============ ============ ========== ======
| | | |
1. |--- Stripe.js ------->| | |
| (card form) | | |
| | | |
2. | pm_xxx token ------>| | |
| | | |
3. | |-- POST /v1/users/ -->| |
| | register |--- Attach PM -->|
| | | |
4. | |<-- user_id ----------| |
| | | |
5. | (user makes | | |
| AI request) | | |
| | | |
6. | |-- POST /v1/charge -->| |
| | (ledger only, | (no card hit) |
| | <50ms) | |
| | | |
7. | | ... charges accumulate ... |
| | | |
8. | | |-- Settle ------->|
| | | (single charge) |
| | | |
9. | |<-- webhook: ---------| |
| | settlement.completed| |
Key architectural points
- Stripe.js runs with Microz's publishable key, not yours. Cards are tokenized under Microz's Stripe account.
- Your backend never touches raw card data. You only ever handle
pm_xxxtokens. - Charges are ledger entries, not card charges. They're instant and free.
- Settlements happen asynchronously.You're notified via webhooks.
- Merchant payouts are handled via Stripe Connect. You set up a connected account during onboarding.
Frontend Integration
The frontend is responsible for one thing: collecting the user's card and obtaining a Stripe PaymentMethod token. This token is then sent to your backend, which registers the user with Microz.
Important
You must use Microz's Stripe publishable keywhen initializing Stripe.js — not your own. Cards are tokenized under Microz's Stripe account.
1. Load Stripe.js
npm install @stripe/stripe-js @stripe/react-stripe-js
2. Initialize with Microz's key
import { loadStripe } from '@stripe/stripe-js';
// IMPORTANT: This is Microz's publishable key, NOT your own Stripe key
export const stripePromise = loadStripe(
'pk_test_51TIeBSAcokt2OJG3M1aNpUjjSwRoTqpLreJGwK8aIzLcTKZYtkFESWUWl6PEMvnVSHS0nXu2cmLcCU7rLCYIQoLh00s7YfLz8l'
);3. Card collection form
import { useState } from 'react';
import {
Elements,
CardElement,
useStripe,
useElements,
} from '@stripe/react-stripe-js';
import { stripePromise } from './stripe';
function CardFormInner({ onToken }: { onToken: (pm: string) => void }) {
const stripe = useStripe();
const elements = useElements();
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) return;
setLoading(true);
setError(null);
const card = elements.getElement(CardElement);
if (!card) return;
const { error: stripeError, paymentMethod } =
await stripe.createPaymentMethod({
type: 'card',
card,
});
if (stripeError) {
setError(stripeError.message ?? 'Card error');
setLoading(false);
return;
}
// Send pm_xxx to your backend
onToken(paymentMethod.id);
setLoading(false);
};
return (
<form onSubmit={handleSubmit}>
<CardElement
options={{
style: {
base: {
fontSize: '16px',
color: '#ffffff',
'::placeholder': { color: '#6b7280' },
},
},
}}
/>
{error && <p style={{ color: '#ef4444' }}>{error}</p>}
<button type="submit" disabled={!stripe || loading}>
{loading ? 'Processing...' : 'Save Card'}
</button>
</form>
);
}
// Wrap with Elements provider
export function CardForm({ onToken }: { onToken: (pm: string) => void }) {
return (
<Elements stripe={stripePromise}>
<CardFormInner onToken={onToken} />
</Elements>
);
}4. Send the token to your backend
async function registerUser(email: string, paymentMethodId: string) {
const res = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, payment_method_token: paymentMethodId }),
});
if (!res.ok) throw new Error('Registration failed');
const { user_id } = await res.json();
// Store user_id in your session/database
return user_id;
}Backend Integration
Your backend is responsible for: registering users, recording charges on every billable action, checking balances, and handling webhooks. All requests require the X-API-Key header.
const MICROZ_BASE = 'https://api.microz.net';
const MICROZ_API_KEY = process.env.MICROZ_API_KEY; // mz_live_...
async function microz(method: string, path: string, body?: object) {
const res = await fetch(`${MICROZ_BASE}${path}`, {
method,
headers: {
'Content-Type': 'application/json',
'X-API-Key': MICROZ_API_KEY!,
},
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json();
if (!res.ok) throw new MicrozError(data);
return data;
}
class MicrozError extends Error {
code: string;
status: number;
account_status?: string;
constructor(data: any) {
super(data.error || 'Unknown error');
this.code = data.code;
this.status = data.status;
this.account_status = data.account_status;
}
}Step 1: Register a user
After your frontend collects a card and sends you the pm_xxx token, register the user with Microz. The returned user_id is a UUID that persists across all your apps.
// In your /api/register endpoint:
const result = await microz('POST', '/v1/users/register', {
email: 'user@example.com',
payment_method_token: 'pm_1abc2def3ghi...', // From Stripe.js
display_name: 'Jane Doe', // Optional
});
// Response:
// {
// "user_id": "550e8400-e29b-41d4-a716-446655440000",
// "account_status": "active",
// "policy_window_deadline": "2026-04-09T14:30:00Z"
// }
// Save the user_id in your database, associated with this user
await db.users.update({
where: { email: 'user@example.com' },
data: { microz_user_id: result.user_id },
});Step 2: Charge on every billable action
Every time a user performs a billable action, call POST /v1/charge. This is a ledger write — no card network communication. It responds in under 50ms.
Tip
Always include an idempotency_key. If your request times out and you retry, the same key prevents double-charging. Keys are valid for 24 hours.
import { randomUUID } from 'crypto';
async function chargeForInference(userId: string, model: string) {
const idempotencyKey = `inf_${randomUUID()}`;
const result = await microz('POST', '/v1/charge', {
user_id: userId,
app_slug: 'my-ai-app',
units: 1,
description: `${model} inference call`,
idempotency_key: idempotencyKey,
});
// Response:
// {
// "status": "queued",
// "charge_id": "e8b7c6d5-...",
// "amount_microcents": 50000, // $0.005
// "balance_microcents": 2500000, // $2.50 accumulated
// "next_trigger": {
// "type": "threshold",
// "remaining_microcents": 2500000 // $2.50 until settlement
// },
// "idempotency_key": "inf_abc123...",
// "created_at": "2026-04-03T14:30:00Z"
// }
return result;
}Step 3: Check a user's balance
Use this to display balance information in your UI, or to check account status before allowing access.
const balance = await microz('GET', `/v1/users/${userId}/balance`);
// Response:
// {
// "balance_microcents": 2500000,
// "account_status": "active",
// "policy_window_deadline": "2026-04-09T14:30:00Z",
// "next_trigger": {
// "type": "threshold",
// "remaining_microcents": 2500000
// },
// "lifetime_charges": 15000000,
// "lifetime_settled": 12500000,
// "merchant_attributions": [
// {
// "merchant_id": "...",
// "name": "My AI App",
// "balance_microcents": 2500000
// }
// ]
// }
// Format for display
const balanceDollars = (balance.balance_microcents / 1_000_000).toFixed(2);
console.log(`Current balance: $${balanceDollars}`);Step 4: On-demand settlement
You can force an immediate settlement if needed — for example, when a user cancels their account or at the end of a session. Subject to the COGS floor evaluation.
const settlement = await microz(
'POST',
`/v1/settlement/${userId}/settle`
);
// Response:
// {
// "settlement_id": "f9a8b7c6-...",
// "status": "initiated"
// }
// The actual settlement result arrives via the
// settlement.completed or settlement.failed webhook.Step 5: Card update (reactivation)
When a user's card is declined and their account enters SUSPENDED_DECLINED state, they need to provide a new card. Collect a new pm_xxx token via Stripe.js and update their instrument.
const result = await microz(
'PUT',
`/v1/users/${userId}/instrument`,
{ payment_method_token: 'pm_new_token...' }
);
// Response:
// {
// "success": true,
// "account_status": "active",
// "policy_window_deadline": "2026-04-09T14:30:00Z"
// }
// Account is immediately reactivated.
// Outstanding balance will be settled on next trigger.Subscription + Overage Model
Microz supports a hybrid billing model where users get included credits from a subscription, and only pay micro-charges for overage beyond their quota. This is controlled by the quota_mode parameter on charge requests.
How it works
- Your app manages the subscription— You handle subscription billing (via Stripe, your own system, etc.) and track how many included units each user has.
- Decrement your own quota first— On each billable action, check if the user has remaining included units. If yes, decrement locally; no Microz call needed.
- Pass overage to Microz— When included units are exhausted, start calling
POST /v1/chargewithquota_mode: true.
async function handleBillableAction(userId: string) {
// 1. Check local subscription quota
const user = await db.users.findUnique({ where: { id: userId } });
if (user.remaining_quota > 0) {
// Still within subscription - decrement locally
await db.users.update({
where: { id: userId },
data: { remaining_quota: { decrement: 1 } },
});
return { billed: false, source: 'subscription' };
}
// 2. Quota exhausted - charge via Microz as overage
const charge = await microz('POST', '/v1/charge', {
user_id: user.microz_user_id,
app_slug: 'my-ai-app',
units: 1,
description: 'GPT-4 overage',
idempotency_key: `ovg_${randomUUID()}`,
quota_mode: true, // Marks this as an overage charge
});
return { billed: true, source: 'overage', charge };
}The quota_mode: trueflag tells Microz this is an overage charge. This distinction appears in settlement receipts, webhooks, and the merchant dashboard — so you and your users can differentiate between subscription usage and pay-per-use overage.
Webhooks
Configure a webhook URL in your Microz dashboard. Microz sends POST requests with JSON payloads signed with HMAC-SHA256. Always verify signatures before processing.
Signature verification
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
}
// In your webhook handler:
app.post('/webhooks/microz', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-microz-signature'] as string;
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, process.env.MICROZ_WEBHOOK_SECRET!)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
// Process event...
res.status(200).send('OK');
});Event types
settlement.completedSettlement captured successfully. Includes per-merchant itemized charge list.
Your action: Send user a receipt. Update your records. This is your confirmation that charges were collected.
{
"event": "settlement.completed",
"settlement_id": "f9a8b7c6-...",
"user_id": "550e8400-...",
"amount_microcents": 5000000,
"amount_display": "$5.00",
"card_last4": "4242",
"stripe_charge_id": "ch_3abc...",
"merchant_attributions": [
{
"merchant_id": "m_abc123",
"app_slug": "my-ai-app",
"amount_microcents": 3500000,
"charge_count": 70,
"payout_amount_microcents": 3150000
}
],
"settled_at": "2026-04-03T14:30:00Z"
}settlement.failedAll retry attempts exhausted. User account suspended. Balance preserved for future collection.
Your action: Restrict user access. Show card-update UI. The account is now in SUSPENDED_DECLINED state.
{
"event": "settlement.failed",
"user_id": "550e8400-...",
"amount_microcents": 5000000,
"failure_reason": "card_declined",
"decline_code": "insufficient_funds",
"retry_count": 3,
"account_status": "suspended_declined",
"failed_at": "2026-04-03T14:30:00Z"
}threshold.approachingUser balance reached 80% of settlement threshold.
Your action: Use for engagement triggers, usage notifications, or pre-settlement UI cues.
{
"event": "threshold.approaching",
"user_id": "550e8400-...",
"balance_microcents": 4000000,
"threshold_microcents": 5000000,
"percentage": 80
}account.suspended_low_balanceSettlement trigger fired but balance is below the COGS floor. Account suspended until user returns and generates enough charges.
Your action: Restrict access. The user owes money but the balance is too low to profitably charge. When they return and accumulate more charges, the account auto-clears.
{
"event": "account.suspended_low_balance",
"user_id": "550e8400-...",
"balance_microcents": 120000,
"floor_microcents": 350000,
"account_status": "suspended_low_balance",
"suspended_at": "2026-04-03T14:30:00Z"
}account.suspended_declinedCard declined after maximum retry attempts. User must update payment method.
Your action: Block access. Show the card-update form (Stripe.js with Microz key). Call PUT /v1/users/:userId/instrument with the new pm_xxx.
{
"event": "account.suspended_declined",
"user_id": "550e8400-...",
"balance_microcents": 5000000,
"decline_code": "expired_card",
"account_status": "suspended_declined",
"suspended_at": "2026-04-03T14:30:00Z"
}account.clearedPreviously suspended user returned and outstanding balance was successfully collected.
Your action: Restore full access. The user is back in good standing.
{
"event": "account.cleared",
"user_id": "550e8400-...",
"settlement_id": "f9a8b7c6-...",
"amount_microcents": 5200000,
"previous_status": "suspended_low_balance",
"account_status": "active",
"cleared_at": "2026-04-03T14:30:00Z"
}chargeback.receivedDispute filed. User permanently banned. Card fingerprint banned across all merchants.
Your action: Immediately revoke all access. Log the incident. The user and their card are permanently blocked from the Microz network.
{
"event": "chargeback.received",
"user_id": "550e8400-...",
"dispute_id": "dp_3abc...",
"amount_microcents": 5000000,
"reason": "fraudulent",
"card_fingerprint": "fp_abc123...",
"merchant_attributions": [
{
"merchant_id": "m_abc123",
"app_slug": "my-ai-app",
"amount_microcents": 3500000
}
],
"banned_at": "2026-04-03T14:30:00Z"
}Account States
Every Microz user has an account_status that determines what actions are allowed. Understanding the state machine is critical for building a good user experience.
UNREGISTERED
|
| POST /v1/users/register
v
ACTIVE <------------------------------------------+
| |
| (balance >= threshold OR Day 6) |
v |
SETTLING |
| |
+--- success -----> ACTIVE (balance reset) ------+
|
+--- card declined (after retries) ---> SUSPENDED_DECLINED
| |
| | PUT /instrument
| | (new card)
| v
| ACTIVE
|
+--- below COGS floor ---> SUSPENDED_LOW_BALANCE
|
| User returns, charges accumulate,
| balance exceeds floor
v
ACTIVE_CLEARING
|
| Settlement succeeds
v
ACTIVE
PERMANENTLY_BANNED (chargeback - no recovery)
State descriptions
ACTIVENormal operating state. Charges are accepted.
SETTLINGSettlement in progress. Charges are queued until settlement completes.
SUSPENDED_DECLINEDCard declined after max retries. User must update payment method via PUT /v1/users/:userId/instrument.
SUSPENDED_LOW_BALANCEPolicy window expired but balance is below COGS floor. Charges blocked until user returns.
ACTIVE_CLEARINGPreviously suspended user returned. Charges are accepted while the system waits for balance to exceed the floor for settlement.
PERMANENTLY_BANNEDChargeback received. User and card fingerprint banned across entire Microz network. No recovery.
Handling states in your app
async function canUserProceed(userId: string): Promise<boolean> {
const { account_status } = await microz(
'GET',
`/v1/users/${userId}/balance`
);
switch (account_status) {
case 'active':
case 'active_clearing':
return true;
case 'suspended_declined':
// Redirect to card update page
throw new CardUpdateRequiredError();
case 'suspended_low_balance':
// The charge call itself will transition them to active_clearing
// But you may want to show a notice
return false;
case 'permanently_banned':
throw new AccountBannedError();
default:
return false;
}
}Dynamic Floor Calculator
Before Microz charges a card, it verifies that the settlement will be profitable after accounting for all costs. This prevents money-losing settlements.
The formula
floor = processing_fees + merchant_cogs + protection_buffer Where: processing_fees = Stripe fee (2.9% + $0.30 per charge) merchant_cogs = Your declared COGS for all units in this settlement protection_buffer = 10% of (processing_fees + merchant_cogs) Example: Settlement amount: $2.00 (2,000,000 microcents) Stripe fee: $2.00 * 2.9% + $0.30 = $0.358 Merchant COGS: 70 units * $0.003/unit = $0.21 Protection buffer: ($0.358 + $0.21) * 10% = $0.057 Floor = $0.358 + $0.21 + $0.057 = $0.625 Since $2.00 > $0.625, settlement proceeds.
When the floor is not met
If the balance is below the floor when a settlement trigger fires (typically the Day 6 policy window), the account enters SUSPENDED_LOW_BALANCE. The balance is preserved — not written off. When the user returns and accumulates enough charges to exceed the floor, the account transitions to ACTIVE_CLEARING and settlement proceeds automatically.
Tip
Set your COGS accurately. Declaring $0 COGS reduces the floor, meaning smaller balances can settle — but if your actual costs are higher, you may lose money on the merchant side. Microz protects against card processing losses; you must protect against your own cost structure.
Chargeback Policy
Microz has a zero-tolerance chargeback policy to protect the network.
What happens on a chargeback
- User permanently banned— The user's account is immediately set to
PERMANENTLY_BANNED. No recovery path. - Card fingerprint banned— The card's Stripe fingerprint is blocklisted across the entire Microz network. The same card cannot be used to register any new account, on any merchant.
- Webhook notification — You receive a
chargeback.receivedevent with per-merchant breakdown of the disputed amount. - Dispute response— Microz handles the Stripe dispute process automatically. Itemized micro-charge logs serve as evidence.
Important
As a merchant, you should revoke all access immediately upon receiving a chargeback.received webhook. Do not wait for dispute resolution.
Security
API key management
- Never expose API keys client-side. Your
mz_live_key should only exist on your backend, in environment variables. - Use
mz_test_keys for development and staging. Test keys operate against a sandbox ledger with no real card charges. - Rotate keys from your dashboard if compromised. Old keys are invalidated immediately.
Idempotency
Every POST /v1/charge should include an idempotency_key. If Microz receives the same key within 24 hours, it returns the original response without modifying the ledger. This makes retries safe.
// Good: Unique per logical action
const key = `user_${userId}_action_${actionId}`;
// Good: UUID per request (safe for retries of same request)
const key = `req_${randomUUID()}`;
// Bad: Reusing keys across different logical actions
const key = 'same-key-every-time'; // DON'T DO THISRate limits
| Endpoint | Limit | Window |
|---|---|---|
POST /v1/charge | 1,000 req/s | Per API key |
POST /v1/users/register | 100 req/min | Per API key |
GET /v1/users/:id/balance | 500 req/min | Per API key |
POST /v1/settlement/:id/settle | 10 req/min | Per user |
Rate-limited responses return 429 Too Many Requests with a Retry-After header in seconds.
PCI compliance
Because card data is collected via Stripe.js and tokenized under Microz's Stripe account, your application never handles raw card numbers. You remain at the lowest PCI compliance level (SAQ-A). Microz handles the rest as a PCI Level 1 compliant service provider.
Full Working Example
A complete Node.js/Express integration showing the entire flow from card collection to charging to webhook handling.
import express from 'express';
import crypto from 'crypto';
const app = express();
// ── Config ──────────────────────────────────────────────────────
const MICROZ_BASE = 'https://api.microz.net';
const MICROZ_API_KEY = process.env.MICROZ_API_KEY!; // mz_live_...
const WEBHOOK_SECRET = process.env.MICROZ_WEBHOOK_SECRET!;
// ── Microz client ──────────────────────────────────────────────
async function microz(method: string, path: string, body?: object) {
const res = await fetch(`${MICROZ_BASE}${path}`, {
method,
headers: {
'Content-Type': 'application/json',
'X-API-Key': MICROZ_API_KEY,
},
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json();
if (!res.ok) {
const err = new Error(data.error) as any;
err.code = data.code;
err.status = res.status;
err.account_status = data.account_status;
throw err;
}
return data;
}
// ── 1. Register user ───────────────────────────────────────────
app.post('/api/register', express.json(), async (req, res) => {
try {
const { email, payment_method_token, display_name } = req.body;
// Register with Microz
const result = await microz('POST', '/v1/users/register', {
email,
payment_method_token,
display_name,
});
// Save microz_user_id in your database
// await db.users.create({ email, microz_user_id: result.user_id });
res.json({ user_id: result.user_id });
} catch (err: any) {
res.status(err.status || 500).json({ error: err.message });
}
});
// ── 2. AI inference endpoint (charges per call) ────────────────
app.post('/api/ai/generate', express.json(), async (req, res) => {
try {
const { microz_user_id, prompt } = req.body;
const idempotencyKey = `gen_${crypto.randomUUID()}`;
// Pre-flight: check account status (optional, for better UX)
const balance = await microz(
'GET',
`/v1/users/${microz_user_id}/balance`
);
if (balance.account_status === 'suspended_declined') {
return res.status(402).json({
error: 'Payment method declined. Please update your card.',
update_card: true,
});
}
if (balance.account_status === 'permanently_banned') {
return res.status(403).json({ error: 'Account banned.' });
}
// ── Do the actual AI work ──
const aiResult = await callOpenAI(prompt);
// ── Charge for it ──
const charge = await microz('POST', '/v1/charge', {
user_id: microz_user_id,
app_slug: 'my-ai-app',
units: 1,
description: 'GPT-4 inference',
idempotency_key: idempotencyKey,
});
res.json({
result: aiResult,
usage: {
charge_id: charge.charge_id,
amount: '$' + (charge.amount_microcents / 1_000_000).toFixed(4),
balance: '$' + (charge.balance_microcents / 1_000_000).toFixed(2),
},
});
} catch (err: any) {
if (err.code === 'ACCOUNT_SUSPENDED') {
return res.status(402).json({
error: 'Account suspended',
account_status: err.account_status,
});
}
res.status(500).json({ error: err.message });
}
});
// ── 3. Card update endpoint ────────────────────────────────────
app.put('/api/update-card', express.json(), async (req, res) => {
try {
const { microz_user_id, payment_method_token } = req.body;
const result = await microz(
'PUT',
`/v1/users/${microz_user_id}/instrument`,
{ payment_method_token }
);
res.json({
success: true,
account_status: result.account_status,
});
} catch (err: any) {
res.status(err.status || 500).json({ error: err.message });
}
});
// ── 4. Webhook handler ─────────────────────────────────────────
app.post(
'/webhooks/microz',
express.raw({ type: 'application/json' }),
async (req, res) => {
// Verify signature
const signature = req.headers['x-microz-signature'] as string;
const payload = req.body.toString();
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload, 'utf8')
.digest('hex');
if (
!crypto.timingSafeEqual(
Buffer.from(signature || '', 'hex'),
Buffer.from(expected, 'hex')
)
) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
switch (event.event) {
case 'settlement.completed':
console.log(`Settlement ${event.settlement_id}: $${
(event.amount_microcents / 1_000_000).toFixed(2)
} collected from user ${event.user_id}`);
// Send receipt email, update records
break;
case 'settlement.failed':
console.log(`Settlement failed for user ${event.user_id}: ${event.failure_reason}`);
// Flag user, show card-update prompt on next visit
// await db.users.update({
// where: { microz_user_id: event.user_id },
// data: { needs_card_update: true },
// });
break;
case 'account.suspended_low_balance':
console.log(`User ${event.user_id} suspended (low balance)`);
// Restrict access until they return
break;
case 'account.suspended_declined':
console.log(`User ${event.user_id} card declined`);
// Force card update on next login
break;
case 'account.cleared':
console.log(`User ${event.user_id} cleared, back to active`);
// Restore full access
break;
case 'chargeback.received':
console.log(`CHARGEBACK: User ${event.user_id} permanently banned`);
// Immediately revoke all access
// await db.users.update({
// where: { microz_user_id: event.user_id },
// data: { banned: true, banned_reason: 'chargeback' },
// });
break;
case 'threshold.approaching':
console.log(`User ${event.user_id} at ${event.percentage}% of threshold`);
// Optional: send usage notification
break;
}
res.status(200).send('OK');
}
);
// ── Placeholder for your AI call ───────────────────────────────
async function callOpenAI(prompt: string) {
// Your actual OpenAI/Anthropic/etc. integration
return { text: 'AI response here' };
}
app.listen(3000, () => console.log('Server running on :3000'));Frontend companion (React)
import { useState, useEffect } from 'react';
import { CardForm } from './CardForm'; // From the Frontend Integration section
function App() {
const [userId, setUserId] = useState<string | null>(
localStorage.getItem('microz_user_id')
);
const [needsCardUpdate, setNeedsCardUpdate] = useState(false);
// ── Registration flow ──
async function handleCardToken(pmToken: string) {
const res = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'user@example.com',
payment_method_token: pmToken,
}),
});
const { user_id } = await res.json();
localStorage.setItem('microz_user_id', user_id);
setUserId(user_id);
}
// ── Card update flow ──
async function handleCardUpdate(pmToken: string) {
await fetch('/api/update-card', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
microz_user_id: userId,
payment_method_token: pmToken,
}),
});
setNeedsCardUpdate(false);
}
// ── AI generation ──
async function generate(prompt: string) {
const res = await fetch('/api/ai/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
microz_user_id: userId,
prompt,
}),
});
if (res.status === 402) {
const data = await res.json();
if (data.update_card) setNeedsCardUpdate(true);
return null;
}
return res.json();
}
// ── Render ──
if (!userId) {
return (
<div>
<h2>Enter your card to get started</h2>
<CardForm onToken={handleCardToken} />
</div>
);
}
if (needsCardUpdate) {
return (
<div>
<h2>Please update your card</h2>
<p>Your card was declined. Enter a new card to continue.</p>
<CardForm onToken={handleCardUpdate} />
</div>
);
}
return (
<div>
<h2>AI Generator</h2>
<button onClick={() => generate('Write me a poem')}>
Generate ($0.005/call)
</button>
</div>
);
}API Reference
Base URL: https://api.microz.net. All requests require the X-API-Key header. All request/response bodies are JSON.
/v1/users/registerRegister a new end-user with payment method. Returns a Microz user_id that persists across all your apps.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| string | Yes | User's email address | |
| payment_method_token | string | Yes | Stripe PaymentMethod ID (pm_xxx) from Stripe.js |
| display_name | string | No | User's display name |
Request Body
{
"email": "user@example.com",
"payment_method_token": "pm_1abc2def3ghi...",
"display_name": "Jane Doe"
}Response
{
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"account_status": "active",
"policy_window_deadline": "2026-04-09T14:30:00Z"
}/v1/chargeRecord a micro-charge against a user's balance. Ledger-only operation with no card network communication. Responds in < 50ms.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| user_id | string (UUID) | Yes | Microz user ID |
| app_slug | string | Yes | Your app's slug |
| units | integer | Yes | Number of billable units |
| description | string | Yes | Human-readable charge description |
| idempotency_key | string | Yes | Unique key for deduplication (24h TTL) |
| quota_mode | boolean | No | If true, marks this as a subscription overage charge |
Request Body
{
"user_id": "550e8400-...",
"app_slug": "my-ai-app",
"units": 1,
"description": "GPT-4 inference call",
"idempotency_key": "req_abc123",
"quota_mode": false
}Response
{
"status": "queued",
"charge_id": "e8b7c6d5-...",
"amount_microcents": 50000,
"balance_microcents": 2500000,
"next_trigger": {
"type": "threshold",
"remaining_microcents": 2500000
},
"idempotency_key": "req_abc123",
"created_at": "2026-04-03T14:30:00Z"
}/v1/users/:userId/balanceGet a user's current accumulated balance, account status, and next settlement trigger.
Response
{
"balance_microcents": 2500000,
"account_status": "active",
"policy_window_deadline": "2026-04-09T14:30:00Z",
"next_trigger": {
"type": "threshold",
"remaining_microcents": 2500000
},
"lifetime_charges": 15000000,
"lifetime_settled": 12500000,
"merchant_attributions": [
{
"merchant_id": "m_abc123",
"name": "My AI App",
"balance_microcents": 2500000
}
]
}/v1/settlement/:userId/settleForce immediate settlement of a user's balance. Subject to COGS floor evaluation. Use at session end, account cancellation, or checkout.
Response
{
"settlement_id": "f9a8b7c6-...",
"status": "initiated"
}/v1/users/:userId/instrumentUpdate payment method for a suspended account. Immediately reactivates the account. Collect the new pm_xxx via Stripe.js using Microz's publishable key.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| payment_method_token | string | Yes | New Stripe PaymentMethod ID (pm_xxx) |
Request Body
{
"payment_method_token": "pm_new_token..."
}Response
{
"success": true,
"account_status": "active",
"policy_window_deadline": "2026-04-09T14:30:00Z"
}/v1/appsCreate a new app with pricing and COGS configuration.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | App display name |
| slug | string | Yes | URL-safe identifier used in charge requests |
| cogs_per_unit_microcents | integer | Yes | Your cost per billable unit in microcents |
| price_per_unit_microcents | integer | Yes | Price charged to user per unit in microcents |
| description | string | No | App description |
Request Body
{
"name": "My AI App",
"slug": "my-ai-app",
"cogs_per_unit_microcents": 30000,
"price_per_unit_microcents": 50000,
"description": "GPT-4 powered writing assistant"
}Response
{
"app_id": "app_abc123...",
"slug": "my-ai-app",
"name": "My AI App",
"cogs_per_unit_microcents": 30000,
"price_per_unit_microcents": 50000,
"created_at": "2026-04-03T14:30:00Z"
}Error Codes
All error responses follow a consistent format:
{
"error": "Human-readable error message",
"code": "MACHINE_READABLE_CODE",
"status": 400,
"account_status": "suspended_declined" // Present when relevant
}| HTTP | Code | Description | Action |
|---|---|---|---|
| 400 | INVALID_REQUEST | Missing or malformed request parameters. | Check your request body against the API reference. |
| 400 | INVALID_APP_SLUG | The app_slug doesn't match any registered app. | Verify the slug in your Microz dashboard. |
| 400 | INVALID_PAYMENT_METHOD | The pm_xxx token is invalid or expired. | Collect a new card token via Stripe.js. |
| 401 | UNAUTHORIZED | Missing or invalid API key. | Check your X-API-Key header. Ensure you're using the correct environment key. |
| 402 | ACCOUNT_SUSPENDED | User account is suspended. Charges not accepted. | Check account_status field. Show card-update UI for SUSPENDED_DECLINED, or wait for SUSPENDED_LOW_BALANCE. |
| 403 | ACCOUNT_BANNED | User is permanently banned (chargeback). | Revoke access. No recovery possible. |
| 404 | USER_NOT_FOUND | No user with this ID exists. | Verify the user_id. The user may need to register first. |
| 409 | DUPLICATE_EMAIL | A user with this email already exists. | Use the existing user's ID, or have them log in. |
| 409 | IDEMPOTENCY_CONFLICT | Same idempotency_key used with different parameters. | Use a unique key per unique charge. Don't reuse keys across different actions. |
| 422 | SETTLEMENT_BELOW_FLOOR | Balance too low for profitable settlement. | Wait for more charges to accumulate, or adjust your COGS. |
| 429 | RATE_LIMITED | Too many requests. | Back off and retry after the Retry-After header duration. |
| 500 | INTERNAL_ERROR | Unexpected server error. | Retry with exponential backoff. Contact support if persistent. |
| 503 | SERVICE_UNAVAILABLE | Microz is temporarily unavailable. | Retry with exponential backoff. Check status.microz.net. |
FAQ
What happens if a charge request fails — does the user still get charged?
No. Charges are atomic. If the API returns an error, the ledger is not modified. Use idempotency keys so you can safely retry failed requests without risk of double-charging.
Can a user have charges from multiple merchants?
Yes. A Microz user_id is network-wide. If a user registers on your app and then uses another Microz-enabled app, their charges accumulate in a single ledger. Settlement collects all charges and distributes payouts to each merchant via Stripe Connect.
What does the user see on their credit card statement?
The statement descriptor is MICROZ.NET. Users see a single charge for the accumulated amount, not individual micro-charges.
What if a user never comes back and their balance is below the floor?
The account stays in SUSPENDED_LOW_BALANCE state. The balance is preserved indefinitely. If the user returns and generates enough charges to exceed the COGS floor, settlement proceeds automatically. There is no write-off or expiration.
How do I test without real card charges?
Use your mz_test_ API key. All operations work against a sandbox ledger. Use Stripe test card numbers (e.g., 4242 4242 4242 4242) for card tokenization.
Can I customize the settlement threshold or policy window?
Yes. Contact us to adjust the threshold (default $5.00) and policy window (default 6 days) for your merchant account. These can be set per-merchant but not per-user.
What's the minimum charge amount?
There is no minimum per-charge amount. You can charge as low as 1 microcent ($0.00000001). The COGS floor only applies at settlement time, not at charge time.
How fast is the charge endpoint?
The POST /v1/charge endpoint is a ledger write with no external API calls. P99 latency is under 50ms. It does not hit the card network.
Do I need my own Stripe account?
You need a Stripe Connect account to receive payouts. Card tokenization and charging happens under Microz's Stripe account. During onboarding, you'll connect your Stripe account for payouts.
What if Microz goes down — do I lose charges?
If the API is unavailable, your charge requests will fail with a 503. Use idempotency keys and retry with exponential backoff. No charges are lost — if we didn't return a success response, the charge wasn't recorded. Your retry will be the authoritative request.
Is Microz PCI compliant?
Microz is a PCI Level 1 compliant service provider. Because you collect cards via Stripe.js (client-side tokenization), your integration qualifies for SAQ-A — the simplest PCI self-assessment. You never handle raw card data.