# 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.

Source: https://business.mysmartassistant.ai/docs/partner-api

> **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`:

```text
timestamp
nonce
host
METHOD
path-with-query
sha256hex(raw body)
```

Then sign it:

```text
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.

```bash
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 |

```json
{
  "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](https://business.mysmartassistant.ai/docs/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

```bash
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:

```bash
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 |

```json
{
  "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.

## Related

- [API keys](https://business.mysmartassistant.ai/docs/api-keys)
- [Custom APIs](https://business.mysmartassistant.ai/docs/custom-apis)
- [Webhooks](https://business.mysmartassistant.ai/docs/webhooks)
