Developers
Partner API
The Partner API lets a CRM partner create and run MySA businesses for its own customers, entirely from the CRM, with no MySA console login.
Warning: Coming soon. The Partner API is not officially live yet. This page describes how it will work so partners can plan their integration; details may change before launch. To be among the first partners, connect with our sales team.
Get a partner account
Partner accounts are not self-serve. To onboard as a partner, first connect with our sales team — use Contact sales on the website or email support@mysmartassistant.ai. Once your partnership is agreed, MySA sets up your partner keys, your IP allowlist and your webhook URL, and shares your credentials and the Postman collection.
Note: You cannot create a partner account from the console. A normal MySA Business login does not give you Partner API access.
Two kinds of key
| Key | Used for | Authentication |
|---|---|---|
| Partner key | Creating, listing, suspending and resuming businesses, rotating their keys, reading your webhook deliveries | A signed request: X-Mysa-Key-Id, X-Mysa-Timestamp, X-Mysa-Nonce, X-Mysa-Signature |
| Business key | Everything one business does: purposes, calls, campaigns, wallet, numbers, Business Details | X-Org-Id + X-Api-Key, no signature |
Partner keys, your IP allowlist and your webhook URL are set up by MySA during onboarding. Every request with either key must come from an IP on your allowlist.
Partner routes live under /v1/partner/. Business routes live under /v1/api/.
Signing a partner request
Build a canonical string from six lines, joined with \n:
timestamp
nonce
host
METHOD
path-with-query
sha256hex(raw body)Then sign it:
X-Mysa-Signature = hex( HMAC-SHA256( secret, canonical ) )| Part | Rules |
|---|---|
timestamp | Unix seconds. Must be within 300 seconds of MySA's clock (the default) |
nonce | 16 to 64 characters of A-Z a-z 0-9 _ -. A new one for every request |
host | The API host MySA gives you, lowercase. Not taken from your request |
METHOD | Uppercase, for example POST |
| path | The path including the query string, for example /v1/partner/sub-accounts?limit=50 |
| body hash | SHA-256 hex of the exact bytes you send. For an empty body, hash the empty string |
The secret only signs; it is never sent.
TS=$(date +%s)
NONCE=$(openssl rand -hex 16)
BODY='{"external_id":"crm-7781","business_name":"Sharma Motors","owner_name":"Anil Sharma","owner_email":"anil@example.com","owner_phone":"+919876543210"}'
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.* //')
CANONICAL=$(printf '%s\n%s\n%s\n%s\n%s\n%s' "$TS" "$NONCE" "YOUR_SIGN_HOST" "POST" "/v1/partner/sub-accounts" "$BODY_HASH")
SIG=$(printf '%s' "$CANONICAL" | openssl dgst -sha256 -hmac "YOUR_PARTNER_SECRET" -hex | sed 's/^.* //')
curl -X POST "https://YOUR_API_HOST/v1/partner/sub-accounts" \
-H "Content-Type: application/json" \
-H "X-Mysa-Key-Id: YOUR_KEY_ID" \
-H "X-Mysa-Timestamp: $TS" \
-H "X-Mysa-Nonce: $NONCE" \
-H "X-Mysa-Signature: $SIG" \
--data-raw "$BODY"Warning: Hash the raw bytes you send, not a re-serialised copy of your JSON. A different key order or spacing produces a different hash and a BAD_CREDENTIALS answer.
Tip: On a 401, print your canonical string first. The most common cause is signing for the wrong host.
Sub-accounts
A sub-account is one business. Create it with your own external_id:
| Field | Rules |
|---|---|
external_id | 1 to 100 of A-Z a-z 0-9 . _ : - |
business_name | 1 to 160 characters |
owner_name | 1 to 160 characters |
owner_email | A valid email with no existing MySA account |
owner_phone | 6 to 32 characters |
{
"data": {
"created": true,
"sub_account": {
"id": "01J9ZK3M5Q8R2T4V6W8X0Y1Z2A",
"external_id": "crm-7781",
"organization_id": "org_123",
"business_name": "Sharma Motors",
"status": "active",
"balance_minor": "0",
"created_at": "2026-10-01T09:30:00.000Z"
},
"api_key": "mysa_xxxxxxxx_xxxxxxxx"
}
}Creating is safe to retry. A new business answers 201 with its business key. Sending the same external_id again answers 200 with created: false and api_key: null. The same external_id with a different owner email or phone is refused.
Use organization_id as X-Org-Id and api_key as X-Api-Key for that business. The key is shown only in this response.
Scopes
Every business key carries the standard workspace scopes (see API keys) plus the ones a business with no screen needs:
purpose.read, purpose.manage, campaign.create, campaign.update, campaign.approve, businessfile.write, billing.read, org.update, payment.request, invoice.read, number.purchase, customapi.manage.
A key gets the scopes that existed when it was issued. A key minted before a scope was added picks it up on its next rotation.
Main endpoints
Partner key (signed)
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/partner/sub-accounts | Create a business, or return the existing one for that external_id |
| GET | /v1/partner/sub-accounts | List businesses. Query: external_id, cursor, limit (1 to 200) |
| GET | /v1/partner/sub-accounts/{id} | One business |
| POST | /v1/partner/sub-accounts/{id}/suspend | Suspend a business; its key stops working |
| POST | /v1/partner/sub-accounts/{id}/resume | Resume a business |
| POST | /v1/partner/sub-accounts/{id}/keys/rotate | Issue a new business key |
| POST | /v1/partner/sub-accounts/{id}/keys/{prefix}/revoke | Revoke one business key |
| GET | /v1/partner/webhook-events | Your webhook deliveries. Query: status (pending, delivered, failed), cursor, limit |
| POST | /v1/partner/webhook-events/{id}/redeliver | Send one event again |
{id} is sub_account.id from the create response.
Business key
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /v1/api/status | — | What this business still needs to set up |
| GET | /v1/api/key | — | This key's prefix and usage |
| POST | /v1/api/key/rotate | — | Rotate this key, at most once in 24 hours |
| GET, POST | /v1/api/purposes | purpose.read, purpose.manage | List or create purposes |
| PATCH | /v1/api/purposes/{id} | purpose.manage | Update a purpose |
| POST | /v1/api/calls | campaign.run | Place 1 to 5000 calls. Requires an Idempotency-Key header |
| GET | /v1/api/calls/{id} | calllog.read | A call's status and result |
| GET | /v1/api/calls/{id}/recording | calllog.read | Redirects to a fresh recording link |
| POST | /v1/api/calls/{id}/cancel | campaign.run | Cancel a queued call |
| GET | /v1/api/batches/{id} | calllog.read | Counts for one batch |
| POST | /v1/api/batches/{id}/cancel | campaign.run | Cancel a batch |
| GET, POST | /v1/api/campaigns | campaign.read, campaign.create | List or create campaigns |
| POST | /v1/api/campaigns/{id}/stop | campaign.run | Stop a campaign |
| GET | /v1/api/follow-ups, /v1/api/reminders | calllog.read | Follow-ups and visit reminders |
| GET, PUT | /v1/api/billing-details | billing.read, org.update | GST and billing details |
| GET | /v1/api/wallet | wallet.read | Balance and recent recharges |
| POST | /v1/api/wallet/recharge-link | payment.request | Create a payment link |
| GET | /v1/api/invoices | invoice.read | Tax invoices |
| GET | /v1/api/numbers | number.read | The business's numbers |
| POST | /v1/api/numbers/purchase | number.purchase | Buy a number after KYC |
| GET, PUT | /v1/api/transfer-settings | org.update | Transfer to a human |
| GET, POST | /v1/api/custom-apis | customapi.manage | The business's Custom APIs |
| POST | /v1/api/business-details/files | businessfile.write | Upload one Business Details file (10 MB) |
Placing calls
curl -X POST https://YOUR_API_HOST/v1/api/calls \
-H "X-Org-Id: org_123" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Idempotency-Key: crm-batch-20261001-0001" \
-H "Content-Type: application/json" \
-d '{
"purpose_id": "PURPOSE_ID",
"contacts": [
{ "phone": "+919876543210", "name": "Ravi Kumar", "client_ref": "LEAD-55", "user_query": "Asked for a test ride" }
]
}'If you do not know the customer's name, send "name": "Customer". The AI then asks for it on the call, and the call's name in GET /v1/api/calls/{id} and in call.completed is the name the customer gave. Your contact's own name is not changed.
Send purpose_id, or campaign_id for a campaign you created. The answer is 202: the calls are queued, not yet dialled. The Idempotency-Key is 8 to 100 of A-Z a-z 0-9 . _ : -. Reuse it only to retry the same batch; a new key queues the calls again.
Transfer numbers
A business can hand a call to one of several named numbers, for example Sales and Support. The AI picks the number from its name and instructions. PUT /v1/api/transfer-settings with targets replaces the whole list:
curl -X PUT https://YOUR_API_HOST/v1/api/transfer-settings -H "X-Org-Id: org_123" -H "X-Api-Key: YOUR_API_KEY" -H "Content-Type: application/json" -d '{
"outbound_enabled": true,
"targets": [
{ "name": "Sales", "number": "+919811111111", "instructions": "When they want to buy" },
{ "name": "Support", "number": "+919822222222", "instructions": "Refunds or a broken order", "briefing_language": "en" }
]
}'GET returns the saved targets and targets_limit, the most numbers this business may set. Every number needs a name. instructions is up to 100 characters. Two numbers cannot share a name or a number. Leave briefing_language out to use the business default. The older number and instructions fields still work and change only the first number.
Webhook events
MySA POSTs events to the webhook URL set up for your partner account:
| Event | When |
|---|---|
call.completed | A call finished |
wallet.credited | A recharge was credited |
number.kyc_verified, number.purchased, number.renewed, number.renewal_failed, number.released | Number lifecycle |
{
"id": "01J9ZM0A1B2C3D4E5F6G7H8J9K",
"type": "call.completed",
"created_at": "2026-10-01T15:12:10.000+05:30",
"organization_id": "org_123",
"data": { "...": "the same call object as GET /v1/api/calls/{id}" }
}Every timestamp in a webhook is ISO 8601 in IST, with +05:30 (for example 2026-10-01T15:12:10.000+05:30). Any ISO 8601 parser reads it as the exact moment. Dates without a time, such as visit_date, are IST dates. Responses from the REST API stay in UTC (Z).
Each delivery carries X-Mysa-Event-Id, X-Mysa-Event-Type and X-Mysa-Signature: t=<unix>,v1=<hex>, where v1 is HMAC-SHA256 of t + "." + body with your webhook secret. Refuse a t more than 5 minutes off your clock.
Reply 2xx within 10 seconds. Anything else is retried with growing gaps for 24 hours, then marked failed; use redeliver to send it again.
Errors
Errors come back as { "error": { "code", "message", "requestId" } }.
| Status | Code | Meaning |
|---|---|---|
| 401 | PARTNER_AUTH_REQUIRED | One of the four X-Mysa-* headers is missing |
| 401 | BAD_CREDENTIALS | Unknown, revoked or expired key, or a wrong signature |
| 401 | STALE_TIMESTAMP | Timestamp outside the allowed window |
| 401 | BAD_NONCE, NONCE_REUSED | Nonce malformed or already used |
| 403 | IP_NOT_ALLOWED | Request came from an IP not on your allowlist |
| 403 | PARTNER_NOT_ACTIVE, SUB_ACCOUNT_SUSPENDED | Partner or business switched off |
| 403 | PARTNER_SUB_ACCOUNT_ONLY | A route for partner businesses only |
| 400 | IDEMPOTENCY_KEY_REQUIRED | POST /v1/api/calls without an Idempotency-Key |
| 409 | EXTERNAL_ID_CONFLICT, EMAIL_TAKEN, SUB_ACCOUNT_LIMIT | Create refused |
| 429 | QUEUE_FULL, ROTATE_TOO_SOON | Too many waiting calls, or a second rotate within 24 hours |
| 503 | NONCE_STORE_UNAVAILABLE | Temporary problem on MySA's side; retry with a new nonce |
Postman collection
A runnable Postman collection, MySA Partner API, covers every route above, with the signing script built in. Fill in apiBase, signHost, keyId and secret; the rest fill themselves from responses. Ask your MySA contact for it.

