CallPayMin V1 API — Complete Guide
Version: 1.0 | Base URL:
https://api.callpaymin.io/api/v1| Last updated: February 2026
CallPayMin is a developer platform for building paid consultation products. You get APIs for video/audio calls, chat messaging, meeting rooms, work sessions (screen-share with billing), job postings, expert management, and AI summaries — all with built-in per-minute and per-message billing, automatic revenue splits, and Stripe-powered payouts.
Table of Contents
- Quick Start
- Authentication
- Payment Modes
- Plans & Pricing
- Rate Limits
- API Reference
- Webhooks
- Common Workflows
- Error Reference
- SDK & Code Examples
Quick Start
Get a paid video call running in 5 minutes.
Step 1 — Get your API key
Sign up at callpaymin.io, create an organization, and grab your API key from the dashboard. Keys look like cpm_live_xxxx.
Step 2 — Create a billing user (the person who pays)
curl -X POST https://api.callpaymin.io/api/v1/billing/users \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "user-123",
"email": "alice@example.com",
"displayName": "Alice Johnson",
"initialBalance": 50.00
}'
Response:
{
"success": true,
"data": {
"id": "usr_abc123",
"externalId": "user-123",
"email": "alice@example.com",
"displayName": "Alice Johnson",
"balance": { "available": 50.00, "pending": 0, "currency": "USD" },
"status": "active"
}
}
Step 3 — Create an expert (the person who gets paid)
curl -X POST https://api.callpaymin.io/api/v1/experts \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "expert-456",
"email": "dr.smith@example.com",
"profile": {
"firstName": "David",
"lastName": "Smith",
"country": "US"
},
"rates": { "perMinute": 5.00 }
}'
Step 4 — Start a video call
curl -X POST https://api.callpaymin.io/api/v1/calls \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"participants": [
{ "externalId": "user-123", "displayName": "Alice Johnson", "role": "client" },
{ "externalId": "expert-456", "displayName": "Dr. Smith", "role": "expert" }
],
"billing": {
"payerId": "user-123",
"ratePerMinute": 5.00
},
"config": { "video": true, "audio": true }
}'
Response includes call.id and WebRTC connection details.
Step 5 — End the call (billing is automatic)
curl -X POST https://api.callpaymin.io/api/v1/calls/call_abc123/end \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "completed" }'
Response:
{
"success": true,
"data": {
"id": "call_abc123",
"status": "ended",
"duration": { "totalSeconds": 312, "billableSeconds": 312, "billableMinutes": 6 },
"cost": {
"total": 30.00,
"breakdown": {
"callMinutes": 30.00,
"platformFee": 1.05,
"expertEarnings": 23.16,
"businessShare": 5.79
}
}
}
}
That's it. Alice was charged $30 (6 min x $5/min). Dr. Smith earned $23.16. Your org earned $5.79. CallPayMin took $1.05.
Authentication
API Key Format
| Environment | Prefix | Example |
|---|---|---|
| Production | cpm_live_ | cpm_live_F1xdKZos0B9ajRQjZ7f8KZgHiepFa5b6 |
| Test | cpm_test_ | cpm_test_aBcDeFgHiJkLmNoPqRsTuVwX |
Sending Your Key
Use either of these headers (both work):
Authorization: Bearer cpm_live_YOUR_KEY
or:
X-API-Key: cpm_live_YOUR_KEY
IP Allowlist (Optional)
You can restrict API access to specific IPs or CIDR ranges in your org settings:
{
"ipAllowlist": ["203.0.113.0/24", "198.51.100.42"]
}
If configured, requests from non-listed IPs get 403 IP_NOT_ALLOWED.
API Scopes
Each API key has scopes that control what it can access:
| Scope | Read Access | Write Access |
|---|---|---|
calls:read / calls:write | List & get calls | Create, start, end calls |
chats:read / chats:write | List & get chats | Create chats, send messages |
rooms:read / rooms:write | List & get rooms | Create, join, close rooms |
billing:read / billing:write | View balances & transactions | Add payment method, charge cards |
experts:read / experts:write | List & get experts | Create, update, payouts, KYC |
jobs:read / jobs:write | List jobs & applications | Create jobs, review applications |
work_sessions:read / work_sessions:write | View sessions & files | Create sessions, upload files |
organization:read / organization:write | View org settings | Update settings, manage team |
summaries:read / summaries:write | View summaries | Generate summaries |
Auth Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | MISSING_API_KEY | No Authorization or X-API-Key header |
| 401 | INVALID_API_KEY_FORMAT | Key doesn't start with cpm_live_ or cpm_test_ |
| 401 | INVALID_API_KEY | Key not found (wrong key or deleted) |
| 401 | API_KEY_REVOKED | Key was revoked |
| 403 | IP_NOT_ALLOWED | Request IP not in allowlist |
| 403 | INSUFFICIENT_PERMISSIONS | Key doesn't have required scope |
Payment Modes
Your organization chooses one of two modes. This determines who handles customer payments.
Self-Managed Mode
You handle payments. You collect money from your customers however you want (Stripe, PayPal, crypto, invoices, etc.), then add funds to their CallPayMin balance via the API.
Your Customer → Pays You → You call POST /billing/users/{id}/add-funds → Balance updated
- You own the customer payment relationship
- CallPayMin charges no platform fees on transactions
- You handle expert payouts yourself
- Lower plan prices
- Best for: businesses with existing payment infrastructure
Managed Mode (Stripe)
CallPayMin handles payments. Users save a credit card via Stripe. CallPayMin auto-charges when balance is low and auto-pays experts via Stripe Connect.
Your Customer → Card on file → CallPayMin auto-charges → Balance updated
→ Expert auto-paid via Stripe Connect
- CallPayMin handles card charging, payouts, KYC
- Platform fees apply (3.5% per transaction)
- Automatic expert payouts to their bank account
- Best for: businesses that don't want to handle payments
Comparison
| Feature | Self-Managed | Managed |
|---|---|---|
| Who charges customers | You | CallPayMin (Stripe) |
| Who pays experts | You | CallPayMin (Stripe Connect) |
| Platform fee | None | 3.5% per transaction |
| Plan prices | Lower | Higher |
| Stripe account needed | No | Yes (auto-created) |
| Expert KYC | You handle | CallPayMin handles |
| Auto-billing | Webhook notification | Auto-charge card |
Revenue Split
When a $100 call happens, the money is split:
Self-Managed (no platform fees):
$100.00 Gross charge
├── $80.00 Expert share (80%) → You pay the expert
└── $20.00 Business share (20%) → Your revenue
Managed (with platform fees):
$100.00 Gross charge
├── Stripe processing fee: -$3.20 (2.9% + $0.30)
├── Platform fee: -$3.50 (3.5%, min $0.50)
├── Net for split: $93.30
│ ├── Expert (80%): $74.64
│ │ ├── Payout fee: -$0.75 (1%, min $0.25)
│ │ └── Expert gets: $73.45
│ └── Business (20%): $18.66
│ ├── Payout fee: -$0.25 (1%, min $0.25)
│ └── Business gets: $18.41
└── CallPayMin profit: $4.50
The default split is 80/20 (expert/business). You can customize this per-expert or per-organization.
Plans & Pricing
Self-Managed Plans
| Plan | Monthly | Call Minutes | Chat Messages | AI Summaries | Rate Limit | API Keys |
|---|---|---|---|---|---|---|
| Free | $0 | 100 | 500 | 10 | 60 req/min | 2 |
| Pay-As-You-Go | $0 | Pay per use | Pay per use | Pay per use | 120 req/min | 5 |
| Starter | $49 | 1,000 | 5,000 | 100 | 300 req/min | 10 |
| Professional | $149 | 2,500 | 12,000 | 250 | 500 req/min | 25 |
| Growth | $249 | 10,000 | 50,000 | 1,000 | 1,000 req/min | 50 |
| Enterprise | Custom | Unlimited | Unlimited | Unlimited | 5,000 req/min | Unlimited |
Managed Plans
| Plan | Monthly | Call Minutes | Chat Messages | AI Summaries | Rate Limit | API Keys |
|---|---|---|---|---|---|---|
| Free | $0 | 50 | 250 | 5 | 30 req/min | 2 |
| Pay-As-You-Go | $0 | Pay per use | Pay per use | Pay per use | 100 req/min | 5 |
| Starter | $99 | 1,000 | 5,000 | 100 | 250 req/min | 10 |
| Professional | $299 | 2,500 | 12,000 | 250 | 400 req/min | 25 |
| Growth | $499 | 10,000 | 50,000 | 1,000 | 800 req/min | 50 |
| Enterprise | Custom | Unlimited | Unlimited | Unlimited | 4,000 req/min | Unlimited |
Overage Rates (when you exceed plan limits)
| Resource | Self-Managed | Managed |
|---|---|---|
| Call minutes | $0.028/min | $0.045/min |
| Chat messages | $0.007/msg | $0.009/msg |
| AI summaries | $0.075/each | $0.095/each |
| Recording | $0.014/min | $0.019/min |
| Transcription | $0.038/min | $0.048/min |
| Room (per user) | $0.032/min | $0.050/min |
Feature Availability
| Feature | Free | PayG | Starter | Professional | Growth | Enterprise |
|---|---|---|---|---|---|---|
| Calls | Yes | Yes | Yes | Yes | Yes | Yes |
| Chats | Yes | Yes | Yes | Yes | Yes | Yes |
| Meeting Rooms | Yes (eval) | No | No | Yes | Yes | Yes |
| Jobs API | Yes (eval, 1 job) | No | No | Yes | Yes | Yes |
| Work Sessions | Yes (eval) | No | No | Yes | Yes | Yes |
| AI Summaries | Yes | Yes | Yes | Yes | Yes | Yes |
| Recording | Yes | Yes | Yes | Yes | Yes | Yes |
Rate Limits
Every response includes rate limit headers:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1708108800
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Max requests per minute for your plan |
X-RateLimit-Remaining | Requests left in this window |
X-RateLimit-Reset | Unix timestamp (seconds) when the window resets |
When you hit the limit, you get 429 Too Many Requests:
{
"error": "RATE_LIMITED",
"message": "Rate limit exceeded. Try again in 45 seconds.",
"limit": 60,
"remaining": 0,
"resetAt": 1708108800
}
Tip: Implement exponential backoff. Wait for resetAt, then retry.
API Reference
All endpoints return JSON in this envelope:
{
"success": true,
"data": { ... }
}
Errors return:
{
"error": "ERROR_CODE",
"message": "Human-readable description"
}
Health & Status
GET /health
Check if the API is running. No authentication required.
curl https://api.callpaymin.io/api/v1/health
{
"status": "ok",
"version": "1.0.0",
"timestamp": "2026-02-16T12:00:00.000Z"
}
GET /status
Detailed service health with subsystem checks. No authentication required.
curl https://api.callpaymin.io/api/v1/status
{
"status": "operational",
"version": "1.0.0",
"timestamp": "2026-02-16T12:00:00.000Z",
"responseTime": "45ms",
"services": [
{ "name": "Firestore Database", "status": "operational", "latency": "12ms" },
{ "name": "WebRTC Signaling", "status": "operational", "latency": "8ms" },
{ "name": "TURN Server", "status": "operational", "latency": "15ms" },
{ "name": "AI Services", "status": "operational", "latency": "200ms" },
{ "name": "Stripe Payments", "status": "operational", "latency": "100ms" }
],
"uptime": "99.99%"
}
Organization
GET /organization
Get your organization's settings and configuration.
Scope: organization:read
curl https://api.callpaymin.io/api/v1/organization \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
{
"success": true,
"data": {
"id": "org_abc123",
"name": "Acme Consulting",
"email": "admin@acme.com",
"plan": "professional",
"paymentMode": "self_managed",
"settings": {},
"status": "active",
"createdAt": "2026-01-15T10:00:00.000Z"
}
}
PUT /organization
Update organization settings.
Scope: organization:write
curl -X PUT https://api.callpaymin.io/api/v1/organization \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Consulting Inc.",
"settings": {
"defaultCurrency": "USD",
"timezone": "America/New_York"
}
}'
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Organization display name |
settings | object | No | Custom settings |
webhooks | object | No | Webhook URL configuration |
payments | object | No | Payment configuration |
GET /organization/plans
View all available plans with pricing for your payment mode.
Scope: organization:read
curl https://api.callpaymin.io/api/v1/organization/plans \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
{
"success": true,
"data": {
"paymentMode": "self_managed",
"plans": [
{
"id": "free",
"name": "Free",
"price": "$0/month",
"limits": { "callMinutes": 100, "chatMessages": 500, "aiSummaries": 10 },
"features": ["calls", "chats", "rooms_eval", "jobs_eval"],
"subscribable": false
},
{
"id": "professional",
"name": "Professional",
"price": "$149/month",
"limits": { "callMinutes": 2500, "chatMessages": 12000, "aiSummaries": 250 },
"features": ["calls", "chats", "rooms", "jobs", "work_sessions"],
"subscribable": true
}
]
}
}
GET /organization/usage
View current billing period usage and remaining quotas.
Scope: organization:read
curl https://api.callpaymin.io/api/v1/organization/usage \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
{
"success": true,
"data": {
"plan": "professional",
"paymentMode": "self_managed",
"billingPeriod": { "start": "2026-02-01", "end": "2026-02-28" },
"usage": {
"callMinutes": { "used": 450, "limit": 2500, "remaining": 2050, "percentUsed": 18 },
"chatMessages": { "used": 1200, "limit": 12000, "remaining": 10800, "percentUsed": 10 },
"aiSummaries": { "used": 25, "limit": 250, "remaining": 225, "percentUsed": 10 }
}
}
}
GET /organization/invoices
List past invoices or get the upcoming invoice.
Scope: organization:read
# Past invoices
curl "https://api.callpaymin.io/api/v1/organization/invoices?limit=10" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
# Upcoming invoice
curl "https://api.callpaymin.io/api/v1/organization/invoices?upcoming=true" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
GET /organization/subscription
Get current subscription details.
Scope: organization:write
curl https://api.callpaymin.io/api/v1/organization/subscription \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /organization/subscription
Subscribe to a plan.
Scope: organization:write
curl -X POST https://api.callpaymin.io/api/v1/organization/subscription \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "planId": "professional" }'
| Field | Type | Required | Description |
|---|---|---|---|
planId | string | Yes | One of: free, startup, professional, enterprise |
Errors:
400 INVALID_PLAN— Plan ID not recognized400 PLAN_NOT_SUBSCRIBABLE— Cannot subscribe to this plan (e.g., free)
DELETE /organization/subscription
Cancel current subscription.
Scope: organization:write
curl -X DELETE https://api.callpaymin.io/api/v1/organization/subscription \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "immediate": false }'
| Field | Type | Required | Description |
|---|---|---|---|
immediate | boolean | No | true = cancel now, false = cancel at period end (default) |
GET /organization/payment-methods
List org-level payment methods (for paying your CallPayMin subscription).
Scope: organization:write
curl https://api.callpaymin.io/api/v1/organization/payment-methods \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /organization/payment-methods
Attach a payment method to your organization.
Scope: organization:write
curl -X POST https://api.callpaymin.io/api/v1/organization/payment-methods \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"paymentMethodId": "pm_1234567890",
"setAsDefault": true
}'
DELETE /organization/payment-methods
Remove a payment method from your organization.
Scope: organization:write
curl -X DELETE https://api.callpaymin.io/api/v1/organization/payment-methods \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "paymentMethodId": "pm_1234567890" }'
POST /organization/setup-intent
Create a Stripe SetupIntent for collecting a card on your organization account.
Scope: organization:write
curl -X POST https://api.callpaymin.io/api/v1/organization/setup-intent \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
{
"success": true,
"data": {
"clientSecret": "seti_xxx_secret_yyy"
}
}
Use the clientSecret with Stripe.js on your frontend to collect card details.
POST /organization/upload-logo
Upload your organization's logo (displayed in dashboards and emails).
Scope: organization:write
curl -X POST https://api.callpaymin.io/api/v1/organization/upload-logo \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"imageData": "data:image/png;base64,iVBORw0KGgo...",
"fileName": "logo.png",
"contentType": "image/png"
}'
| Field | Type | Required | Description |
|---|---|---|---|
imageData | string | Yes | Base64-encoded image (with or without data URI prefix) |
fileName | string | No | File name |
contentType | string | No | MIME type: image/png, image/jpeg, image/svg+xml, image/webp |
Limits: Max 5MB. Allowed types: PNG, JPEG, SVG, WebP.
POST /organization/team/invite
Invite a team member to your organization.
Scope: organization:write
curl -X POST https://api.callpaymin.io/api/v1/organization/team/invite \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "teammate@example.com",
"role": "admin"
}'
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Email to invite |
role | string | No | Role for the invitee |
Invitation link expires in 7 days. An email is sent automatically.
Errors:
400 ALREADY_MEMBER— Email already belongs to a team member400 INVITE_EXISTS— Pending invitation already exists for this email
GET /organization/team/invite
List pending team invitations.
Scope: organization:write
curl https://api.callpaymin.io/api/v1/organization/team/invite \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
DELETE /organization/team/invite
Revoke a pending invitation.
Scope: organization:write
curl -X DELETE https://api.callpaymin.io/api/v1/organization/team/invite \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "inviteId": "inv_abc123" }'
GET /organization/webhooks
Get webhook configuration, recent deliveries, and available event types.
Scope: organization:write
curl https://api.callpaymin.io/api/v1/organization/webhooks \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
{
"success": true,
"data": {
"endpoints": {
"callStarted": "https://yourapp.com/webhooks/calls",
"callEnded": "https://yourapp.com/webhooks/calls",
"chatMessage": null,
"balanceLow": "https://yourapp.com/webhooks/billing",
"expertVerified": null
},
"secretPrefix": "whsec_EuPi...",
"availableEvents": [
{ "type": "call.created", "description": "When a new call is created" },
{ "type": "call.started", "description": "When a call becomes active" },
{ "type": "call.ended", "description": "When a call ends with billing details" },
{ "type": "chat.message", "description": "When a chat message is sent" },
{ "type": "balance.low", "description": "When a user's balance drops below threshold" }
]
}
}
PUT /organization/webhooks
Update webhook endpoint URLs. All URLs must be HTTPS.
Scope: organization:write
curl -X PUT https://api.callpaymin.io/api/v1/organization/webhooks \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"callStarted": "https://yourapp.com/webhooks/calls",
"callEnded": "https://yourapp.com/webhooks/calls",
"chatMessage": "https://yourapp.com/webhooks/chats",
"balanceLow": "https://yourapp.com/webhooks/billing",
"expertVerified": "https://yourapp.com/webhooks/experts"
}'
| Field | Type | Required | Description |
|---|---|---|---|
callStarted | string | No | URL for call.created, call.started, room.created, room.started events |
callEnded | string | No | URL for call.ended, chat.ended, summary.generated events |
chatMessage | string | No | URL for chat.message events |
balanceLow | string | No | URL for balance.low, balance.recharged events |
expertVerified | string | No | URL for expert.verified, expert.payout events |
Set a field to null to disable that webhook.
POST /organization/webhooks
Rotate your webhook signing secret. Old secret remains valid briefly for transition.
Scope: organization:write
curl -X POST https://api.callpaymin.io/api/v1/organization/webhooks \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
{
"success": true,
"data": {
"message": "Webhook secret rotated",
"secret": "whsec_newSecretHere...",
"note": "Update your webhook verification code with the new secret"
}
}
GET /organization/webhook-deliveries
View recent webhook delivery attempts and their status.
Scope: organization:read
curl "https://api.callpaymin.io/api/v1/organization/webhook-deliveries?limit=20" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
| Query Param | Type | Default | Description |
|---|---|---|---|
status | string | All | Filter: success, failed, pending |
eventType | string | All | Filter by event type |
limit | number | 20 | Max deliveries to return |
{
"success": true,
"data": {
"stats": { "total": 150, "success": 145, "failed": 3, "pending": 2 },
"deliveries": [
{
"id": "del_abc",
"eventId": "evt_xyz",
"url": "https://yourapp.com/webhooks/calls",
"status": "success",
"attempts": 1,
"lastAttemptAt": "2026-02-16T12:00:00Z",
"response": { "status": 200 },
"createdAt": "2026-02-16T12:00:00Z"
}
]
}
}
Stripe Integration Endpoints
These endpoints manage your Stripe Connected Account (for managed mode).
POST /organization/stripe/onboarding-link
Get a Stripe onboarding URL to complete your account setup.
Scope: organization:write
curl -X POST https://api.callpaymin.io/api/v1/organization/stripe/onboarding-link \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
{ "success": true, "data": { "url": "https://connect.stripe.com/setup/..." } }
POST /organization/stripe/dashboard-link
Get a direct link to your Stripe Express Dashboard.
Scope: organization:read
curl -X POST https://api.callpaymin.io/api/v1/organization/stripe/dashboard-link \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /organization/stripe/sync-status
Sync your Stripe account verification status to CallPayMin.
Scope: organization:write
curl -X POST https://api.callpaymin.io/api/v1/organization/stripe/sync-status \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
{
"success": true,
"data": {
"status": "verified",
"account": {
"id": "acct_xxx",
"charges_enabled": true,
"payouts_enabled": true,
"details_submitted": true
}
}
}
POST /organization/stripe/balance
Get your Stripe account balance.
Scope: organization:read
curl -X POST https://api.callpaymin.io/api/v1/organization/stripe/balance \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /organization/stripe/switch-payment-mode
Switch between self-managed and managed payment modes.
Scope: organization:write
curl -X POST https://api.callpaymin.io/api/v1/organization/stripe/switch-payment-mode \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "newMode": "managed" }'
| Field | Type | Required | Description |
|---|---|---|---|
newMode | string | Yes | "self_managed" or "managed" |
Restriction: Only free plan organizations can switch modes.
GET /organization/stripe/debug-account
Debug view of your Stripe account (development use).
Scope: organization:read
curl https://api.callpaymin.io/api/v1/organization/stripe/debug-account \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /organization/stripe/activate-test
Activate test mode for your Stripe account (development only).
Scope: organization:write
curl -X POST https://api.callpaymin.io/api/v1/organization/stripe/activate-test \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
Billing / Users
Billing users represent the people who pay for calls, chats, and sessions. Each user has a balance that gets deducted during service usage.
POST /billing/users
Create a new billing user.
Scope: billing:read
curl -X POST https://api.callpaymin.io/api/v1/billing/users \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "user-123",
"email": "alice@example.com",
"displayName": "Alice Johnson",
"phone": "+1234567890",
"initialBalance": 50.00,
"currency": "USD",
"autoRecharge": {
"enabled": true,
"threshold": 5.00,
"amount": 25.00,
"webhookUrl": "https://yourapp.com/webhooks/recharge"
},
"metadata": {
"plan": "premium",
"internalUserId": "12345"
}
}'
| Field | Type | Required | Description |
|---|---|---|---|
externalId | string | Yes | Your unique ID for this user |
email | string | Yes | User's email |
displayName | string | Yes | Display name |
phone | string | No | Phone number |
initialBalance | number | No | Starting balance in USD (default: 0) |
currency | string | No | Currency code (default: "USD") |
autoRecharge.enabled | boolean | No | Enable auto-recharge (default: false) |
autoRecharge.threshold | number | No | Trigger recharge when balance drops below this (default: $5) |
autoRecharge.amount | number | No | Amount to add when recharging (default: $25) |
autoRecharge.webhookUrl | string | No | Self-managed: URL to notify when balance is low |
metadata | object | No | Custom key-value pairs |
{
"success": true,
"data": {
"id": "usr_mlpylwjl7frvh3",
"externalId": "user-123",
"email": "alice@example.com",
"displayName": "Alice Johnson",
"balance": { "available": 50.00, "pending": 0, "currency": "USD" },
"autoRecharge": { "enabled": true, "threshold": 5.00, "amount": 25.00 },
"stats": { "totalDeposited": 50.00, "totalSpent": 0, "callCount": 0, "chatCount": 0 },
"status": "active",
"createdAt": "2026-02-16T12:00:00.000Z"
}
}
GET /billing/users
List all billing users with optional filters.
Scope: billing:read
curl "https://api.callpaymin.io/api/v1/billing/users?status=active&balanceBelow=10&limit=20" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
| Query Param | Type | Default | Description |
|---|---|---|---|
status | string | All | Filter by status |
balanceBelow | number | — | Users with balance below this amount |
balanceAbove | number | — | Users with balance above this amount |
limit | number | 50 | Items per page (max 100) |
cursor | string | — | Pagination cursor from previous response |
{
"success": true,
"data": {
"data": [ { "id": "usr_abc", "externalId": "user-123", "balance": { "available": 50.00 }, ... } ],
"hasMore": true,
"cursor": "usr_xyz"
}
}
GET /billing/users/{userId}
Get a specific user's details and balance.
Scope: billing:read
curl https://api.callpaymin.io/api/v1/billing/users/usr_mlpylwjl7frvh3 \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /billing/users/{userId}/add-funds
Add payment method to a user's balance. This is how you top up users in self-managed mode.
Scope: billing:write
curl -X POST https://api.callpaymin.io/api/v1/billing/users/usr_abc123/add-funds \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 25.00,
"reference": "stripe_pi_xxx",
"description": "Manual top-up"
}'
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount to add (must be positive) |
currency | string | No | Currency (default: user's currency) |
reference | string | No | Your payment reference ID |
description | string | No | Note for the transaction |
Errors:
400 INVALID_AMOUNT— Amount must be positive404 USER_NOT_FOUND— User ID doesn't exist
POST /billing/users/{userId}/charge
Charge a user's saved credit card (managed mode only).
Scope: billing:write
curl -X POST https://api.callpaymin.io/api/v1/billing/users/usr_abc123/charge \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 25.00,
"description": "Balance top-up",
"passStripeFeeToUser": false
}'
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount to charge (minimum $1.00) |
paymentMethodId | string | No | Specific card to charge (default: user's default card) |
description | string | No | Description for the charge |
passStripeFeeToUser | boolean | No | If true, user is charged extra to cover Stripe's 2.9%+$0.30 fee |
Important: Only works in managed mode. In self-managed mode, returns 400 CHARGE_NOT_AVAILABLE_SELF_MANAGED.
Errors:
400 CHARGE_NOT_AVAILABLE_SELF_MANAGED— Switch to managed mode first400 NO_PAYMENT_METHOD— User has no saved card402 CARD_DECLINED— Card was declined
GET /billing/users/{userId}/transactions
View transaction history for a user.
Scope: billing:read
curl "https://api.callpaymin.io/api/v1/billing/users/usr_abc123/transactions?type=debit&limit=20" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
| Query Param | Type | Default | Description |
|---|---|---|---|
type | string | All | credit, debit, or refund |
category | string | All | deposit, call, chat, room, work_session |
limit | number | 50 | Items per page |
cursor | string | — | Pagination cursor |
GET /billing/users/{userId}/payment-methods
List saved payment methods for a user (managed mode).
Scope: billing:write
curl https://api.callpaymin.io/api/v1/billing/users/usr_abc123/payment-methods \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /billing/users/{userId}/payment-methods
Save a payment method to a user's account.
Scope: billing:write
curl -X POST https://api.callpaymin.io/api/v1/billing/users/usr_abc123/payment-methods \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"paymentMethodId": "pm_card_visa_xxx",
"setAsDefault": true
}'
DELETE /billing/users/{userId}/payment-methods/{paymentMethodId}
Remove a saved payment method.
Scope: billing:write
curl -X DELETE https://api.callpaymin.io/api/v1/billing/users/usr_abc123/payment-methods/pm_card_visa_xxx \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /billing/users/{userId}/setup-intent
Create a Stripe SetupIntent so the user can save a card via your frontend.
Scope: billing:write
curl -X POST https://api.callpaymin.io/api/v1/billing/users/usr_abc123/setup-intent \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
{
"success": true,
"data": {
"clientSecret": "seti_xxx_secret_yyy",
"usage": "off_session"
}
}
Use the clientSecret with Stripe.js confirmCardSetup() on your frontend.
Experts
Experts are the service providers who earn money during calls, chats, and work sessions.
POST /experts
Create a new expert profile.
Scope: experts:read
curl -X POST https://api.callpaymin.io/api/v1/experts \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "expert-456",
"email": "dr.smith@example.com",
"profile": {
"firstName": "David",
"lastName": "Smith",
"country": "US",
"bio": "Senior consultant with 10 years experience",
"skills": ["consulting", "strategy"]
},
"rates": { "perMinute": 5.00 },
"revenueShare": { "expert": 80, "platform": 20 },
"metadata": { "speciality": "business-strategy" }
}'
| Field | Type | Required | Description |
|---|---|---|---|
externalId | string | Yes | Your unique ID for this expert |
email | string | Yes | Expert's email |
profile.firstName | string | Yes | First name |
profile.lastName | string | Yes | Last name |
profile.country | string | Yes | Country code (e.g., "US") |
profile.bio | string | No | Short bio |
profile.skills | string[] | No | Skill tags |
rates.perMinute | number | Yes | Default rate per minute |
revenueShare.expert | number | No | Expert's percentage (default: 80) |
revenueShare.platform | number | No | Business's percentage (default: 20) |
metadata | object | No | Custom key-value pairs |
GET /experts
List all experts with optional filters.
Scope: experts:read
curl "https://api.callpaymin.io/api/v1/experts?status=active&limit=20" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
| Query Param | Type | Default | Description |
|---|---|---|---|
status | string | All | Filter by status |
limit | number | 50 | Items per page |
cursor | string | — | Pagination cursor |
GET /experts/{expertId}
Get a specific expert's profile and earnings.
Scope: experts:read
curl https://api.callpaymin.io/api/v1/experts/exp_abc123 \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
PUT /experts/{expertId}
Update an expert's profile, rates, or payout settings.
Scope: experts:read
curl -X PUT https://api.callpaymin.io/api/v1/experts/exp_abc123 \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"rates": { "perMinute": 7.50 },
"profile": { "bio": "Updated bio" }
}'
| Field | Type | Required | Description |
|---|---|---|---|
profile | object | No | Profile fields to update |
rates | object | No | Rate updates |
revenueShare | object | No | Revenue split updates |
payout | object | No | Payout settings |
metadata | object | No | Custom data |
GET /experts/{expertId}/earnings
Get an expert's earnings summary.
Scope: experts:read
curl https://api.callpaymin.io/api/v1/experts/exp_abc123/earnings \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
{
"success": true,
"data": {
"lifetime": { "gross": 5000.00, "net": 4500.00 },
"pending": 250.00,
"total": 4500.00,
"paidOut": 4250.00
}
}
GET /experts/{expertId}/payouts
List payout history.
Scope: experts:read
curl "https://api.callpaymin.io/api/v1/experts/exp_abc123/payouts?limit=10" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /experts/{expertId}/payouts
Request a manual payout to the expert.
Scope: experts:read
curl -X POST https://api.callpaymin.io/api/v1/experts/exp_abc123/payouts \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "amount": 100.00 }'
Errors:
400 PAYOUTS_NOT_ENABLED— Expert hasn't completed Stripe onboarding400 BELOW_MINIMUM_PAYOUT— Amount below minimum payout threshold
POST /experts/{expertId}/kyc-link
Generate a Stripe Connect onboarding link for the expert to verify their identity and set up payouts.
Scope: experts:write
curl -X POST https://api.callpaymin.io/api/v1/experts/exp_abc123/kyc-link \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
{
"success": true,
"data": {
"onboardingUrl": "https://connect.stripe.com/setup/e/...",
"status": "pending"
}
}
Jobs & Applications
Post jobs, receive applications from experts, review and negotiate.
Plan requirement: Professional, Growth, or Enterprise.
POST /jobs
Create a new job posting.
Scope: jobs:read | Feature: jobs
curl -X POST https://api.callpaymin.io/api/v1/jobs \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "React Developer for Dashboard Project",
"description": "Build an analytics dashboard using React and D3.js",
"skills": ["react", "d3.js", "typescript"],
"ratePerMinute": 2.50,
"requirements": {
"experience": "3+ years React",
"availability": "20 hours/week"
},
"metadata": { "projectId": "proj-123" }
}'
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Job title |
description | string | Yes | Job description |
skills | string[] | No | Required skills |
ratePerMinute | number | Yes | Pay rate per minute |
requirements | object | No | Additional requirements |
metadata | object | No | Custom data |
Errors:
403 JOB_LIMIT_REACHED— Plan's job limit exceeded
GET /jobs
List all jobs.
Scope: jobs:read | Feature: jobs
curl "https://api.callpaymin.io/api/v1/jobs?status=open&limit=20" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
GET /jobs/{jobId}
Get job details.
Scope: jobs:read | Feature: jobs
curl https://api.callpaymin.io/api/v1/jobs/job_abc123 \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
PUT /jobs/{jobId}
Update a job posting.
Scope: jobs:write | Feature: jobs
curl -X PUT https://api.callpaymin.io/api/v1/jobs/job_abc123 \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Updated Title",
"status": "closed"
}'
DELETE /jobs/{jobId}
Delete a job posting.
Scope: jobs:write | Feature: jobs
curl -X DELETE https://api.callpaymin.io/api/v1/jobs/job_abc123 \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
GET /jobs/{jobId}/applications
List applications for a job.
Scope: jobs:read | Feature: jobs
curl "https://api.callpaymin.io/api/v1/jobs/job_abc123/applications?status=pending&limit=20" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /jobs/{jobId}/applications/{appId}/review
Approve or reject an application.
Scope: jobs:write | Feature: jobs
curl -X POST https://api.callpaymin.io/api/v1/jobs/job_abc123/applications/app_xyz/review \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "approve",
"notes": "Great portfolio. Welcome aboard!"
}'
| Field | Type | Required | Description |
|---|---|---|---|
action | string | Yes | "approve" or "reject" |
notes | string | No | Notes for the applicant |
Errors:
400 ALREADY_REVIEWED— Application was already reviewed400 INVALID_ACTION— Must be "approve" or "reject"
POST /jobs/{jobId}/applications/{appId}/negotiate
Start rate negotiation with an applicant. Sends an email to the expert.
Scope: jobs:write | Feature: jobs
curl -X POST https://api.callpaymin.io/api/v1/jobs/job_abc123/applications/app_xyz/negotiate \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"negotiationNotes": "We love your profile! Would you consider $2.00/min instead?",
"counterOfferRate": 2.00
}'
| Field | Type | Required | Description |
|---|---|---|---|
negotiationNotes | string | Yes | Message to the applicant |
counterOfferRate | number | No | Counter-offer rate per minute |
Calls
Create and manage video/audio calls with automatic per-minute billing.
POST /calls
Create a new call session.
Scope: calls:read
curl -X POST https://api.callpaymin.io/api/v1/calls \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"participants": [
{ "externalId": "user-123", "displayName": "Alice", "role": "client" },
{ "externalId": "expert-456", "displayName": "Dr. Smith", "role": "expert" }
],
"billing": {
"payerId": "user-123",
"ratePerMinute": 5.00,
"currency": "USD",
"gracePeriodSeconds": 60
},
"config": {
"video": true,
"audio": true,
"aiSummaryEnabled": true
},
"metadata": { "topic": "legal-consultation" }
}'
| Field | Type | Required | Description |
|---|---|---|---|
participants | array | Yes | 2+ participants |
participants[].externalId | string | Yes | Your user ID |
participants[].displayName | string | Yes | Display name |
participants[].role | string | Yes | "client" or "expert" |
billing.payerId | string | Yes | externalId of the person who pays |
billing.ratePerMinute | number | Yes | Rate per minute in USD |
billing.currency | string | No | Currency (default: "USD") |
billing.gracePeriodSeconds | number | No | Free seconds before billing starts (default: 60) |
config.video | boolean | No | Enable video (default: true) |
config.audio | boolean | No | Enable audio (default: true) |
config.aiSummaryEnabled | boolean | No | Auto-generate AI summary when call ends |
metadata | object | No | Custom data |
Errors:
400 INVALID_PARTICIPANTS— Need at least 2 participants400 INVALID_BILLING— Missing payerId or ratePerMinute404 PAYER_NOT_FOUND— Payer externalId doesn't match any billing user402 INSUFFICIENT_BALANCE— Payer doesn't have enough balance for estimated 5 min
{
"success": true,
"data": {
"id": "call_mlpyn54umfnbu651",
"participants": [
{ "id": "part_abc", "externalId": "user-123", "displayName": "Alice", "role": "client", "connectionStatus": "connecting" },
{ "id": "part_def", "externalId": "expert-456", "displayName": "Dr. Smith", "role": "expert", "connectionStatus": "connecting" }
],
"billing": {
"payerId": "usr_abc",
"ratePerMinute": 5.00,
"currency": "USD",
"gracePeriodSeconds": 60
},
"webrtc": {
"roomId": "room_call_mlpyn54umfnbu651",
"signalingUrl": "wss://signaling.callpaymin.io"
},
"status": "pending",
"createdAt": "2026-02-16T12:00:00.000Z"
}
}
GET /calls
List calls with optional filters.
Scope: calls:read
curl "https://api.callpaymin.io/api/v1/calls?status=active&limit=20" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
| Query Param | Type | Default | Description |
|---|---|---|---|
status | string | All | pending, active, ended |
limit | number | 50 | Items per page |
cursor | string | — | Pagination cursor |
GET /calls/{callId}
Get call details including duration and cost.
Scope: calls:read
curl https://api.callpaymin.io/api/v1/calls/call_abc123 \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /calls/{callId}/start
Mark a call as active (starts the billing clock).
Scope: calls:write
curl -X POST https://api.callpaymin.io/api/v1/calls/call_abc123/start \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /calls/{callId}/end
End a call and finalize billing. Charges the payer and credits the expert automatically.
Scope: calls:write
curl -X POST https://api.callpaymin.io/api/v1/calls/call_abc123/end \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "completed" }'
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | No | "completed", "insufficient_funds", "participant_left", "error" (default: "completed") |
Billing details:
- Minimum billing: 1 minute (calls < 60s are billed as 1 minute)
- Calls under 5 seconds: no charge (marked as
call_too_short) - Duplicate prevention: deterministic billing ID prevents double-charging on retries
- Revenue split is calculated and applied automatically
{
"success": true,
"data": {
"id": "call_abc123",
"status": "ended",
"endReason": "completed",
"duration": {
"totalSeconds": 312,
"billableSeconds": 312,
"startedAt": "2026-02-16T12:00:00.000Z",
"endedAt": "2026-02-16T12:05:12.000Z"
},
"cost": {
"total": 30.00,
"breakdown": {
"callMinutes": 30.00,
"platformFee": 1.05,
"expertEarnings": 23.16,
"businessShare": 5.79
}
}
}
}
GET /calls/{callId}/credentials
Get WebRTC connection details for a participant.
Scope: calls:read
curl "https://api.callpaymin.io/api/v1/calls/call_abc123/credentials?externalId=user-123" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
| Query Param | Type | Required | Description |
|---|---|---|---|
participantId | string | One of these | Internal participant ID |
externalId | string | One of these | Your external user ID |
{
"success": true,
"data": {
"callId": "call_abc123",
"roomId": "room_call_abc123",
"signaling": {
"url": "wss://signaling.callpaymin.io",
"apiUrl": "https://signaling.callpaymin.io"
},
"ice": {
"urls": ["turn:turn.callpaymin.io:3478", "turns:turn.callpaymin.io:5349"],
"username": "callpaymin",
"credential": "xxx",
"credentialType": "password"
},
"config": { "video": true, "audio": true },
"participants": [
{ "id": "part_abc", "externalId": "user-123", "displayName": "Alice", "role": "client" },
{ "id": "part_def", "externalId": "expert-456", "displayName": "Dr. Smith", "role": "expert" }
]
}
}
Errors:
404 NOT_PARTICIPANT— The given ID doesn't match any participant400 CALL_ENDED— Cannot get credentials for an ended call
GET /calls/{callId}/recording
Get recording status and metadata.
Scope: calls:read
curl https://api.callpaymin.io/api/v1/calls/call_abc123/recording \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
Recording status: "none" | "recording" | "uploaded" | "processing"
PATCH /calls/{callId}/recording
Start or stop recording.
Scope: calls:write
# Start recording
curl -X PATCH https://api.callpaymin.io/api/v1/calls/call_abc123/recording \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "action": "start" }'
# Stop recording
curl -X PATCH https://api.callpaymin.io/api/v1/calls/call_abc123/recording \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "action": "stop" }'
POST /calls/{callId}/recording
Upload a recording file.
Scope: calls:write
curl -X POST https://api.callpaymin.io/api/v1/calls/call_abc123/recording \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"fileUrl": "https://storage.example.com/recording.webm",
"durationSeconds": 312,
"format": "webm",
"fileSize": 15000000
}'
| Field | Type | Required | Description |
|---|---|---|---|
fileUrl | string | Yes | URL of the recording file |
durationSeconds | number | Yes | Recording duration in seconds |
format | string | No | File format (e.g., "webm", "mp4") |
fileSize | number | No | File size in bytes |
GET /calls/{callId}/transcription
Get call transcription.
Scope: calls:read
curl https://api.callpaymin.io/api/v1/calls/call_abc123/transcription \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
Errors:
404 TRANSCRIPTION_NOT_FOUND— No transcription exists. Upload a recording first, then generate.
POST /calls/{callId}/transcription
Generate a transcription from the recording.
Scope: calls:write
curl -X POST https://api.callpaymin.io/api/v1/calls/call_abc123/transcription \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
Errors:
400 RECORDING_NOT_READY— Upload a recording first
Chats
Create text-based chat sessions with per-message billing.
POST /chats
Create a new chat session.
Scope: chats:read
curl -X POST https://api.callpaymin.io/api/v1/chats \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"participants": [
{ "externalId": "user-123", "displayName": "Alice", "role": "user" },
{ "externalId": "expert-456", "displayName": "Dr. Smith", "role": "expert" }
],
"billing": {
"payerId": "user-123",
"ratePerMinute": 5.00
},
"metadata": { "topic": "tax-advice" }
}'
| Field | Type | Required | Description |
|---|---|---|---|
participants | array | Yes | At least 1 expert required |
participants[].role | string | Yes | "user" or "expert" |
billing.payerId | string | Yes | externalId of the payer |
billing.ratePerMinute | number | No | Rate per minute (message cost = rate / 10) |
metadata | object | No | Custom data |
Chat billing: Each message costs ratePerMinute / 10. First 10 messages are free.
GET /chats
List chats.
Scope: chats:read
curl "https://api.callpaymin.io/api/v1/chats?status=active&limit=20" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
GET /chats/{chatId}
Get chat details.
Scope: chats:read
curl https://api.callpaymin.io/api/v1/chats/chat_abc123 \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /chats/{chatId}/messages
Send a message in a chat. Billing is automatic after the free tier (10 messages).
Scope: chats:read
curl -X POST https://api.callpaymin.io/api/v1/chats/chat_abc123/messages \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"senderExternalId": "user-123",
"content": "Can you help me with my tax return?",
"contentType": "text"
}'
| Field | Type | Required | Description |
|---|---|---|---|
senderExternalId | string | Yes | Sender's external ID |
content | string | Yes | Message text |
contentType | string | No | "text" (default) or "file" |
attachment | object | No | File attachment details |
Errors:
400 CHAT_NOT_ACTIVE— Chat has ended402 INSUFFICIENT_BALANCE— Payer doesn't have enough balance403 SENDER_NOT_PARTICIPANT— Sender not in this chat
GET /chats/{chatId}/messages
Get messages in a chat.
Scope: chats:read
curl "https://api.callpaymin.io/api/v1/chats/chat_abc123/messages?limit=50&order=asc" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
| Query Param | Type | Default | Description |
|---|---|---|---|
limit | number | 50 | Messages per page |
cursor | string | — | Pagination cursor |
order | string | "desc" | "asc" or "desc" |
POST /chats/{chatId}/end
End a chat session and finalize billing.
Scope: chats:write
curl -X POST https://api.callpaymin.io/api/v1/chats/chat_abc123/end \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
Meeting Rooms
Multi-participant meeting rooms with real-time billing per participant.
Plan requirement: Professional, Growth, or Enterprise (Free plan gets evaluation access).
POST /rooms
Create a new meeting room.
Scope: rooms:read | Feature: rooms
curl -X POST https://api.callpaymin.io/api/v1/rooms \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"expertExternalId": "expert-456",
"name": "Tax Consultation Room",
"description": "Weekly tax Q&A session",
"maxParticipants": 10,
"scheduledTime": "2026-02-20T14:00:00.000Z",
"scheduledEndTime": "2026-02-20T15:00:00.000Z",
"settings": {
"ratePerMinute": 2.00,
"video": true,
"audio": true
},
"metadata": { "recurring": true }
}'
| Field | Type | Required | Description |
|---|---|---|---|
expertExternalId | string | Yes | Expert who hosts the room |
name | string | Yes | Room name |
description | string | No | Room description |
maxParticipants | number | No | Max participants (default: no limit) |
scheduledTime | string | No | ISO 8601 start time (creates as "scheduled") |
scheduledEndTime | string | No | ISO 8601 end time |
meetingBaseUrl | string | No | Your frontend URL for the meeting |
settings | object | No | Room settings (rate, video, audio) |
metadata | object | No | Custom data |
Room states: scheduled → active → closed
If no scheduledTime, the room is created as active immediately.
GET /rooms
List rooms.
Scope: rooms:read | Feature: rooms
curl "https://api.callpaymin.io/api/v1/rooms?status=active&limit=20" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
| Query Param | Type | Default | Description |
|---|---|---|---|
status | string | All | scheduled, active, closed |
expertExternalId | string | — | Filter by expert |
limit | number | 50 | Items per page |
cursor | string | — | Pagination cursor |
GET /rooms/{roomId}
Get room details including participants.
Scope: rooms:read | Feature: rooms
curl https://api.callpaymin.io/api/v1/rooms/room_abc123 \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /rooms/{roomId}/start
Start a scheduled room (changes status from scheduled to active).
Scope: rooms:write | Feature: rooms
curl -X POST https://api.callpaymin.io/api/v1/rooms/room_abc123/start \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "expertExternalId": "expert-456" }'
| Field | Type | Required | Description |
|---|---|---|---|
expertExternalId | string | Yes | Must match the room's host expert |
Errors:
400 ROOM_NOT_SCHEDULED— Room is not in scheduled state403 NOT_ROOM_OWNER— You're not the room's expert
POST /rooms/{roomId}/join
Join an active room. Starts billing for the joining participant.
Scope: rooms:write | Feature: rooms
curl -X POST https://api.callpaymin.io/api/v1/rooms/room_abc123/join \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"participantExternalId": "user-123",
"participantName": "Alice Johnson"
}'
| Field | Type | Required | Description |
|---|---|---|---|
participantExternalId | string | Yes | Billing user's externalId |
participantName | string | No | Display name override |
Errors:
400 ROOM_NOT_ACTIVE— Room not yet started or already closed400 WAITING_FOR_HOST— Expert hasn't joined yet402 INSUFFICIENT_BALANCE— Not enough balance for minimum billing409 ROOM_FULL— Max participants reached
POST /rooms/{roomId}/heartbeat
Send a liveness ping (call every 30 seconds to prevent stale participant detection).
Scope: rooms:read | Feature: rooms
curl -X POST https://api.callpaymin.io/api/v1/rooms/room_abc123/heartbeat \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "participantExternalId": "user-123" }'
{ "success": true, "data": { "ok": true } }
If no heartbeat is received for 60 seconds (participants) or 45 seconds (expert), the participant is automatically removed.
POST /rooms/{roomId}/billing-increment
Process a 5-minute billing increment for a participant. Call this every 5 minutes per participant.
Scope: rooms:write | Feature: rooms
curl -X POST https://api.callpaymin.io/api/v1/rooms/room_abc123/billing-increment \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "participantExternalId": "user-123" }'
{
"success": true,
"data": {
"charged": 10.00,
"minutes": 5,
"balance": { "available": 40.00 },
"shouldDisconnect": false
}
}
If shouldDisconnect: true, the participant's balance is too low and they should be removed.
How room billing works:
- Your frontend calls
/billing-incrementevery 5 minutes per participant - The server uses a Firestore transaction to atomically:
- Re-read the room doc (checks session still active)
- Check participant balance
- Deduct from payer, credit expert
- Update room billing records
- If balance is insufficient, charges whatever remains (partial charge) and signals disconnect
- Auto-billing is triggered proactively after each increment
POST /rooms/{roomId}/leave
A participant leaves the room voluntarily.
Scope: rooms:write | Feature: rooms
curl -X POST https://api.callpaymin.io/api/v1/rooms/room_abc123/leave \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"participantExternalId": "user-123",
"reason": "done"
}'
When the expert leaves, all participant billing sessions are finalized automatically.
POST /rooms/{roomId}/remove-participant
Expert removes a participant from the room.
Scope: rooms:write | Feature: rooms
curl -X POST https://api.callpaymin.io/api/v1/rooms/room_abc123/remove-participant \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"expertExternalId": "expert-456",
"participantExternalId": "user-123"
}'
Only the room's expert can remove participants.
POST /rooms/{roomId}/close
Close a room. Finalizes billing for all remaining participants.
Scope: rooms:write | Feature: rooms
curl -X POST https://api.callpaymin.io/api/v1/rooms/room_abc123/close \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "expertExternalId": "expert-456" }'
Errors:
400 ROOM_ALREADY_CLOSED— Room is already closed403 NOT_ROOM_OWNER— Only the expert can close the room
DELETE /rooms/{roomId}
Delete a scheduled room (before it starts).
Scope: rooms:write | Feature: rooms
curl -X DELETE https://api.callpaymin.io/api/v1/rooms/room_abc123 \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "expertExternalId": "expert-456" }'
Errors:
400 ONLY_SCHEDULED_ROOMS— Can only delete rooms that haven't started yet
Work Sessions
Screen-sharing work sessions with 5-minute incremental billing, chat, screenshots, and file sharing.
Plan requirement: Professional, Growth, or Enterprise.
POST /work-sessions
Create a new work session.
Scope: work_sessions:read | Feature: work_sessions
curl -X POST https://api.callpaymin.io/api/v1/work-sessions \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"jobId": "job_abc123",
"questerId": "usr_client",
"workerId": "exp_expert",
"ratePerMinute": 2.50,
"metadata": { "task": "dashboard-build" }
}'
| Field | Type | Required | Description |
|---|---|---|---|
jobId | string | Yes | Associated job ID |
questerId | string | Yes | Billing user ID (who pays) |
workerId | string | Yes | Expert ID (who works) |
ratePerMinute | number | Yes | Work rate per minute |
metadata | object | No | Custom data |
Errors:
403 EXPERT_DAILY_LIMIT_REACHED— Expert has worked 480 minutes (8 hours) today404 PARTICIPANT_NOT_FOUND— Quester or worker ID not found
GET /work-sessions
List work sessions.
Scope: work_sessions:read | Feature: work_sessions
curl "https://api.callpaymin.io/api/v1/work-sessions?status=active&limit=20" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
| Query Param | Type | Default | Description |
|---|---|---|---|
status | string | All | active, completed, cancelled |
workerId | string | — | Filter by expert |
questerId | string | — | Filter by client |
jobId | string | — | Filter by job |
limit | number | 50 | Items per page |
cursor | string | — | Pagination cursor |
GET /work-sessions/{sessionId}
Get work session details.
Scope: work_sessions:read | Feature: work_sessions
curl "https://api.callpaymin.io/api/v1/work-sessions/ws_abc123?questerId=usr_client" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
The questerId query param is required for authorization.
POST /work-sessions/{sessionId}/end
End a work session. Bills the unbilled remainder (since last increment).
Scope: work_sessions:write | Feature: work_sessions
Note: The
questerIdquery parameter is required on all work session sub-endpoints for authorization.
curl -X POST "https://api.callpaymin.io/api/v1/work-sessions/ws_abc123/end?questerId=usr_client" \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "endReason": "completed" }'
Billing: Only charges for time since the last 5-minute increment. For example, if the last increment was at minute 42 and the session ends at minute 44, only 2 minutes are billed.
POST /work-sessions/{sessionId}/process-increment
Process a 5-minute billing increment. Call this every 5 minutes during the session.
Scope: work_sessions:write | Feature: work_sessions
curl -X POST "https://api.callpaymin.io/api/v1/work-sessions/ws_abc123/process-increment?questerId=usr_client" \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"questerId": "usr_client",
"videoCallActive": true
}'
| Field | Type | Required | Description |
|---|---|---|---|
questerId | string | Yes | Billing user ID |
videoCallActive | boolean | No | If true, adds $0.04/min video fee |
How work session billing works:
- Your frontend calls
/process-incrementevery 5 minutes - Server uses Firestore transaction to atomically deduct from quester and credit worker
- If video call is active, adds $0.04/min video call platform fee
- If balance is insufficient, charges whatever remains (partial charge)
- Auto-billing is triggered proactively after each increment
- When session ends,
/endonly bills the unbilled remainder
GET /work-sessions/{sessionId}/chat
Get chat messages in a work session.
Scope: work_sessions:read | Feature: work_sessions
curl "https://api.callpaymin.io/api/v1/work-sessions/ws_abc123/chat?questerId=usr_client&limit=50" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /work-sessions/{sessionId}/chat
Send a chat message in a work session.
Scope: work_sessions:read | Feature: work_sessions
curl -X POST "https://api.callpaymin.io/api/v1/work-sessions/ws_abc123/chat?questerId=usr_client" \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"senderId": "usr_client",
"senderRole": "quester",
"text": "Can you focus on the charts section?"
}'
| Field | Type | Required | Description |
|---|---|---|---|
senderId | string | Yes | Sender's user/expert ID |
senderRole | string | Yes | "quester" or "worker" |
text | string | Yes | Message text |
GET /work-sessions/{sessionId}/screenshots
Get screenshots captured during a work session.
Scope: work_sessions:read | Feature: work_sessions
curl "https://api.callpaymin.io/api/v1/work-sessions/ws_abc123/screenshots?questerId=usr_client" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /work-sessions/{sessionId}/screenshots
Upload a screenshot from the work session.
Scope: work_sessions:read | Feature: work_sessions
curl -X POST "https://api.callpaymin.io/api/v1/work-sessions/ws_abc123/screenshots?questerId=usr_client" \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"imageBase64": "iVBORw0KGgo...",
"width": 1920,
"height": 1080
}'
GET /work-sessions/{sessionId}/files
List files or download a specific file.
Scope: work_sessions:read, work_sessions:write | Feature: work_sessions
# List files
curl "https://api.callpaymin.io/api/v1/work-sessions/ws_abc123/files?questerId=usr_client" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
# Download specific file
curl "https://api.callpaymin.io/api/v1/work-sessions/ws_abc123/files?questerId=usr_client&fileKey=report.pdf" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /work-sessions/{sessionId}/files
Upload a file to a work session (stored in Cloudflare R2).
Scope: work_sessions:read, work_sessions:write | Feature: work_sessions
curl -X POST "https://api.callpaymin.io/api/v1/work-sessions/ws_abc123/files?questerId=usr_client" \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"fileName": "report.pdf",
"fileData": "JVBERi0xLjQ...",
"fileType": "application/pdf",
"senderId": "usr_client",
"senderRole": "quester"
}'
| Field | Type | Required | Description |
|---|---|---|---|
fileName | string | Yes | File name with extension |
fileData | string | Yes | Base64-encoded file content |
fileType | string | No | MIME type |
senderId | string | Yes | Uploader's ID |
senderRole | string | No | "quester" or "worker" |
Limits: Max 10MB. Allowed: code, docs, archives, images, config files.
GET /work-sessions/{sessionId}/review
Get review for a completed work session.
Scope: work_sessions:read, work_sessions:write | Feature: work_sessions
curl "https://api.callpaymin.io/api/v1/work-sessions/ws_abc123/review?questerId=usr_client" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /work-sessions/{sessionId}/review
Submit a review for a completed work session.
Scope: work_sessions:read, work_sessions:write | Feature: work_sessions
curl -X POST "https://api.callpaymin.io/api/v1/work-sessions/ws_abc123/review?questerId=usr_client" \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"rating": 5,
"comment": "Excellent work! Delivered exactly what I needed.",
"deliverableReceived": true,
"reviewerName": "Alice Johnson"
}'
| Field | Type | Required | Description |
|---|---|---|---|
rating | number | Yes | 1-5 stars |
comment | string | No | Review text |
deliverableReceived | boolean | No | Did you receive the deliverable? |
reviewerName | string | No | Reviewer's display name |
Errors:
400 SESSION_NOT_COMPLETED— Can only review completed sessions400 ALREADY_REVIEWED— Review already submitted
AI Summaries
Generate AI-powered summaries of calls and chats.
POST /summaries/calls/{callId}
Generate an AI summary for a completed call.
Scope: summaries:write
curl -X POST https://api.callpaymin.io/api/v1/summaries/calls/call_abc123 \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
Errors:
400 CALL_NOT_ENDED— Call must be ended before generating a summary404 CALL_NOT_FOUND— Call doesn't exist
{
"success": true,
"data": {
"id": "sum_abc",
"callId": "call_abc123",
"summary": "Alice consulted Dr. Smith about her tax situation...",
"keyPoints": ["Discussed Q4 deductions", "Recommended filing extension"],
"generatedAt": "2026-02-16T12:10:00.000Z"
}
}
GET /summaries/calls/{callId}
Get an existing call summary.
Scope: summaries:read
curl https://api.callpaymin.io/api/v1/summaries/calls/call_abc123 \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
POST /summaries/chats/{chatId}
Generate an AI summary for a chat.
Scope: summaries:write
curl -X POST https://api.callpaymin.io/api/v1/summaries/chats/chat_abc123 \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
GET /summaries/chats/{chatId}
Get an existing chat summary.
Scope: summaries:read
curl https://api.callpaymin.io/api/v1/summaries/chats/chat_abc123 \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
Webhooks
Webhooks let your server receive real-time notifications when events happen (call ended, message sent, balance low, etc.).
Setup
- Configure endpoints via the API:
curl -X PUT https://api.callpaymin.io/api/v1/organization/webhooks \
-H "Authorization: Bearer cpm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"callStarted": "https://yourapp.com/webhooks/calls",
"callEnded": "https://yourapp.com/webhooks/calls",
"chatMessage": "https://yourapp.com/webhooks/chats",
"balanceLow": "https://yourapp.com/webhooks/billing",
"expertVerified": "https://yourapp.com/webhooks/experts"
}'
- Get your signing secret — available in the webhook config response (
secretPrefix) or via the dashboard.
Event Types
| Event | Webhook Field | Description |
|---|---|---|
call.created | callStarted | New call created |
call.started | callStarted | Call became active |
call.ended | callEnded | Call ended with billing details |
chat.created | callStarted | New chat created |
chat.message | chatMessage | Message sent in a chat |
chat.ended | callEnded | Chat ended |
room.created | callStarted | New room created |
room.started | callStarted | Room became active |
room.participant_joined | callStarted | Someone joined a room |
room.participant_left | callEnded | Someone left a room |
room.billing_increment | callEnded | Room billing tick processed |
room.closed | callEnded | Room closed |
balance.low | balanceLow | User balance dropped below threshold |
balance.recharged | balanceLow | User balance auto-recharged |
expert.verified | expertVerified | Expert completed KYC |
expert.payout | expertVerified | Expert payout processed |
summary.generated | callEnded | AI summary ready |
transcription.completed | callEnded | Transcription ready |
Webhook Payload
Every webhook sends a POST request with this format:
Headers:
Content-Type: application/json
X-CallPayMin-Event: call.ended
X-CallPayMin-Delivery: del_abc123
X-CallPayMin-Signature: t=1708108800,v1=5257a869...
Body:
{
"id": "evt_abc123",
"type": "call.ended",
"data": {
"callId": "call_xyz",
"duration": { "totalSeconds": 312, "billableMinutes": 6 },
"cost": { "total": 30.00 },
"endReason": "completed"
},
"created": 1708108800
}
Signature Verification
Always verify webhook signatures to ensure the request came from CallPayMin.
How it works:
- Parse the
X-CallPayMin-Signatureheader:t=<timestamp>,v1=<signature> - Construct the signed string:
{timestamp}.{raw_json_body} - Compute HMAC-SHA256 using your webhook secret
- Compare (timing-safe) against the provided signature
- Check timestamp is within 5 minutes of current time
JavaScript / Node.js:
const crypto = require('crypto');
function verifyWebhook(rawBody, signatureHeader, webhookSecret) {
const parts = signatureHeader.split(',');
const timestamp = parts.find(p => p.startsWith('t=')).slice(2);
const signature = parts.find(p => p.startsWith('v1=')).slice(3);
// Check timestamp (reject if > 5 minutes old)
const age = Math.abs(Date.now() / 1000 - parseInt(timestamp));
if (age > 300) return false;
// Compute expected signature
const signedPayload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', webhookSecret)
.update(signedPayload)
.digest('hex');
// Timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
}
// Express.js example
app.post('/webhooks/calls', express.raw({ type: 'application/json' }), (req, res) => {
const isValid = verifyWebhook(
req.body.toString(),
req.headers['x-callpaymin-signature'],
'whsec_YOUR_SECRET'
);
if (!isValid) return res.status(401).send('Invalid signature');
const event = JSON.parse(req.body);
console.log('Event:', event.type, event.data);
res.status(200).json({ received: true });
});
Python:
import hmac
import hashlib
import time
def verify_webhook(raw_body: str, signature_header: str, webhook_secret: str) -> bool:
parts = dict(p.split('=', 1) for p in signature_header.split(','))
timestamp = parts.get('t', '')
signature = parts.get('v1', '')
# Check timestamp
age = abs(time.time() - int(timestamp))
if age > 300:
return False
# Compute expected signature
signed_payload = f"{timestamp}.{raw_body}"
expected = hmac.new(
webhook_secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
Go:
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"math"
"strconv"
"strings"
"time"
)
func verifyWebhook(rawBody, signatureHeader, webhookSecret string) bool {
parts := make(map[string]string)
for _, p := range strings.Split(signatureHeader, ",") {
kv := strings.SplitN(p, "=", 2)
if len(kv) == 2 {
parts[kv[0]] = kv[1]
}
}
timestamp := parts["t"]
signature := parts["v1"]
ts, _ := strconv.ParseInt(timestamp, 10, 64)
age := math.Abs(float64(time.Now().Unix() - ts))
if age > 300 {
return false
}
signedPayload := fmt.Sprintf("%s.%s", timestamp, rawBody)
mac := hmac.New(sha256.New, []byte(webhookSecret))
mac.Write([]byte(signedPayload))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}
PHP:
function verifyWebhook(string $rawBody, string $signatureHeader, string $webhookSecret): bool {
$parts = [];
foreach (explode(',', $signatureHeader) as $part) {
[$key, $value] = explode('=', $part, 2);
$parts[$key] = $value;
}
$timestamp = $parts['t'] ?? '';
$signature = $parts['v1'] ?? '';
if (abs(time() - (int)$timestamp) > 300) {
return false;
}
$signedPayload = "{$timestamp}.{$rawBody}";
$expected = hash_hmac('sha256', $signedPayload, $webhookSecret);
return hash_equals($expected, $signature);
}
Ruby:
require 'openssl'
def verify_webhook(raw_body, signature_header, webhook_secret)
parts = signature_header.split(',').map { |p| p.split('=', 2) }.to_h
timestamp = parts['t']
signature = parts['v1']
return false if (Time.now.to_i - timestamp.to_i).abs > 300
signed_payload = "#{timestamp}.#{raw_body}"
expected = OpenSSL::HMAC.hexdigest('sha256', webhook_secret, signed_payload)
ActiveSupport::SecurityUtils.secure_compare(signature, expected)
end
Retry Policy
| Attempt | Delay | Total time elapsed |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 1 minute | 1 min |
| 3 | 5 minutes | 6 min |
| 4 | 15 minutes | 21 min |
| 5 | 1 hour | 1 hr 21 min |
After 5 failed attempts, the delivery is marked as failed. Your webhook endpoint must return a 2xx status code within 10 seconds.
Monitoring
Check delivery status with:
curl "https://api.callpaymin.io/api/v1/organization/webhook-deliveries?status=failed&limit=10" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
Common Workflows
Workflow 1: Paid Video Call (Full Lifecycle)
1. Create billing user POST /billing/users
2. Create expert POST /experts
3. Add payment method to user POST /billing/users/{id}/add-funds
4. Create call POST /calls
5. Get WebRTC credentials GET /calls/{id}/credentials?externalId=xxx
6. Connect participants via WebRTC (your frontend)
7. Start call (begin billing) POST /calls/{id}/start
8. ... call in progress ...
9. End call (auto-bills) POST /calls/{id}/end
10. Generate AI summary POST /summaries/calls/{id}
11. Check user balance GET /billing/users/{id}
12. Check expert earnings GET /experts/{id}/earnings
Workflow 2: Chat Consultation
1. Create billing user POST /billing/users (skip if exists)
2. Create expert POST /experts (skip if exists)
3. Add payment method POST /billing/users/{id}/add-funds
4. Create chat POST /chats
5. Send messages POST /chats/{id}/messages (first 10 free, then billed)
6. ... chat in progress ...
7. End chat POST /chats/{id}/end
8. Generate summary POST /summaries/chats/{id}
Workflow 3: Meeting Room with Billing
1. Create expert POST /experts (skip if exists)
2. Create room POST /rooms
3. Expert joins / starts room POST /rooms/{id}/start (if scheduled)
4. Participants join POST /rooms/{id}/join (one per user)
5. Start heartbeat timer POST /rooms/{id}/heartbeat (every 30s per participant)
6. Start billing timer POST /rooms/{id}/billing-increment (every 5 min per participant)
7. ... meeting in progress ...
8. Participant leaves POST /rooms/{id}/leave
9. Expert closes room POST /rooms/{id}/close (finalizes all billing)
Workflow 4: Job Posting to Work Session
1. Create a job POST /jobs
2. Wait for applications GET /jobs/{id}/applications (poll or use webhooks)
3. Review application POST /jobs/{id}/applications/{appId}/review
4. (Optional) Negotiate rate POST /jobs/{id}/applications/{appId}/negotiate
5. Create work session POST /work-sessions
6. Start billing timer POST /work-sessions/{id}/process-increment (every 5 min)
7. Chat during work POST /work-sessions/{id}/chat
8. Upload files POST /work-sessions/{id}/files
9. Capture screenshots POST /work-sessions/{id}/screenshots
10. End session POST /work-sessions/{id}/end
11. Submit review POST /work-sessions/{id}/review
Workflow 5: Self-Managed Auto-Billing
When you create a billing user with auto-recharge and a webhook URL:
1. User's balance drops below threshold
2. CallPayMin sends webhook to your URL:
POST https://yourapp.com/webhooks/recharge
{
"type": "balance.low",
"data": {
"userId": "usr_abc",
"externalId": "user-123",
"currentBalance": 3.50,
"threshold": 5.00,
"suggestedAmount": 25.00
}
}
3. Your server charges the user's card (via your Stripe/payment provider)
4. Your server adds funds:
POST /billing/users/{id}/add-funds { "amount": 25.00 }
5. User continues their session uninterrupted
Workflow 6: Set Up Managed Payments (Stripe)
1. Switch to managed mode POST /organization/stripe/switch-payment-mode { "newMode": "managed" }
2. Complete Stripe onboarding POST /organization/stripe/onboarding-link (redirect user to URL)
3. Sync verification status POST /organization/stripe/sync-status
4. Create billing user POST /billing/users
5. Create Stripe setup intent POST /billing/users/{id}/setup-intent
6. Collect card on frontend (use Stripe.js with clientSecret)
7. Save payment method POST /billing/users/{id}/payment-methods
8. Now auto-recharge works! (CallPayMin charges the card when balance is low)
Error Reference
HTTP Status Codes
| Status | Meaning |
|---|---|
| 200 | Success |
| 201 | Created (new resource) |
| 400 | Bad Request (invalid input) |
| 401 | Unauthorized (bad or missing API key) |
| 402 | Payment Required (insufficient balance or card declined) |
| 403 | Forbidden (wrong scope, plan limit, or IP blocked) |
| 404 | Not Found |
| 405 | Method Not Allowed |
| 409 | Conflict (duplicate resource) |
| 429 | Rate Limited |
| 500 | Server Error |
Common Error Codes
Authentication (401)
| Code | Fix |
|---|---|
MISSING_API_KEY | Add Authorization: Bearer cpm_live_xxx header |
INVALID_API_KEY_FORMAT | Key must start with cpm_live_ or cpm_test_ |
INVALID_API_KEY | Check your key in the dashboard |
API_KEY_REVOKED | Generate a new key |
Authorization (403)
| Code | Fix |
|---|---|
INSUFFICIENT_PERMISSIONS | Add required scope to your API key |
IP_NOT_ALLOWED | Add your IP to the allowlist |
FEATURE_NOT_INCLUDED | Upgrade your plan |
JOB_LIMIT_REACHED | Upgrade plan or close existing jobs |
WORK_SESSION_LIMIT_REACHED | Upgrade plan for more concurrent sessions |
EXPERT_DAILY_LIMIT_REACHED | Expert has worked 8 hours today |
NOT_ROOM_OWNER | Only the room's expert can perform this action |
FORBIDDEN | You don't have access to this resource |
Payment (402)
| Code | Fix |
|---|---|
INSUFFICIENT_BALANCE | Add payment method to the user's account |
CARD_DECLINED | User needs to update their card |
PAYMENT_FAILED | Retry or use a different payment method |
NO_PAYMENT_METHOD | Save a card first via setup-intent |
Not Found (404)
| Code | Fix |
|---|---|
CALL_NOT_FOUND | Check the call ID |
CHAT_NOT_FOUND | Check the chat ID |
ROOM_NOT_FOUND | Check the room ID |
USER_NOT_FOUND | Check the user ID |
EXPERT_NOT_FOUND | Check the expert ID |
JOB_NOT_FOUND | Check the job ID |
SESSION_NOT_FOUND | Check the work session ID |
PAYER_NOT_FOUND | The payerId doesn't match any billing user's externalId |
TRANSCRIPTION_NOT_FOUND | Upload a recording first |
Bad Request (400)
| Code | Fix |
|---|---|
INVALID_PARTICIPANTS | Need at least 2 call participants |
INVALID_BILLING | Missing payerId or ratePerMinute |
INVALID_AMOUNT | Amount must be positive |
CHAT_NOT_ACTIVE | Chat has already ended |
CALL_ALREADY_ENDED | Call has already been ended |
ROOM_NOT_ACTIVE | Room hasn't started or is closed |
ROOM_NOT_SCHEDULED | Room must be in scheduled state to start |
ROOM_ALREADY_CLOSED | Room is already closed |
SESSION_NOT_ACTIVE | Work session is not active |
SESSION_NOT_COMPLETED | Can only review completed sessions |
ALREADY_REVIEWED | Review already submitted |
ALREADY_MEMBER | User is already a team member |
CHARGE_NOT_AVAILABLE_SELF_MANAGED | Switch to managed mode to charge cards |
SDK & Code Examples
JavaScript/TypeScript API Wrapper
class CallPayMin {
private baseUrl: string;
private apiKey: string;
constructor(apiKey: string, baseUrl = 'https://api.callpaymin.io/api/v1') {
this.apiKey = apiKey;
this.baseUrl = baseUrl;
}
private async request(method: string, path: string, body?: any) {
const res = await fetch(`${this.baseUrl}${path}`, {
method,
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json();
if (!res.ok) {
throw new Error(`${data.error}: ${data.message}`);
}
return data.data || data;
}
// Billing Users
async createUser(data: any) { return this.request('POST', '/billing/users', data); }
async getUser(userId: string) { return this.request('GET', `/billing/users/${userId}`); }
async addFunds(userId: string, amount: number, ref?: string) {
return this.request('POST', `/billing/users/${userId}/add-funds`, { amount, reference: ref });
}
// Experts
async createExpert(data: any) { return this.request('POST', '/experts', data); }
async getExpert(expertId: string) { return this.request('GET', `/experts/${expertId}`); }
// Calls
async createCall(data: any) { return this.request('POST', '/calls', data); }
async startCall(callId: string) { return this.request('POST', `/calls/${callId}/start`); }
async endCall(callId: string, reason = 'completed') {
return this.request('POST', `/calls/${callId}/end`, { reason });
}
async getCredentials(callId: string, externalId: string) {
return this.request('GET', `/calls/${callId}/credentials?externalId=${externalId}`);
}
// Chats
async createChat(data: any) { return this.request('POST', '/chats', data); }
async sendMessage(chatId: string, senderExternalId: string, content: string) {
return this.request('POST', `/chats/${chatId}/messages`, { senderExternalId, content });
}
async endChat(chatId: string) { return this.request('POST', `/chats/${chatId}/end`); }
// Rooms
async createRoom(data: any) { return this.request('POST', '/rooms', data); }
async joinRoom(roomId: string, participantExternalId: string) {
return this.request('POST', `/rooms/${roomId}/join`, { participantExternalId });
}
async closeRoom(roomId: string, expertExternalId: string) {
return this.request('POST', `/rooms/${roomId}/close`, { expertExternalId });
}
// Work Sessions
async createWorkSession(data: any) { return this.request('POST', '/work-sessions', data); }
async endWorkSession(sessionId: string) {
return this.request('POST', `/work-sessions/${sessionId}/end`);
}
// AI Summaries
async generateCallSummary(callId: string) {
return this.request('POST', `/summaries/calls/${callId}`);
}
async generateChatSummary(chatId: string) {
return this.request('POST', `/summaries/chats/${chatId}`);
}
}
// Usage
const cpm = new CallPayMin('cpm_live_YOUR_KEY');
const user = await cpm.createUser({
externalId: 'user-123',
email: 'alice@example.com',
displayName: 'Alice',
initialBalance: 50.00,
});
const call = await cpm.createCall({
participants: [
{ externalId: 'user-123', displayName: 'Alice', role: 'client' },
{ externalId: 'expert-456', displayName: 'Dr. Smith', role: 'expert' },
],
billing: { payerId: 'user-123', ratePerMinute: 5.00 },
});
// ... connect WebRTC, then:
await cpm.startCall(call.id);
// ... when done:
const result = await cpm.endCall(call.id);
console.log(`Cost: $${result.cost.total}`);
WebRTC Connection Example
async function connectToCall(callId, externalId, apiKey) {
// 1. Get credentials
const res = await fetch(
`https://api.callpaymin.io/api/v1/calls/${callId}/credentials?externalId=${externalId}`,
{ headers: { 'Authorization': `Bearer ${apiKey}` } }
);
const { data } = await res.json();
// 2. Create peer connection with TURN servers
const pc = new RTCPeerConnection({
iceServers: [{
urls: data.ice.urls,
username: data.ice.username,
credential: data.ice.credential,
}],
});
// 3. Add local media
const stream = await navigator.mediaDevices.getUserMedia({
video: data.config.video,
audio: data.config.audio,
});
stream.getTracks().forEach(track => pc.addTrack(track, stream));
// 4. Handle remote media
pc.ontrack = (event) => {
document.getElementById('remoteVideo').srcObject = event.streams[0];
};
// 5. Signal via your signaling server (data.signaling.url)
// ... implement offer/answer exchange ...
return { pc, stream };
}
Pagination
All list endpoints use cursor-based pagination:
First page:
curl "https://api.callpaymin.io/api/v1/calls?limit=20" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
Response includes a cursor for the next page:
{ "data": [...], "hasMore": true, "cursor": "call_xyz" }
Next page — pass the cursor:
curl "https://api.callpaymin.io/api/v1/calls?limit=20&cursor=call_xyz" \
-H "Authorization: Bearer cpm_live_YOUR_KEY"
| Param | Default | Max | Description |
|---|---|---|---|
limit | 50 | 100 | Items per page |
cursor | — | — | Token from previous response |
Response Envelope
Success:
{
"success": true,
"data": { ... }
}
Error:
{
"error": "ERROR_CODE",
"message": "What went wrong and how to fix it"
}
List:
{
"success": true,
"data": {
"data": [ ... ],
"hasMore": true,
"cursor": "next_page_token"
}
}
Need Help?
- API Status:
GET /api/v1/status - Swagger UI:
GET /api/v1/api-docs(interactive API explorer) - OpenAPI Spec:
GET /openapi.yaml(machine-readable) - Support: support@callpaymin.io
- Dashboard: callpaymin.io