Skip to content
Splashify Pro
Docs

Endpoints

Available for accounts in India.

All paths are relative to:

text
https://apis.splashifypro.com/api/v1/reseller-api

Every endpoint below gives you the scope it needs, the JSON body it expects, and a real example response. The fields in each example are what the endpoint actually returns. If something is not in the example, it is not sent.

Conventions

Read these once and the rest of the page is predictable.

Success envelope. Every response carries success. Read it rather than inferring from the status code:

json
{ "success": true, "clients": [] }

Failure envelope. Always the same shape:

json
{
  "success": false,
  "message": "You don't have permission to perform this action.",
  "missing_permission": "clients:billing"
}

Money is a plain number in rupees: 1250.5 means ₹1,250.50. Never a string, never paise.

Timestamps come in three forms, and which one you get depends on the endpoint:

A zero timestamp means "never": 0 in epoch form, "0001-01-01T00:00:00Z" in RFC 3339 form. Treat both as null.

IDs are UUID strings.

Omitted fields are deliberate. A money field your key is not scoped for is left out of the JSON rather than sent as 0. Test for presence, not for zero.

New fields will appear. We add them without warning; we do not remove or repurpose existing ones. Parse defensively.

Account

GET /me

Scope: none. Start here. It confirms the key works and tells you what it can do.

No request body.

json
{
  "success": true,
  "reseller": {
    "reseller_id": "0f5d2c9e-1a7b-4c3d-8e6f-5a4b3c2d1e0f",
    "first_name": "Rohan",
    "last_name": "Kapoor",
    "company_name": "Brightway Digital",
    "email": "[email protected]",
    "mobile": "9876500001",
    "country_code": "+91",
    "gst_no": "",
    "gst_treatment": "",
    "zoho_customer_id": "",
    "status": "active",
    "created_at": "2026-02-11T06:20:41Z",
    "totp_enabled": true,
    "profile_picture_url": "https://cdn.splashifypro.com/…",
    "products": ["app", "rcs"],
    "is_owner": false,
    "wallet_balance": 18450.25
  }
}

products is what your account sells. Gate your own UI on it exactly as our panel does. is_owner is always false for an API key: a key acts for the account, never as the owner.

wallet_balance is present only with wallet:view.

Dashboard

GET /dashboard/stats

Scope: none for the counts. markup:view adds profit, wallet:view adds balances.

No request body.

json
{
  "success": true,
  "stats": {
    "total_clients": 142,
    "app_clients": 118,
    "rcs_clients": 24,
    "active_clients": 96,
    "inactive_clients": 22,
    "activity_tracked_clients": 118,
    "active_subscriptions": 88,
    "new_clients_this_month": 7,
    "open_tickets": 3,
    "pending_tasks": 5,
    "total_messages_sent": 1284310,

    "subscription_profit": 41200,
    "message_profit": 9863.4,

    "wallet_balance": 18450.25,
    "held_credit": 4200,
    "available_to_give": 14250.25
  }
}

The last two blocks are the scoped ones. Without markup:view there is no subscription_profit or message_profit key at all; without wallet:view, no wallet_balance, held_credit or available_to_give.

activity_tracked_clients is the denominator for active/inactive. RCS clients are excluded because that tenancy has no session data. So active_clients + inactive_clients not adding up to total_clients is expected, not a bug.

GET /dashboard/trends

Scope: none for volume. markup:view fills in profit.

No request body. Returns the last 30 days, oldest first, with no gaps: a day with no activity is present with zeros.

json
{
  "success": true,
  "trends": {
    "messages": [
      { "date": "2026-07-26", "count": 4120, "profit": 312.4 },
      { "date": "2026-07-27", "count": 0, "profit": 0 }
    ],
    "clients": [
      { "date": "2026-07-26", "count": 2 }
    ],
    "wallet": [
      { "date": "2026-07-26", "amount": 5000 }
    ]
  }
}

Without markup:view, profit is 0 on every point rather than absent, so the series keeps its shape, so your chart needs no branch.

WhatsApp clients

Requires the app product.

GET /clients

Scope: clients:view

No request body.

json
{
  "success": true,
  "clients": [
    {
      "user_id": "9f2c77aa-2b1e-4a3d-9d10-6d0a41c2f8e1",
      "first_name": "Aarav",
      "last_name": "Mehta",
      "display_name": "Aarav Mehta",
      "company_name": "Acme Boutique",
      "email": "[email protected]",
      "mobile": "9876501234",
      "country_code": "+91",
      "email_verified": true,
      "mobile_verified": true,
      "profile_completed": true,
      "is_taxable": false,
      "gst_no": "",
      "gst_treatment": "",
      "website": "https://acmeboutique.in",
      "timezone": "Asia/Kolkata",
      "country": "India",
      "state": "West Bengal",
      "company_size": "11-50",
      "industry": "Retail",
      "zoho_customer_id": "",
      "status": "active",
      "created_at": 1750921241000,
      "updated_at": 1756014000000,
      "joined_at": 1750921241000,
      "waba_id": "123456789012345",
      "phone_number_id": "109876543210987",
      "waba_phone_number": "+919876501234",
      "trial_ends_at": 0,
      "account_manager": {
        "member_id": "3b8e91d4-7c22-4f1a-9b03-118d2a5e77c9",
        "name": "Priya Nair",
        "email": "[email protected]",
        "phone": "9876500102",
        "avatar": "",
        "is_owner": false
      },
      "plan_name": "Growth",
      "plan_status": "active",
      "plan_expires_at": 1761292800000
    }
  ]
}

account_manager is null when nobody is assigned. Test for presence rather than for a blank name.

GET /clients/{user_id}

Scope: clients:view

No request body. The same client object as above, plus WhatsApp Business Account detail:

json
{
  "success": true,
  "client": { "user_id": "9f2c77aa-…", "display_name": "Aarav Mehta", "…": "…" },
  "waba": {
    "waba_id": "123456789012345",
    "phone_number_id": "109876543210987",
    "phone_number": "+919876501234",
    "verified_name": "Acme Boutique",
    "quality_rating": "GREEN",
    "messaging_limit": "TIER_10K",
    "waba_status": "CONNECTED",
    "platform_type": "CLOUD_API",
    "throughput_level": "STANDARD",
    "waba_name": "Acme Boutique",
    "waba_currency": "INR",
    "business_verification_status": "verified",
    "profile_about": "Shop with us",
    "profile_description": "Online retail",
    "profile_address": "Kolkata, West Bengal",
    "profile_email": "[email protected]",
    "profile_vertical": "RETAIL",
    "profile_websites": ["https://acmeboutique.in"],
    "profile_picture_url": "https://cdn.splashifypro.com/…",
    "last_synced_at": 1756014000000,
    "created_at": 1750921241000,
    "updated_at": 1756014000000
  }
}

waba is null when the client has not connected WhatsApp. Mid-signup it comes back partial and says so:

json
{
  "waba_status": "in_progress",
  "signup_event": "PHONE_NUMBER_ADDED",
  "partial": true
}

POST /clients/{user_id}/plan

Scope: clients:billing. This spends your wallet.

Request body:

json
{ "plan_id": "d41c0a7e-3f52-4a1b-9c88-2e6f0b19aa34" }

Response:

json
{ "success": true, "message": "Plan assigned successfully" }

Errors worth handling:

json
{ "success": false, "message": "Insufficient wallet balance" }
json
{ "success": false, "message": "plan_id is required" }

Call /plan/estimate first if you want to show the cost before committing.

POST /clients/{user_id}/plan/estimate

Scope: clients:billing. Charges nothing. It runs the same proration arithmetic POST /plan would.

Request body:

json
{ "plan_id": "d41c0a7e-3f52-4a1b-9c88-2e6f0b19aa34" }

Response:

json
{
  "success": true,
  "current_plan_name": "Starter",
  "days_remaining": 20,
  "prorated_credit": 133.33,
  "new_plan_name": "Growth",
  "new_plan_cycle": "monthly",
  "new_wholesale": 500,
  "new_selling_price": 899,
  "amount_to_deduct": 366.67,
  "reseller_balance": 18450.25,
  "sufficient_balance": true,
  "is_downgrade": false,
  "downgrade_reason": ""
}

new_wholesale is your cost and appears only with markup:view. amount_to_deduct is what leaves your wallet if you proceed.

There is no plan-removal endpoint.

To move a customer to a different plan, POST /clients/{user_id}/plan with the new plan_id. Assigning over an existing plan prorates the unused time into a credit. A bare removal would only throw that credit away, refund nothing, and leave no record of what the customer had been on.

If a plan genuinely has to be cleared, contact support.

POST /clients/{user_id}/status

Scope: clients:edit. Toggles between active and suspended.

No request body.

json
{ "success": true, "message": "Status updated successfully", "status": "suspended" }

Read status back rather than assuming which way it flipped.

POST /clients/{user_id}/wallet

Scope: clients:billing. Adds credit to the client's wallet. Your own balance is not cut now: the credit is held against it (it counts under held_credit in /dashboard/stats), and your wallet pays your cost as the client spends it. You can give at most your available_to_give.

Request body:

json
{ "amount": 500, "note": "August top-up" }

Response:

json
{ "success": true, "message": "Wallet credited successfully", "new_balance": 1750.5 }
json
{ "success": false, "message": "You can give up to 41850.00 right now. Your balance is 48250.00, and 6400.00 is already held by credit your clients have not spent yet." }

Not idempotent. If this times out, read the client back before retrying. A blind retry credits twice.

POST /clients/{user_id}/ad-credits

Scope: clients:billing. Same shape, different balance: Advertisement Credits rather than the message wallet.

json
{ "amount": 250, "note": "Campaign budget" }
json
{ "success": true, "message": "AI credits added successfully", "new_balance": 950 }

PATCH /clients/{user_id}/email-pricing

Scope: clients:billing

json
{ "tier": "tier_custom", "marketing_price": 0.05, "transactional_price": 0.03 }

default puts the client back on your default tier from GET /markup. The two prices are saved exactly as sent, so leaving them out clears any custom price. Custom prices below your own cost are refused:

json
{ "success": false, "message": "Custom prices must be >= ₹0.02 marketing / ₹0.01 transactional (your wholesale cost)" }

Success is deliberately bare:

json
{ "success": true }

GET /clients/{user_id}/invoices

Scope: clients:view

Optional ?sync=true forces a refresh from your Zoho Billing before answering. It is slower, so use it only when you have reason to think the cache is stale.

json
{
  "success": true,
  "zoho_active": true,
  "invoices": [
    {
      "invoice_id": "INV-000142",
      "invoice_number": "INV-000142",
      "date": "2026-08-01",
      "due_date": "2026-08-15",
      "total": 1060.82,
      "balance": 0,
      "status": "paid"
    }
  ]
}

When you have not connected Zoho Billing the endpoint still succeeds. Check zoho_active before telling a user they have no invoices:

json
{ "success": true, "invoices": [], "zoho_active": false }

GET /clients/{user_id}/rcs-price

Scope: clients:view

json
{
  "success": true,
  "effective_price": 0.35,
  "default_selling_price": 0.3,
  "is_custom": true,
  "minimum_selling_price": 0.22,
  "margin_per_message": 0.13
}

minimum_selling_price (your highest RCS cost: the dearest of your Promotional, Transactional and OTP costs) and margin_per_message need markup:view and are otherwise absent.

PUT /clients/{user_id}/rcs-price

Scope: clients:billing

json
{ "selling_price": 0.35 }
json
{
  "success": true,
  "message": "RCS price updated for this client",
  "selling_price": 0.35,
  "default_price": 0.3,
  "margin_per_message": 0.13
}

Send null to drop the override and return the client to your default price:

json
{ "selling_price": null }
json
{ "success": true, "message": "Price reset to your default" }

Below your own cost is refused, and the message names the floor:

json
{ "success": false, "message": "Price cannot be below your highest cost of ₹0.2200 per message" }

PUT /clients/{user_id}/account-manager

Scope: clients:edit

json
{ "member_id": "3b8e91d4-7c22-4f1a-9b03-118d2a5e77c9" }
json
{
  "success": true,
  "message": "Account manager assigned",
  "account_manager": {
    "member_id": "3b8e91d4-7c22-4f1a-9b03-118d2a5e77c9",
    "name": "Priya Nair",
    "email": "[email protected]",
    "phone": "9876500102",
    "avatar": "",
    "is_owner": false
  }
}

Send an empty member_id to unassign:

json
{ "member_id": "" }
json
{ "success": true, "message": "Account manager removed" }

DELETE /clients/{user_id}/trial

Scope: clients:trial_end, not clients:billing.

Ends the free trial immediately. Selling to a customer and cutting their trial short are different acts, so this costs its own scope.

No request body.

json
{ "success": true, "message": "Free trial ended" }

RCS clients

Requires the RCS product. A separate set of accounts from WhatsApp clients: different accounts, no shared data, and its own scopes.

GET /rcs-clients

Scope: rcs_clients:view

No request body.

json
{
  "success": true,
  "default_price": 0.3,
  "clients": [
    {
      "user_id": "b71a0c33-5d94-4e2a-88c1-0af7b6e39d52",
      "name": "Anil Verma",
      "email": "[email protected]",
      "phone": "9876502001",
      "company_name": "Metro Motors",
      "status": "active",
      "wallet_amount": 2400,
      "billing_mode": "prepaid",
      "bots_count": 3,
      "active_bots_count": 2,
      "rcs_price": 0.35,
      "has_custom_price": true,
      "joined_at": "2026-05-04T10:11:00Z",
      "created_at": "2026-05-04T10:11:00Z"
    }
  ]
}

POST /rcs-clients/{user_id}/wallet

Scope: rcs_clients:billing. Adds credit to the RCS client's wallet. Your own balance is not cut now: your wallet pays your cost per message as the client sends. You can give at most your available_to_give.

json
{ "amount": 1000 }
json
{ "success": true, "message": "Wallet credited. Your balance is charged per message as they send." }

POST /rcs-clients/{user_id}/status

Scope: rcs_clients:edit. Unlike the WhatsApp equivalent, this one is explicit rather than a toggle.

json
{ "status": "suspend" }

status must be "active" or "suspend".

json
{ "success": true, "message": "Client updated", "status": "suspend" }
json
{ "success": false, "message": "Status must be active or suspend" }

GET /rcs-clients/{user_id}/rcs-price

Scope: rcs_clients:view

json
{
  "success": true,
  "effective_price": 0.35,
  "default_selling_price": 0.3,
  "is_custom": true,
  "minimum_selling_price": 0.22,
  "margin_per_message": 0.13
}

PUT /rcs-clients/{user_id}/rcs-price

Scope: rcs_clients:billing. Same body and responses as the WhatsApp client pricing endpoint above.

json
{ "selling_price": 0.35 }

Plans

Requires the app product.

GET /plans

Scope: plans:view (implied by plans:manage)

No request body.

json
{
  "success": true,
  "plans": [
    {
      "reseller_id": "0f5d2c9e-1a7b-4c3d-8e6f-5a4b3c2d1e0f",
      "reseller_plan_id": "d41c0a7e-3f52-4a1b-9c88-2e6f0b19aa34",
      "base_plan_id": "7c19b8f0-4a21-4c3e-b8f7-5d2e19c0a441",
      "name": "Growth",
      "description": "For growing teams",
      "features": ["10,000 conversations", "5 team seats"],
      "billing_cycle": "monthly",
      "selling_price": 899,
      "wholesale_price": 500,
      "tax_name": "GST",
      "tax_percentage": 18,
      "marketing_markup_pct": 20,
      "utility_markup_pct": 15,
      "auth_markup_pct": 10,
      "zoho_product_id": "",
      "is_active": true,
      "is_customized": true,
      "sort_order": 2,
      "created_at": "2026-03-01T08:00:00Z",
      "updated_at": "2026-08-01T09:12:00Z"
    }
  ]
}

wholesale_price is absent entirely without markup:view, because it is your cost. is_customized distinguishes a plan you priced from a platform default you have not touched.

POST /plans

Scope: plans:manage

json
{
  "base_plan_id": "7c19b8f0-4a21-4c3e-b8f7-5d2e19c0a441",
  "name": "Growth",
  "description": "For growing teams",
  "features": ["10,000 conversations", "5 team seats"],
  "billing_cycle": "monthly",
  "selling_price": 899,
  "tax_name": "GST",
  "tax_percentage": 18,
  "zoho_product_id": "",
  "is_active": true,
  "sort_order": 2
}
json
{
  "success": true,
  "reseller_plan_id": "d41c0a7e-3f52-4a1b-9c88-2e6f0b19aa34",
  "message": "Plan created"
}

Pricing below cost is refused, and the response tells you the floor:

json
{ "success": false, "message": "selling_price must be >= wholesale_price", "wholesale_price": 500 }
json
{ "success": false, "message": "Wholesale price not set for this plan. Contact admin." }

GET /plans/{plan_id}

Scope: plans:view

json
{ "success": true, "plan": { "reseller_plan_id": "d41c0a7e-…", "name": "Growth", "…": "…" } }

PUT /plans/{plan_id}

Scope: plans:manage

json
{
  "base_plan_id": "7c19b8f0-4a21-4c3e-b8f7-5d2e19c0a441",
  "name": "Growth",
  "selling_price": 949,
  "tax_name": "GST",
  "tax_percentage": 18,
  "marketing_markup_pct": 22,
  "utility_markup_pct": 15,
  "auth_markup_pct": 10,
  "zoho_product_id": ""
}
json
{ "success": true, "message": "Plan updated" }

Editing a platform default for the first time creates your own copy of it, and the response says so. Store the new id:

json
{
  "success": true,
  "reseller_plan_id": "f80b2c11-9a44-4d18-b5e0-71c3a6f2b8d7",
  "message": "Plan customized"
}

DELETE /plans/{plan_id}

Scope: plans:manage

No request body.

json
{ "success": true, "message": "Plan deleted" }

A plan in use cannot be deleted:

json
{ "success": false, "message": "Cannot delete plan because one or more clients are currently subscribed to it." }

Add-ons

GET /addons

Scope: addons:view (implied)

json
{
  "success": true,
  "addons": [
    {
      "reseller_id": "0f5d2c9e-1a7b-4c3d-8e6f-5a4b3c2d1e0f",
      "reseller_addon_id": "2ac71e55-0b3f-4c92-8a17-9d5e2f6b1c04",
      "base_addon_id": "91d4f0b2-6d13-4a70-9c55-3f1b8e2d4a09",
      "name": "Extra agent seat",
      "description": "One additional team seat",
      "type": "seat",
      "billing_cycle": "monthly",
      "unit_label": "seat",
      "max_quantity": 20,
      "selling_price": 199,
      "wholesale_price": 120,
      "tax_name": "GST",
      "tax_percentage": 18,
      "zoho_addon_id": "",
      "is_active": true,
      "sort_order": 1,
      "created_at": "2026-03-01T08:00:00Z",
      "updated_at": "2026-08-01T09:12:00Z"
    }
  ]
}

POST /addons

Scope: addons:manage

json
{
  "base_addon_id": "91d4f0b2-6d13-4a70-9c55-3f1b8e2d4a09",
  "name": "Extra agent seat",
  "description": "One additional team seat",
  "type": "seat",
  "billing_cycle": "monthly",
  "unit_label": "seat",
  "max_quantity": 20,
  "selling_price": 199,
  "tax_name": "GST",
  "tax_percentage": 18,
  "zoho_addon_id": "",
  "is_active": true,
  "sort_order": 1
}
json
{
  "success": true,
  "reseller_addon_id": "2ac71e55-0b3f-4c92-8a17-9d5e2f6b1c04",
  "message": "Add-on created"
}

PUT /addons/{addon_id}

Scope: addons:manage

json
{
  "name": "Extra agent seat",
  "description": "One additional team seat",
  "selling_price": 219,
  "tax_name": "GST",
  "tax_percentage": 18,
  "zoho_addon_id": "",
  "is_active": true,
  "sort_order": 1
}
json
{ "success": true, "message": "Addon updated" }

DELETE /addons/{addon_id}

Scope: addons:manage

json
{ "success": true, "message": "Addon deleted" }

Coupons

GET /coupons

Scope: coupons:view (implied)

json
{
  "success": true,
  "coupons": [
    {
      "id": "5e2c88a1-3b70-4d29-9e14-c07a5b2f6d18",
      "code": "DIWALI25",
      "description": "Festive offer",
      "discount_type": "percentage",
      "discount_value": 25,
      "plan_ids": ["d41c0a7e-3f52-4a1b-9c88-2e6f0b19aa34"],
      "billing_cycles": ["yearly"],
      "max_uses": 100,
      "uses_count": 14,
      "expires_at": "2026-11-15T23:59:59Z",
      "is_active": true,
      "created_at": "2026-08-01T09:00:00Z",
      "updated_at": "2026-08-01T09:00:00Z"
    }
  ]
}

The coupon's identifier is id, not coupon_id. It is the value to put in the path for update and delete.

POST /coupons

Scope: coupons:manage

json
{
  "code": "DIWALI25",
  "description": "Festive offer",
  "discount_type": "percentage",
  "discount_value": 25,
  "plan_ids": ["d41c0a7e-3f52-4a1b-9c88-2e6f0b19aa34"],
  "billing_cycles": ["yearly"],
  "max_uses": 100,
  "expires_at": "2026-11-15T23:59:59Z",
  "is_active": true
}
json
{ "success": true, "message": "Coupon created" }
json
{ "success": false, "message": "discount_type must be 'percentage' or 'fixed'" }
json
{ "success": false, "message": "expires_at must be RFC3339" }

PUT /coupons/{coupon_id}

Scope: coupons:manage. Same body as create.

json
{ "success": true, "message": "Coupon updated" }

DELETE /coupons/{coupon_id}

Scope: coupons:manage

json
{ "success": true, "message": "Coupon deleted" }

Pricing

These endpoints return your wholesale cost and margin. Scope keys accordingly.

GET /markup

Scope: markup:view (implied)

No request body.

json
{
  "success": true,
  "markup": {
    "whatsapp": {
      "enabled": true,
      "marketing_pct": 20,
      "utility_pct": 15,
      "auth_pct": 15
    },
    "whatsapp_cost": {
      "marketing": 0.89,
      "utility": 0.13,
      "authentication": 0.13,
      "service": 0.13
    },
    "rcs": {
      "wholesale_price_per_message": 0.15,
      "markup_enabled": true,
      "markup_pct": 33.33
    },
    "email": {
      "reseller_marketing_wholesale": 0.02,
      "reseller_transactional_wholesale": 0.01,
      "marketing_markup": 0,
      "transactional_markup": 0,
      "default_tier": "tier_1k_10k",
      "tier_pricing": [
        { "tier_key": "tier_1k_10k", "marketing_price": 0.05, "transactional_price": 0.03 }
      ]
    },
    "whatsapp_trial": {
      "enabled": true,
      "marketing_markup_pct": 25,
      "utility_markup_pct": 15,
      "auth_markup_pct": 15
    },
    "wallet": { "tax_name": "IGST", "tax_percentage": 18 },
    "voice_ai": { "wholesale_per_minute": 2, "markup_per_minute": 1.5, "customer_pays": 3.5 },
    "ai": {
      "chat_agent": { "unit": "million_tokens", "wholesale": 30, "markup": 10, "customer_pays": 40 },
      "chat_agent_backup": { "unit": "million_tokens", "wholesale": 65, "markup": 10, "customer_pays": 75, "markup_of": "chat_agent" },
      "chat_agent_backup_output": { "unit": "million_tokens", "wholesale": 330, "markup": 10, "customer_pays": 340, "markup_of": "chat_agent" },
      "template_generation": { "unit": "generation", "wholesale": 1, "markup": 1, "customer_pays": 2 },
      "flow_generation": { "unit": "generation", "wholesale": 2.5, "markup": 1.5, "customer_pays": 4 },
      "ctwa_copy_generation": { "unit": "generation", "wholesale": 1.5, "markup": 1, "customer_pays": 2.5 },
      "ctwa_lead_scoring": { "unit": "conversation", "wholesale": 0.25, "markup": 0.25, "customer_pays": 0.5 },
      "ctwa_ads_coach": { "unit": "analysis", "wholesale": 2.5, "markup": 1.5, "customer_pays": 4 },
      "ctwa_image_gen": { "unit": "image", "wholesale": 6, "markup": 4, "customer_pays": 10 },
      "inbox_ai_suggest": { "unit": "generation", "wholesale": 0.25, "markup": 0.25, "customer_pays": 0.5 },
      "inbox_ai_summary": { "unit": "generation", "wholesale": 0.5, "markup": 0.5, "customer_pays": 1 },
      "inbox_ai_rewrite": { "unit": "generation", "wholesale": 0.25, "markup": 0.25, "customer_pays": 0.5 },
      "ai_media_reading": { "unit": "million_tokens", "wholesale": 350, "markup": 150, "customer_pays": 500 }
    }
  }
}

whatsapp is your account-wide WhatsApp markup, used only for clients whose plan has no markup of its own. The panel sets WhatsApp markup per plan instead (see PUT /plans/{plan_id}).

In ai, chat_agent_backup and chat_agent_backup_output are the rates for the tokens a backup AI model read and wrote when the main one could not answer. They have no markup of their own: markup_of names the row whose markup they carry, so your chat agent markup is added to them too. ai_media_reading is AI reading documents (PDF, Word, Excel), images and voice notes, per million tokens.

PUT /markup

Scope: markup:manage

The body is flat. Only what you send is saved; anything you leave out keeps its current value. Some fields are saved one by one, others in groups:

For the WhatsApp, Free trial and CTWA AI groups, send every field of the group. A field you leave out of a group you did send is saved as false or 0. (RCS keeps the one you leave out.)

json
{
  "wallet_tax_name": "IGST",
  "wallet_tax_percentage": 18,
  "voice_ai_markup_per_minute": 1.5,
  "chat_agent_markup_per_mtok": 10,
  "template_gen_markup": 1,
  "flow_gen_markup": 1.5,
  "inbox_ai_suggest_markup": 0.25,
  "inbox_ai_summary_markup": 0.5,
  "inbox_ai_rewrite_markup": 0.25,
  "media_reading_markup_per_mtok": 150,

  "trial_markup_enabled": true,
  "trial_marketing_markup_pct": 25,
  "trial_utility_markup_pct": 15,
  "trial_auth_markup_pct": 15,

  "email_default_tier": "tier_1k_10k",
  "email_tier_pricing": [
    { "tier_key": "tier_1k_10k", "marketing_price": 0.05, "transactional_price": 0.03 }
  ]
}
json
{ "success": true, "message": "Markup settings saved" }

Sending all three of marketing_markup_pct, utility_markup_pct and auth_markup_pct also copies them onto every one of your plans, replacing each plan's own WhatsApp markup. Leave them out unless that is what you want.

Percentages must be between 0 and 100. Below-cost email pricing is refused, naming the tier and the floor.

GET /rcs-pricing

Scope: markup:view (implied)

json
{
  "success": true,
  "wholesale_price": 0.15,
  "default_selling_price": 0.2,
  "margin_per_message": 0.05,
  "minimum_selling_price": 0.15,
  "cost_by_type": [
    { "category": "promotional", "label": "Promotional", "cost": 0.15, "app_cost": 0.15, "app_cost_is_standard": false },
    { "category": "transactional", "label": "Transactional", "cost": 0.12, "app_cost": 0.12, "app_cost_is_standard": false },
    { "category": "otp", "label": "OTP", "cost": 0.11, "app_cost": 0.11, "app_cost_is_standard": false }
  ],
  "per_type_set": true
}

cost_by_type is what one RCS message of each type costs you. cost is a message from an RCS panel client, app_cost one from an app client. The type comes from the bot that sends the message.

Until Splashify Pro sets a cost for each type, per_type_set is false: every RCS panel message costs wholesale_price, and an app message costs our standard rate for its type (app_cost_is_standard is true). Once the types are set, cost and app_cost are the same.

minimum_selling_price is your highest RCS cost: the dearest of the three cost values. One default price covers every type, so a lower price would lose money on some messages, and PUT /rcs-pricing refuses it.

wholesale_price is the base your default price is worked out from, and margin_per_message is default_selling_price minus it. It does not move when your cost per type changes, so neither does what your clients pay.

PUT /rcs-pricing

Scope: markup:manage. Sets the default price every RCS client pays unless they have their own price.

json
{ "selling_price": 0.2 }
json
{
  "success": true,
  "message": "RCS price updated",
  "selling_price": 0.2,
  "markup_pct": 33.33,
  "margin_per_message": 0.05
}
json
{ "success": false, "message": "Your RCS cost has not been set yet. Ask Splashify Pro to set it before pricing." }
json
{ "success": false, "message": "Selling price cannot be below your highest cost of ₹0.1500 per message" }

GET /sms-pricing

Scope: markup:view (implied). Requires the app product.

json
{
  "success": true,
  "markup_pct": 25,
  "pricing": [
    { "category": "promotional", "label": "Promotional", "cost_per_part": 0.15, "cost_is_wholesale": true, "base_per_part": 0.18, "selling_per_part": 0.225 },
    { "category": "transactional", "label": "Transactional", "cost_per_part": 0.2, "cost_is_wholesale": false, "base_per_part": 0.2, "selling_per_part": 0.25 },
    { "category": "service_implicit", "label": "Service (implicit)", "cost_per_part": 0.2, "cost_is_wholesale": false, "base_per_part": 0.2, "selling_per_part": 0.25 },
    { "category": "service_explicit", "label": "Service (explicit)", "cost_per_part": 0.2, "cost_is_wholesale": false, "base_per_part": 0.2, "selling_per_part": 0.25 }
  ],
  "note": "Price per SMS part. Where your cost is our standard rate, a customer with their own price set by Splashify Pro uses that price as your cost instead."
}

One row per DLT type. The template decides the type of each SMS. All prices are per SMS part.

PUT /sms-pricing

Scope: markup:manage. Requires the app product. One percentage for every SMS type, from 0 to 500.

json
{ "markup_pct": 30 }
json
{ "success": true, "message": "SMS markup updated", "markup_pct": 30 }
json
{ "success": false, "message": "Markup must be between 0 and 500 percent." }

GET /cost-changes

Scope: markup:view (implied)

Splashify Pro can change your RCS and SMS costs at any time. Each change is a notice with the types that moved, their old and new price, newest first. You get the changes of the last 30 days that you have not dismissed, at most 5. A new cost applies to messages sent after the change.

json
{
  "success": true,
  "changes": [
    {
      "change_id": "6f1c2a40-a0b1-11f1-8000-0242ac120002",
      "product": "rcs",
      "name": "RCS",
      "unit": "per message",
      "lines": [
        { "type": "promotional", "label": "Promotional", "old": 0.15, "new": 0.14 },
        { "type": "otp", "label": "OTP", "old": 0.15, "new": 0.11 }
      ],
      "changed_at": "2026-10-03T09:30:00Z"
    }
  ]
}

product is rcs or sms, and unit says what one price is for: per message or per SMS part. An old of 0 means the type had no cost of its own before. On the first RCS change, if you sell both the app and the RCS panel, each type can come twice, with label naming the surface, for example Promotional, RCS panel and Promotional, app.

POST /cost-changes/{change_id}/dismiss

Scope: markup:view (implied). Hides one notice. No request body.

json
{ "success": true }

GET /trial-config

Scope: none.

json
{ "success": true, "trial_enabled": true, "trial_days": 7, "active_trial_clients": 6 }

PUT /trial-config

Scope: plans:manage

json
{ "enabled": false, "end_active_trials": true }
json
{
  "success": true,
  "message": "Free trial disabled",
  "trial_enabled": false,
  "trials_ended": 6
}

trials_ended is how many customers you just moved off trial. Read it before telling anyone the change was harmless.

Money

GET /wallet

Scope: wallet:view (implied)

json
{
  "success": true,
  "wallet": {
    "balance": 18450.25,
    "total_recharged": 250000,
    "updated_at": "2026-08-23T18:02:11Z"
  }
}

GET /wallet/transactions

Scope: wallet:view (implied). Newest first.

json
{
  "success": true,
  "transactions": [
    {
      "transaction_id": "b0f1c2d3-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
      "type": "debit",
      "amount": 500,
      "note": "Plan assigned to Aarav Mehta",
      "credited_by": "system",
      "created_at": "2026-08-23T11:04:22Z"
    }
  ]
}

type is one of recharge, debit, credit, consumption or consumption_refund. Money leaves your wallet on debit (a plan or add-on cost) and consumption (your cost for what your clients used). The others add money. amount is always positive: the direction lives in type, so do not expect negative numbers.

GET /subscription

Scope: none for the unpaid-invoice warning; subscription:view for the subscription itself and the invoice history.

json
{
  "success": true,
  "unpaid_invoice": {
    "invoice_id": "7e10b3a2-6c48-4b91-a0d2-5f83e1c7b9a4",
    "zoho_invoice_number": "INV-DEMO-0219",
    "total": 5900,
    "due_date": "2026-08-28",
    "status": "pending"
  },
  "subscription": {
    "items": [{ "sku_name": "Reseller platform fee", "amount": 5000 }],
    "billing_cycle": "monthly",
    "billing_day": 1,
    "next_billing_date": "2026-09-01",
    "status": "active",
    "notes": "",
    "monthly_total": 5000,
    "created_at": "2026-02-11T06:20:41Z",
    "updated_at": "2026-08-01T00:05:00Z"
  },
  "invoices": [
    {
      "invoice_id": "7e10b3a2-6c48-4b91-a0d2-5f83e1c7b9a4",
      "zoho_invoice_id": "",
      "zoho_invoice_number": "INV-DEMO-0219",
      "period_start": "2026-08-01",
      "period_end": "2026-08-31",
      "issue_date": "2026-08-01",
      "due_date": "2026-08-28",
      "extended_until": "",
      "items": [{ "sku_name": "Reseller platform fee", "amount": 5000 }],
      "subtotal": 5000,
      "tax_amount": 900,
      "total": 5900,
      "status": "pending",
      "paid_at": null,
      "payment_id": "",
      "created_at": "2026-08-01T00:05:00Z"
    }
  ]
}

Invoice status is pending, overdue or paid. unpaid_invoice is null when you are paid up. It is returned without subscription:view on purpose, so an integration can warn about an overdue account without being scoped for billing detail. Without that scope, subscription and invoices are absent entirely.

Topping up your own wallet is not in the API. It goes through a payment flow that needs a browser.

Support tickets

GET /tickets

Scope: tickets:view (implied)

json
{
  "success": true,
  "scope_all": false,
  "tickets": [
    {
      "ticket_id": "c4a71f90-2d63-4e07-b1a8-6f95c3d0e2b7",
      "created_at": "2026-08-20T07:41:00Z",
      "updated_at": "2026-08-22T10:02:00Z",
      "user_id": "9f2c77aa-2b1e-4a3d-9d10-6d0a41c2f8e1",
      "user_name": "Aarav Mehta",
      "user_email": "[email protected]",
      "title": "Template rejected",
      "status": "open",
      "category": "general",
      "priority": "high",
      "assigned_to": "3b8e91d4-7c22-4f1a-9b03-118d2a5e77c9",
      "assigned_to_name": "Priya Nair"
    }
  ]
}

scope_all tells you whether the list is everything (true) or only what the key may see. assigned_to is null when unassigned.

GET /tickets/{ticket_id}

Scope: tickets:view

json
{
  "success": true,
  "ticket": {
    "ticket_id": "c4a71f90-2d63-4e07-b1a8-6f95c3d0e2b7",
    "user_id": "9f2c77aa-2b1e-4a3d-9d10-6d0a41c2f8e1",
    "user_name": "Aarav Mehta",
    "user_email": "[email protected]",
    "title": "Template rejected",
    "description": "My template was rejected without a reason.",
    "category": "general",
    "priority": "high",
    "status": "open",
    "admin_note": "",
    "created_at": "2026-08-20T07:41:00Z",
    "updated_at": "2026-08-22T10:02:00Z",
    "closed_at": null,
    "forwarded_to_admin": false,
    "forwarded_at": null
  },
  "replies": [
    {
      "reply_id": "aa31d0c7-8b52-4f16-9c30-2e7d15a4b8f9",
      "message": "Could you share the template name?",
      "sender_type": "reseller",
      "sender_id": "3b8e91d4-7c22-4f1a-9b03-118d2a5e77c9",
      "created_at": "2026-08-20T08:15:00Z"
    }
  ]
}

sender_type is user (your client), reseller (you or your team) or admin (Splashify Pro support, on a forwarded ticket). category is general, bug, billing, account or feature_request.

POST /tickets/{ticket_id}/reply

Scope: tickets:manage

json
{ "message": "We have re-submitted the template for you." }
json
{ "success": true, "message": "Reply sent" }

POST /tickets/{ticket_id}/close

Scope: tickets:manage. No request body.

json
{ "success": true, "message": "Ticket closed" }

POST /tickets/{ticket_id}/reopen

Scope: tickets:manage. No request body.

json
{ "success": true, "message": "Ticket reopened" }

POST /tickets/{ticket_id}/assign

Scope: tickets:view_all

json
{ "member_id": "3b8e91d4-7c22-4f1a-9b03-118d2a5e77c9" }
json
{ "success": true, "message": "Ticket assigned", "assigned_to_name": "Priya Nair" }

An empty member_id unassigns:

json
{ "member_id": "" }
json
{ "success": true, "message": "Ticket unassigned" }

Tasks

GET /tasks

Scope: none. Optional filters: ?status=open and ?assignee_id={uuid}.

json
{
  "success": true,
  "scope_all": true,
  "tasks": [
    {
      "task_id": "d90a12bc-4f37-4a58-9e21-7c0b3d5a6e14",
      "title": "Follow up on Acme Boutique renewal",
      "description": "Plan expires in 5 days",
      "assignee_id": "3b8e91d4-7c22-4f1a-9b03-118d2a5e77c9",
      "assignee_name": "Priya Nair",
      "client_id": "9f2c77aa-2b1e-4a3d-9d10-6d0a41c2f8e1",
      "status": "open",
      "priority": "high",
      "due_date": 1756944000000,
      "created_at": "2026-08-21T09:00:00Z",
      "updated_at": "2026-08-21T09:00:00Z"
    }
  ]
}

A key always sees every task (scope_all: true), because it acts for the account rather than for one person.

POST /tasks

Scope: tasks:assign

json
{
  "title": "Follow up on Acme Boutique renewal",
  "description": "Plan expires in 5 days",
  "assignee_id": "3b8e91d4-7c22-4f1a-9b03-118d2a5e77c9",
  "client_id": "9f2c77aa-2b1e-4a3d-9d10-6d0a41c2f8e1",
  "priority": "high",
  "due_date": 1756944000000
}
json
{
  "success": true,
  "message": "Task created",
  "task": { "task_id": "d90a12bc-4f37-4a58-9e21-7c0b3d5a6e14", "status": "open", "…": "…" }
}

PATCH /tasks/{task_id}

Scope: none to change status; tasks:assign to change anything else.

Every field is optional. Send only what you are changing:

json
{ "status": "done" }
json
{
  "title": "Renewal call",
  "description": "Call before Friday",
  "assignee_id": "3b8e91d4-7c22-4f1a-9b03-118d2a5e77c9",
  "priority": "medium",
  "due_date": 1757030400000
}
json
{
  "success": true,
  "message": "Task updated",
  "task": { "task_id": "d90a12bc-…", "status": "done", "…": "…" }
}

DELETE /tasks/{task_id}

Scope: tasks:assign. No request body.

json
{ "success": true, "message": "Task deleted" }

Branding

GET /branding

Scope: branding:view (implied)

json
{
  "success": true,
  "branding": {
    "brand_name": "Brightway",
    "brand_color": "#0F766E",
    "logo_url": "https://cdn.splashifypro.com/…",
    "favicon_url": "https://cdn.splashifypro.com/…",
    "app_qr_url": "",
    "app_logo_url": "",
    "support_email": "[email protected]",
    "support_phone": "+91 98765 00100",
    "link_schedule_demo": "https://brightway.in/book-a-demo",
    "link_setup_guide": "",
    "link_tutorials": "",
    "link_postman_collection": "",
    "link_terms": "https://brightway.in/terms",
    "link_privacy": "https://brightway.in/privacy",
    "onesignal_app_id": "",
    "onesignal_api_key": "",
    "updated_at": "2026-08-10T12:00:00Z"
  }
}

PUT /branding

Scope: branding:manage

json
{
  "brand_name": "Brightway",
  "brand_color": "#0F766E",
  "support_email": "[email protected]",
  "support_phone": "+91 98765 00100",
  "link_schedule_demo": "https://brightway.in/book-a-demo",
  "link_setup_guide": "",
  "link_tutorials": "",
  "link_postman_collection": "",
  "link_terms": "https://brightway.in/terms",
  "link_privacy": "https://brightway.in/privacy",
  "onesignal_app_id": "",
  "onesignal_api_key": "",
  "app_logo_url": ""
}
json
{ "success": true, "message": "Branding updated" }

Images (logo, favicon, app QR) are uploaded through the panel rather than the API. They are file uploads with server-side processing.

Domains

GET /domain/status

Scope: domain:view (implied). Requires the app product.

Nothing connected yet:

json
{ "success": true, "connected": false }

Connected. domain.cname_record is exactly what to put in your DNS:

json
{
  "success": true,
  "connected": true,
  "domain": {
    "custom_domain": "app.brightway.in",
    "verification_token": "splashify-verify-0f5d2c9e",
    "dns_verified": true,
    "dns_verified_at": "2026-06-02T09:11:00Z",
    "status": "verified",
    "ssl_auto_renew": true,
    "ssl_status": "active",
    "ssl_issued_at": null,
    "ssl_expiry": null,
    "saas": true,
    "cname_record": {
      "type": "CNAME",
      "name": "app",
      "target": "app.splashifypro.com",
      "proxy": "DNS only (grey cloud)",
      "ttl": 300
    },
    "created_at": "2026-06-02T09:00:00Z",
    "updated_at": "2026-06-02T09:20:00Z"
  }
}

status is pending until the record is found and the certificate is issued, then verified. Show cname_record as it is rather than hard-coding the target. It can differ between accounts and change when we move hosting.

GET /domain/rcs/status

Scope: domain:view (implied). Requires the RCS product. The domain for your RCS panel:

json
{
  "success": true,
  "connected": true,
  "domain": {
    "rcs_domain": "rcs.brightway.in",
    "status": "pending",
    "ssl_status": "pending",
    "dns_verified": false,
    "saas": true,
    "cname_record": {
      "type": "CNAME",
      "name": "rcs",
      "target": "app.splashifypro.com",
      "proxy": "DNS only (grey cloud)",
      "ttl": 300
    }
  }
}

Integrations

GET /integrations

Scope: integrations:view (implied)

Credentials are never returned, only whether something is configured, plus the non-secret parts you need in order to display it.

json
{
  "success": true,
  "google": {
    "configured": true,
    "client_id": "1234-abc.apps.googleusercontent.com",
    "updated_at": "2026-07-01T10:00:00Z"
  },
  "smtp": {
    "configured": true,
    "host": "mail.brightway.in",
    "port": 587,
    "username": "[email protected]",
    "from_name": "Brightway",
    "from_email": "[email protected]",
    "is_active": true,
    "updated_at": "2026-07-01T10:00:00Z"
  },
  "whatsapp": {
    "configured": true,
    "phone_number_id": "109876543210987",
    "template_name": "welcome_reseller",
    "language_code": "en",
    "is_active": true,
    "updated_at": "2026-07-01T10:00:00Z"
  },
  "live_chat": {
    "configured": false,
    "widget_code": "",
    "is_active": false,
    "updated_at": null
  }
}

There is no SMTP password, no Google client secret and no API token in this payload, and no endpoint that returns them.

Product gates

Two families of endpoints depend on what your account carries rather than on scopes:

  • /clients/*, /plans/*, /addons/*, /coupons/*, /sms-pricing, /domain/status need the app product
  • /rcs-clients/*, /domain/rcs/status need the RCS product

Asking for one you do not carry returns 403:

json
{ "success": false, "message": "Your account does not include this product.", "missing_product": "rcs" }

No scope grants a product. Contact support to have one added.