CallPayMin API Docs

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

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

EnvironmentPrefixExample
Productioncpm_live_cpm_live_F1xdKZos0B9ajRQjZ7f8KZgHiepFa5b6
Testcpm_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:

ScopeRead AccessWrite Access
calls:read / calls:writeList & get callsCreate, start, end calls
chats:read / chats:writeList & get chatsCreate chats, send messages
rooms:read / rooms:writeList & get roomsCreate, join, close rooms
billing:read / billing:writeView balances & transactionsAdd payment method, charge cards
experts:read / experts:writeList & get expertsCreate, update, payouts, KYC
jobs:read / jobs:writeList jobs & applicationsCreate jobs, review applications
work_sessions:read / work_sessions:writeView sessions & filesCreate sessions, upload files
organization:read / organization:writeView org settingsUpdate settings, manage team
summaries:read / summaries:writeView summariesGenerate summaries

Auth Errors

StatusCodeMeaning
401MISSING_API_KEYNo Authorization or X-API-Key header
401INVALID_API_KEY_FORMATKey doesn't start with cpm_live_ or cpm_test_
401INVALID_API_KEYKey not found (wrong key or deleted)
401API_KEY_REVOKEDKey was revoked
403IP_NOT_ALLOWEDRequest IP not in allowlist
403INSUFFICIENT_PERMISSIONSKey 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

FeatureSelf-ManagedManaged
Who charges customersYouCallPayMin (Stripe)
Who pays expertsYouCallPayMin (Stripe Connect)
Platform feeNone3.5% per transaction
Plan pricesLowerHigher
Stripe account neededNoYes (auto-created)
Expert KYCYou handleCallPayMin handles
Auto-billingWebhook notificationAuto-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

PlanMonthlyCall MinutesChat MessagesAI SummariesRate LimitAPI Keys
Free$01005001060 req/min2
Starter$591,000Unlimited100300 req/min10
Professional$1792,500Unlimited250500 req/min25
Growth$34910,000Unlimited1,0001,000 req/min50
EnterpriseCustomUnlimitedUnlimitedUnlimited5,000 req/minUnlimited

Managed Plans

PlanMonthlyCall MinutesChat MessagesAI SummariesRate LimitAPI Keys
Free$050250530 req/min2
Starter$1291,000Unlimited100250 req/min10
Professional$3492,500Unlimited250400 req/min25
Growth$69910,000Unlimited1,000800 req/min50
Enterprise$1,49925,000Unlimited1,0004,000 req/min50

Allowance policy

There are no automatic usage-overage charges. Upgrade or contact sales when a monthly allowance is exhausted. Annual plans retain monthly usage allowances.

Feature Availability

FeatureFreePayGStarterProfessionalGrowthEnterprise
CallsYesYesYesYesYesYes
ChatsYesYesYesYesYesYes
Meeting RoomsYes (eval)NoNoYesYesYes
Jobs APIYes (eval, 1 job)NoNoYesYesYes
Work SessionsYes (eval)NoNoYesYesYes
AI SummariesYesYesYesYesYesYes
RecordingYesYesYesYesYesYes

Rate Limits

Every response includes rate limit headers:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1708108800
HeaderMeaning
X-RateLimit-LimitMax requests per minute for your plan
X-RateLimit-RemainingRequests left in this window
X-RateLimit-ResetUnix 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"
    }
  }'
FieldTypeRequiredDescription
namestringNoOrganization display name
settingsobjectNoCustom settings
webhooksobjectNoWebhook URL configuration
paymentsobjectNoPayment 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": "$179/month",
        "limits": { "callMinutes": 2500, "chatMessages": "Unlimited", "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" }'
FieldTypeRequiredDescription
planIdstringYesOne of: free, startup, professional, enterprise

Errors:

  • 400 INVALID_PLAN — Plan ID not recognized
  • 400 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 }'
FieldTypeRequiredDescription
immediatebooleanNotrue = 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.

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"
  }'
FieldTypeRequiredDescription
imageDatastringYesBase64-encoded image (with or without data URI prefix)
fileNamestringNoFile name
contentTypestringNoMIME 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"
  }'
FieldTypeRequiredDescription
emailstringYesEmail to invite
rolestringNoRole for the invitee

Invitation link expires in 7 days. An email is sent automatically.

Errors:

  • 400 ALREADY_MEMBER — Email already belongs to a team member
  • 400 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"
  }'
FieldTypeRequiredDescription
callStartedstringNoURL for call.created, call.started, room.created, room.started events
callEndedstringNoURL for call.ended, chat.ended, summary.generated events
chatMessagestringNoURL for chat.message events
balanceLowstringNoURL for balance.low, balance.recharged events
expertVerifiedstringNoURL 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 ParamTypeDefaultDescription
statusstringAllFilter: success, failed, pending
eventTypestringAllFilter by event type
limitnumber20Max 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" }'
FieldTypeRequiredDescription
newModestringYes"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"
    }
  }'
FieldTypeRequiredDescription
externalIdstringYesYour unique ID for this user
emailstringYesUser's email
displayNamestringYesDisplay name
phonestringNoPhone number
initialBalancenumberNoStarting balance in USD (default: 0)
currencystringNoCurrency code (default: "USD")
autoRecharge.enabledbooleanNoEnable auto-recharge (default: false)
autoRecharge.thresholdnumberNoTrigger recharge when balance drops below this (default: $5)
autoRecharge.amountnumberNoAmount to add when recharging (default: $25)
autoRecharge.webhookUrlstringNoSelf-managed: URL to notify when balance is low
metadataobjectNoCustom 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 ParamTypeDefaultDescription
statusstringAllFilter by status
balanceBelownumberUsers with balance below this amount
balanceAbovenumberUsers with balance above this amount
limitnumber50Items per page (max 100)
cursorstringPagination 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"
  }'
FieldTypeRequiredDescription
amountnumberYesAmount to add (must be positive)
currencystringNoCurrency (default: user's currency)
referencestringNoYour payment reference ID
descriptionstringNoNote for the transaction

Errors:

  • 400 INVALID_AMOUNT — Amount must be positive
  • 404 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
  }'
FieldTypeRequiredDescription
amountnumberYesAmount to charge (minimum $1.00)
paymentMethodIdstringNoSpecific card to charge (default: user's default card)
descriptionstringNoDescription for the charge
passStripeFeeToUserbooleanNoIf 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 first
  • 400 NO_PAYMENT_METHOD — User has no saved card
  • 402 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 ParamTypeDefaultDescription
typestringAllcredit, debit, or refund
categorystringAlldeposit, call, chat, room, work_session
limitnumber50Items per page
cursorstringPagination 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" }
  }'
FieldTypeRequiredDescription
externalIdstringYesYour unique ID for this expert
emailstringYesExpert's email
profile.firstNamestringYesFirst name
profile.lastNamestringYesLast name
profile.countrystringYesCountry code (e.g., "US")
profile.biostringNoShort bio
profile.skillsstring[]NoSkill tags
rates.perMinutenumberYesDefault rate per minute
revenueShare.expertnumberNoExpert's percentage (default: 80)
revenueShare.platformnumberNoBusiness's percentage (default: 20)
metadataobjectNoCustom 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 ParamTypeDefaultDescription
statusstringAllFilter by status
limitnumber50Items per page
cursorstringPagination 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" }
  }'
FieldTypeRequiredDescription
profileobjectNoProfile fields to update
ratesobjectNoRate updates
revenueShareobjectNoRevenue split updates
payoutobjectNoPayout settings
metadataobjectNoCustom 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 onboarding
  • 400 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" }
  }'
FieldTypeRequiredDescription
titlestringYesJob title
descriptionstringYesJob description
skillsstring[]NoRequired skills
ratePerMinutenumberYesPay rate per minute
requirementsobjectNoAdditional requirements
metadataobjectNoCustom 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!"
  }'
FieldTypeRequiredDescription
actionstringYes"approve" or "reject"
notesstringNoNotes for the applicant

Errors:

  • 400 ALREADY_REVIEWED — Application was already reviewed
  • 400 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
  }'
FieldTypeRequiredDescription
negotiationNotesstringYesMessage to the applicant
counterOfferRatenumberNoCounter-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" }
  }'
FieldTypeRequiredDescription
participantsarrayYes2+ participants
participants[].externalIdstringYesYour user ID
participants[].displayNamestringYesDisplay name
participants[].rolestringYes"client" or "expert"
billing.payerIdstringYesexternalId of the person who pays
billing.ratePerMinutenumberYesRate per minute in USD
billing.currencystringNoCurrency (default: "USD")
billing.gracePeriodSecondsnumberNoFree seconds before billing starts (default: 60)
config.videobooleanNoEnable video (default: true)
config.audiobooleanNoEnable audio (default: true)
config.aiSummaryEnabledbooleanNoAuto-generate AI summary when call ends
metadataobjectNoCustom data

Errors:

  • 400 INVALID_PARTICIPANTS — Need at least 2 participants
  • 400 INVALID_BILLING — Missing payerId or ratePerMinute
  • 404 PAYER_NOT_FOUND — Payer externalId doesn't match any billing user
  • 402 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 ParamTypeDefaultDescription
statusstringAllpending, active, ended
limitnumber50Items per page
cursorstringPagination 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" }'
FieldTypeRequiredDescription
reasonstringNo"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 ParamTypeRequiredDescription
participantIdstringOne of theseInternal participant ID
externalIdstringOne of theseYour 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 participant
  • 400 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
  }'
FieldTypeRequiredDescription
fileUrlstringYesURL of the recording file
durationSecondsnumberYesRecording duration in seconds
formatstringNoFile format (e.g., "webm", "mp4")
fileSizenumberNoFile 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" }
  }'
FieldTypeRequiredDescription
participantsarrayYesAt least 1 expert required
participants[].rolestringYes"user" or "expert"
billing.payerIdstringYesexternalId of the payer
billing.ratePerMinutenumberNoRate per minute (message cost = rate / 10)
metadataobjectNoCustom 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"
  }'
FieldTypeRequiredDescription
senderExternalIdstringYesSender's external ID
contentstringYesMessage text
contentTypestringNo"text" (default) or "file"
attachmentobjectNoFile attachment details

Errors:

  • 400 CHAT_NOT_ACTIVE — Chat has ended
  • 402 INSUFFICIENT_BALANCE — Payer doesn't have enough balance
  • 403 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 ParamTypeDefaultDescription
limitnumber50Messages per page
cursorstringPagination cursor
orderstring"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 }
  }'
FieldTypeRequiredDescription
expertExternalIdstringYesExpert who hosts the room
namestringYesRoom name
descriptionstringNoRoom description
maxParticipantsnumberNoMax participants (default: no limit)
scheduledTimestringNoISO 8601 start time (creates as "scheduled")
scheduledEndTimestringNoISO 8601 end time
meetingBaseUrlstringNoYour frontend URL for the meeting
settingsobjectNoRoom settings (rate, video, audio)
metadataobjectNoCustom data

Room states: scheduledactiveclosed

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 ParamTypeDefaultDescription
statusstringAllscheduled, active, closed
expertExternalIdstringFilter by expert
limitnumber50Items per page
cursorstringPagination 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" }'
FieldTypeRequiredDescription
expertExternalIdstringYesMust match the room's host expert

Errors:

  • 400 ROOM_NOT_SCHEDULED — Room is not in scheduled state
  • 403 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"
  }'
FieldTypeRequiredDescription
participantExternalIdstringYesBilling user's externalId
participantNamestringNoDisplay name override

Errors:

  • 400 ROOM_NOT_ACTIVE — Room not yet started or already closed
  • 400 WAITING_FOR_HOST — Expert hasn't joined yet
  • 402 INSUFFICIENT_BALANCE — Not enough balance for minimum billing
  • 409 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:

  1. Your frontend calls /billing-increment every 5 minutes per participant
  2. 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
  3. If balance is insufficient, charges whatever remains (partial charge) and signals disconnect
  4. 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 closed
  • 403 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" }
  }'
FieldTypeRequiredDescription
jobIdstringYesAssociated job ID
questerIdstringYesBilling user ID (who pays)
workerIdstringYesExpert ID (who works)
ratePerMinutenumberYesWork rate per minute
metadataobjectNoCustom data

Errors:

  • 403 EXPERT_DAILY_LIMIT_REACHED — Expert has worked 480 minutes (8 hours) today
  • 404 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 ParamTypeDefaultDescription
statusstringAllactive, completed, cancelled
workerIdstringFilter by expert
questerIdstringFilter by client
jobIdstringFilter by job
limitnumber50Items per page
cursorstringPagination 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 questerId query 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
  }'
FieldTypeRequiredDescription
questerIdstringYesBilling user ID
videoCallActivebooleanNoIf true, adds $0.04/min video fee

How work session billing works:

  1. Your frontend calls /process-increment every 5 minutes
  2. Server uses Firestore transaction to atomically deduct from quester and credit worker
  3. If video call is active, adds $0.04/min video call platform fee
  4. If balance is insufficient, charges whatever remains (partial charge)
  5. Auto-billing is triggered proactively after each increment
  6. When session ends, /end only 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?"
  }'
FieldTypeRequiredDescription
senderIdstringYesSender's user/expert ID
senderRolestringYes"quester" or "worker"
textstringYesMessage 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"
  }'
FieldTypeRequiredDescription
fileNamestringYesFile name with extension
fileDatastringYesBase64-encoded file content
fileTypestringNoMIME type
senderIdstringYesUploader's ID
senderRolestringNo"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"
  }'
FieldTypeRequiredDescription
ratingnumberYes1-5 stars
commentstringNoReview text
deliverableReceivedbooleanNoDid you receive the deliverable?
reviewerNamestringNoReviewer's display name

Errors:

  • 400 SESSION_NOT_COMPLETED — Can only review completed sessions
  • 400 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 summary
  • 404 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

  1. 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"
  }'
  1. Get your signing secret — available in the webhook config response (secretPrefix) or via the dashboard.

Event Types

EventWebhook FieldDescription
call.createdcallStartedNew call created
call.startedcallStartedCall became active
call.endedcallEndedCall ended with billing details
chat.createdcallStartedNew chat created
chat.messagechatMessageMessage sent in a chat
chat.endedcallEndedChat ended
room.createdcallStartedNew room created
room.startedcallStartedRoom became active
room.participant_joinedcallStartedSomeone joined a room
room.participant_leftcallEndedSomeone left a room
room.billing_incrementcallEndedRoom billing tick processed
room.closedcallEndedRoom closed
balance.lowbalanceLowUser balance dropped below threshold
balance.rechargedbalanceLowUser balance auto-recharged
expert.verifiedexpertVerifiedExpert completed KYC
expert.payoutexpertVerifiedExpert payout processed
summary.generatedcallEndedAI summary ready
transcription.completedcallEndedTranscription 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:

  1. Parse the X-CallPayMin-Signature header: t=<timestamp>,v1=<signature>
  2. Construct the signed string: {timestamp}.{raw_json_body}
  3. Compute HMAC-SHA256 using your webhook secret
  4. Compare (timing-safe) against the provided signature
  5. 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

AttemptDelayTotal time elapsed
1Immediate0
21 minute1 min
35 minutes6 min
415 minutes21 min
51 hour1 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

StatusMeaning
200Success
201Created (new resource)
400Bad Request (invalid input)
401Unauthorized (bad or missing API key)
402Payment Required (insufficient balance or card declined)
403Forbidden (wrong scope, plan limit, or IP blocked)
404Not Found
405Method Not Allowed
409Conflict (duplicate resource)
429Rate Limited
500Server Error

Common Error Codes

Authentication (401)

CodeFix
MISSING_API_KEYAdd Authorization: Bearer cpm_live_xxx header
INVALID_API_KEY_FORMATKey must start with cpm_live_ or cpm_test_
INVALID_API_KEYCheck your key in the dashboard
API_KEY_REVOKEDGenerate a new key

Authorization (403)

CodeFix
INSUFFICIENT_PERMISSIONSAdd required scope to your API key
IP_NOT_ALLOWEDAdd your IP to the allowlist
FEATURE_NOT_INCLUDEDUpgrade your plan
JOB_LIMIT_REACHEDUpgrade plan or close existing jobs
WORK_SESSION_LIMIT_REACHEDUpgrade plan for more concurrent sessions
EXPERT_DAILY_LIMIT_REACHEDExpert has worked 8 hours today
NOT_ROOM_OWNEROnly the room's expert can perform this action
FORBIDDENYou don't have access to this resource

Payment (402)

CodeFix
INSUFFICIENT_BALANCEAdd payment method to the user's account
CARD_DECLINEDUser needs to update their card
PAYMENT_FAILEDRetry or use a different payment method
NO_PAYMENT_METHODSave a card first via setup-intent

Not Found (404)

CodeFix
CALL_NOT_FOUNDCheck the call ID
CHAT_NOT_FOUNDCheck the chat ID
ROOM_NOT_FOUNDCheck the room ID
USER_NOT_FOUNDCheck the user ID
EXPERT_NOT_FOUNDCheck the expert ID
JOB_NOT_FOUNDCheck the job ID
SESSION_NOT_FOUNDCheck the work session ID
PAYER_NOT_FOUNDThe payerId doesn't match any billing user's externalId
TRANSCRIPTION_NOT_FOUNDUpload a recording first

Bad Request (400)

CodeFix
INVALID_PARTICIPANTSNeed at least 2 call participants
INVALID_BILLINGMissing payerId or ratePerMinute
INVALID_AMOUNTAmount must be positive
CHAT_NOT_ACTIVEChat has already ended
CALL_ALREADY_ENDEDCall has already been ended
ROOM_NOT_ACTIVERoom hasn't started or is closed
ROOM_NOT_SCHEDULEDRoom must be in scheduled state to start
ROOM_ALREADY_CLOSEDRoom is already closed
SESSION_NOT_ACTIVEWork session is not active
SESSION_NOT_COMPLETEDCan only review completed sessions
ALREADY_REVIEWEDReview already submitted
ALREADY_MEMBERUser is already a team member
CHARGE_NOT_AVAILABLE_SELF_MANAGEDSwitch 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"
ParamDefaultMaxDescription
limit50100Items per page
cursorToken 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