Platform · Partner API

One signed call creates a whole client account.

24 server-to-server endpoints for platforms that resell messaging. Create an account for each of your clients, send on their behalf, quote before you send, and pull a ledger to reconcile against your own billing. Provisioning is keyed on your id for the client, so calling it twice is safe.

Signed, with a 5-minute windowScoped keys + IP allowlistRepeat calls return the same account
POST /partner/tenants
curl https://server.quickgrow.ai/api/v1/partner/tenants \
  -H "X-Partner-Key: pk_..." \
  -H "X-Partner-Signature: t=1755912000,v1=..." \
  -H "Content-Type: application/json" \
  -d '{
    "externalId": "acme-retail-4821",
    "brandName": "Acme Retail",
    "ownerEmail": "[email protected]",
    "ownerFirstName": "Ayesha",
    "ownerLastName": "Rahman"
  }'

# 201  { "data": { "tenantId": "9f2a1c34-...", "apiKey": "mk_...", "created": true  } }
# the same call again, any time later:
# 201  { "data": { "tenantId": "9f2a1c34-...", "apiKey": null,    "created": false } }

Every call is signed, scoped and pinned to your servers.

A partner key is issued by us, never self-serve. It is a server-to-server credential: it carries no browser session, it never reaches your client's browser, and four separate checks run before a route body executes.

A key and a signature

Send X-Partner-Key with your pk_ key and X-Partner-Signature as t=<unix>,v1=<hex>. The signature covers the exact request body bytes, keyed by the raw partner key, and is compared in constant time.

A 5-minute window

The timestamp is signed together with the body and must be within five minutes of our clock, so a captured request cannot be replayed later under a fresh timestamp.

An IP allowlist

Give us the addresses your backend calls from and everything else is refused, so a leaked key is useless off your network. Leave the list empty and the check is skipped.

Per-key scopes

A key holds any of provision, read, send, billing — or all. Each route demands exactly one. A key with no scopes can do nothing, which is the safe default rather than an accident.

Errors come back in one shape: { error: { code, message } }.
StatusWhat produced it
Key missing, unknown, disabled or expired401The pk_ key did not resolve to a live key.
Signature missing, malformed or wrong401No signature header, a bad t=…,v1=… shape, or a body that does not match it.
Timestamp more than 5 minutes out401Clock drift on your side, or a replayed request.
Caller IP not on the allowlist403The key is valid, but not from that address.
Key lacks the scope the route needs403A read-only key calling a send or provision route.
Client id belongs to another partner404Not 403 — you cannot even learn that the account exists.
Per-minute send ceiling reached429The per-account velocity cap on the send path.

Keyed on your id, not ours.

You already have an id for every client in your own system. Pass it as externalId and it becomes the identity for that account across the whole API — every later route addresses the client by your id, so you never have to store ours.

  1. We issue the key

    Partner access is arranged with us, not signed up for. You tell us the scopes you need and the addresses you will call from; we hand back a pk_ key.

  2. POST /partner/tenants

    Send your externalId, the brand name, and the owner’s name and email. Country and currency are optional. You get back the account id and a fresh API key for it.

  3. Call it again safely

    Your partner id and your externalId together are unique in our database. A repeat call — a retry, a double-submit, two workers racing — returns the same account with created:false and no second key.

  4. Then send, read and settle

    From there the account behaves like any other QuickGrow account, and you drive it over the remaining endpoints using your own id in the path.

What one provisioning call actually creates

The account

  • A separate account with its own data — nothing is shared with your other clients
  • Named from the brand name you sent, which becomes their sender name
  • Country and currency set from your payload, or left at our defaults

The login

  • A real owner login for the person whose email you sent
  • A set-password link, valid for seven days
  • No plaintext password is generated, stored or emailed — ever

The key

  • An mk_ API key for that account, returned once in the response
  • Scoped to send and read messages, read and write contacts, read templates
  • Never billing, never settings, never team management
03 — Sending

Send one, or send fifty thousand.

There are two send paths. One message at a time for codes, receipts and alerts — you get the message id back, which is the id delivery webhooks carry. Or a batch: an explicit list of numbers and addresses, or a saved segment, rendered from inline text or a template.

  • Batches take an Idempotency-Key. Repeat it and you get the original batch back, not a second send.
  • scheduledAt queues a batch for later; omit it and it goes now.
  • Quote a batch first and get the recipient count, the SMS segment count and the estimated cost in taka — without sending.
  • Poll the batch for per-state counts, or pull a signed report you can hand a client.
  • Re-queue only the recipients that failed; delivered ones are never touched.
SMS · Email · WhatsApp Per-recipient suppressions
POST /partner/tenants/:externalId/messages/bulk
curl https://server.quickgrow.ai/api/v1/partner/tenants/acme-retail-4821/messages/bulk \
  -H "X-Partner-Key: pk_..." \
  -H "X-Partner-Signature: t=1755912600,v1=..." \
  -H "Idempotency-Key: fees-aug-2026" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "SMS",
    "recipients": ["+8801710000001", "+8801710000002"],
    "text": "Fees for {{name}} are due Friday.",
    "scheduledAt": "2026-09-01T09:00:00.000Z"
  }'

# 202  { "data": { "batchId": "6c1e...", "accepted": 2, "rejected": [] } }
# then: GET .../messages/bulk/6c1e...         -> per-state counts
#       GET .../messages/bulk/6c1e.../report  -> signed delivery report

The rules that will bite you if you design around them late.

These are platform rules, not per-partner settings. Build for them in the first sprint rather than discovering them in production.

Read this one first

The night window is enforced

SMS and WhatsApp run consent, then the quiet-hours window, then a per-contact frequency cap before a message leaves. In Bangladesh the window is 21:00 to 08:00. A batch that lands inside it is deferred to the next allowed time; a single send is refused. Email is not gated. Never design a flow that assumes a one-time code goes out at 11pm.

A per-minute ceiling per account

Both send paths check a per-account, per-channel velocity cap and return 429 with a rate_limited code once it is hit. It is a platform default — 600 a minute today — not something a partner raises from the API. Queue and retry rather than bursting.

Suppressions are per recipient

Add an opt-out for one phone number or email on one account, and list them back. They are honoured on the batch path at dispatch. Recipient values are stored hashed with that account’s own salt, so a suppression list is not a readable address book.

You get told, and you can prove it.

Give us a URL and a secret and we post six events to it. Every delivery is signed the same way your requests are, so your receiver verifies ours with the code you already wrote.

Six events, signed

tenant.provisioned, subscription.activated, wallet.credited, wallet.low, wallet.depleted and wallet.autocredit_failed. Each carries an X-QuickGrow-Signature of t=…,v1=…, and your correlation id is echoed inside the signed body when you sent one.

Failures queue, and you replay them

Deliveries are retried with backoff. Anything that exhausts its attempts is kept rather than dropped, and one call replays them — all of them, or a single job you name.

A ledger, not a guess

Ask for every account of yours that changed since a timestamp and get subscription status, credits, spend and balance for each. Pull it nightly and reconcile against your own invoices.

Wallet auto-refill — what it does, and what it does not

You can store a threshold and a top-up amount per account. When the balance drops below the threshold we raise wallet.low, email the account owner a reminder, and — once the balance reaches zero — raise wallet.depleted as well. It does not charge a saved card. There is no off-session charge today, so a sweep that cannot collect raises wallet.autocredit_failed rather than record a credit that never happened. Treat it as a low-balance alarm you route into your own dunning, and use the hosted top-up link to actually take the money.

All 24 endpoints, grouped by the scope they need.

Everything lives under /api/v1/partner. Routes that address one client take your externalId in the path. Give each of your own services only the scopes it uses.

provision — 9

  • POST /tenants
  • PATCH /tenants/:externalId
  • POST /tenants/:externalId/rotate-key

read — 8

  • GET /branding
  • GET /reconciliation?since=
  • GET /tenants/:externalId/analytics

send — 3

  • POST /tenants/:externalId/messages
  • POST /tenants/:externalId/messages/bulk
  • POST /tenants/:externalId/messages/bulk/:batchId/retry-failed

billing — 4

  • GET /plans
  • POST /tenants/:externalId/subscription/checkout-session
  • POST /tenants/:externalId/wallet/topup-session
The QuickGrow API reference inside the product: an endpoint navigator beside the request and response detail for a single endpoint.
Reference

The reference is part of the product.

Request and response shapes, headers and error codes live in a browsable reference rather than a PDF that goes stale. The account-level API your clients use is documented in the same place.

  • Every success is wrapped in { data: … }; every failure in { error: { code, message } }
  • Hosted checkout and top-up links can carry your own colour and logo
  • GET /branding returns the Powered by QuickGrow name, logo and channel icons

Two different jobs, two different keys.

Most integrations only need the account-level API. Reach for the Partner API when you are creating and running accounts that belong to other businesses.

The account-level API is documented at /features/api. The commercial terms for running client accounts are at /solutions/agencies.
Account APIPartner API
Who it is forOne business sending its own messagesA platform running accounts for its clients
Credentialmk_ account keypk_ partner key plus a request signature
How you get itSelf-serve in the dashboardArranged with us
Creates accountsNoYes
Sends messagesYesYes
Reads another accountNoOnly accounts you provisioned
Suspend, resume, eraseNoYes
Billing and reconciliationNoYes

Before you write the integration.

What happens if my provisioning call times out and I retry?

Nothing bad. The account is keyed on your partner id together with your own externalId, and that pair is unique in the database. The retry returns the existing account with created:false. Two workers racing the same call resolve the same way. Only the first call returns an API key, so store it when you get it.

Can the key you mint for my client touch their billing?

No. The key created during provisioning is scoped to sending and reading messages, reading and writing contacts, and reading templates. Billing, account settings and team management are outside its scope, so a leaked key cannot buy anything or change who has access.

Do you email my client a password?

Never. We store a random hash so the login exists but has no known password, and mail them a set-password link that expires after seven days. They choose their own password the first time they sign in.

Tell us what you are building on top.

Partner keys are issued by hand, with the scopes and source addresses you need. Send us the shape of your integration and we will set it up.