Endpoints
Available for accounts in India.
All paths are relative to:
https://apis.splashifypro.com/api/v1/reseller-apiEvery 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:
{ "success": true, "clients": [] }Failure envelope. Always the same shape:
{
"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.
{
"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.
{
"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.
{
"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.
{
"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:
{
"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:
{
"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:
{ "plan_id": "d41c0a7e-3f52-4a1b-9c88-2e6f0b19aa34" }Response:
{ "success": true, "message": "Plan assigned successfully" }Errors worth handling:
{ "success": false, "message": "Insufficient wallet balance" }{ "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:
{ "plan_id": "d41c0a7e-3f52-4a1b-9c88-2e6f0b19aa34" }Response:
{
"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}/planwith the newplan_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.
{ "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:
{ "amount": 500, "note": "August top-up" }Response:
{ "success": true, "message": "Wallet credited successfully", "new_balance": 1750.5 }{ "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.
{ "amount": 250, "note": "Campaign budget" }{ "success": true, "message": "AI credits added successfully", "new_balance": 950 }PATCH /clients/{user_id}/email-pricing
Scope: clients:billing
{ "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:
{ "success": false, "message": "Custom prices must be >= ₹0.02 marketing / ₹0.01 transactional (your wholesale cost)" }Success is deliberately bare:
{ "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.
{
"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:
{ "success": true, "invoices": [], "zoho_active": false }GET /clients/{user_id}/rcs-price
Scope: clients:view
{
"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
{ "selling_price": 0.35 }{
"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:
{ "selling_price": null }{ "success": true, "message": "Price reset to your default" }Below your own cost is refused, and the message names the floor:
{ "success": false, "message": "Price cannot be below your highest cost of ₹0.2200 per message" }PUT /clients/{user_id}/account-manager
Scope: clients:edit
{ "member_id": "3b8e91d4-7c22-4f1a-9b03-118d2a5e77c9" }{
"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:
{ "member_id": "" }{ "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.
{ "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.
{
"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.
{ "amount": 1000 }{ "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.
{ "status": "suspend" }status must be "active" or "suspend".
{ "success": true, "message": "Client updated", "status": "suspend" }{ "success": false, "message": "Status must be active or suspend" }GET /rcs-clients/{user_id}/rcs-price
Scope: rcs_clients:view
{
"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.
{ "selling_price": 0.35 }Plans
Requires the app product.
GET /plans
Scope: plans:view (implied by plans:manage)
No request body.
{
"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
{
"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
}{
"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:
{ "success": false, "message": "selling_price must be >= wholesale_price", "wholesale_price": 500 }{ "success": false, "message": "Wholesale price not set for this plan. Contact admin." }GET /plans/{plan_id}
Scope: plans:view
{ "success": true, "plan": { "reseller_plan_id": "d41c0a7e-…", "name": "Growth", "…": "…" } }PUT /plans/{plan_id}
Scope: plans:manage
{
"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": ""
}{ "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:
{
"success": true,
"reseller_plan_id": "f80b2c11-9a44-4d18-b5e0-71c3a6f2b8d7",
"message": "Plan customized"
}DELETE /plans/{plan_id}
Scope: plans:manage
No request body.
{ "success": true, "message": "Plan deleted" }A plan in use cannot be deleted:
{ "success": false, "message": "Cannot delete plan because one or more clients are currently subscribed to it." }Add-ons
GET /addons
Scope: addons:view (implied)
{
"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
{
"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
}{
"success": true,
"reseller_addon_id": "2ac71e55-0b3f-4c92-8a17-9d5e2f6b1c04",
"message": "Add-on created"
}PUT /addons/{addon_id}
Scope: addons:manage
{
"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
}{ "success": true, "message": "Addon updated" }DELETE /addons/{addon_id}
Scope: addons:manage
{ "success": true, "message": "Addon deleted" }Coupons
GET /coupons
Scope: coupons:view (implied)
{
"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
{
"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
}{ "success": true, "message": "Coupon created" }{ "success": false, "message": "discount_type must be 'percentage' or 'fixed'" }{ "success": false, "message": "expires_at must be RFC3339" }PUT /coupons/{coupon_id}
Scope: coupons:manage. Same body as create.
{ "success": true, "message": "Coupon updated" }DELETE /coupons/{coupon_id}
Scope: coupons:manage
{ "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.
{
"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.)
{
"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 }
]
}{ "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)
{
"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.
{ "selling_price": 0.2 }{
"success": true,
"message": "RCS price updated",
"selling_price": 0.2,
"markup_pct": 33.33,
"margin_per_message": 0.05
}{ "success": false, "message": "Your RCS cost has not been set yet. Ask Splashify Pro to set it before pricing." }{ "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.
{
"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.
{ "markup_pct": 30 }{ "success": true, "message": "SMS markup updated", "markup_pct": 30 }{ "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.
{
"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.
{ "success": true }GET /trial-config
Scope: none.
{ "success": true, "trial_enabled": true, "trial_days": 7, "active_trial_clients": 6 }PUT /trial-config
Scope: plans:manage
{ "enabled": false, "end_active_trials": true }{
"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)
{
"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.
{
"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.
{
"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)
{
"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
{
"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
{ "message": "We have re-submitted the template for you." }{ "success": true, "message": "Reply sent" }POST /tickets/{ticket_id}/close
Scope: tickets:manage. No request body.
{ "success": true, "message": "Ticket closed" }POST /tickets/{ticket_id}/reopen
Scope: tickets:manage. No request body.
{ "success": true, "message": "Ticket reopened" }POST /tickets/{ticket_id}/assign
Scope: tickets:view_all
{ "member_id": "3b8e91d4-7c22-4f1a-9b03-118d2a5e77c9" }{ "success": true, "message": "Ticket assigned", "assigned_to_name": "Priya Nair" }An empty member_id unassigns:
{ "member_id": "" }{ "success": true, "message": "Ticket unassigned" }Tasks
GET /tasks
Scope: none. Optional filters: ?status=open and ?assignee_id={uuid}.
{
"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
{
"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
}{
"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:
{ "status": "done" }{
"title": "Renewal call",
"description": "Call before Friday",
"assignee_id": "3b8e91d4-7c22-4f1a-9b03-118d2a5e77c9",
"priority": "medium",
"due_date": 1757030400000
}{
"success": true,
"message": "Task updated",
"task": { "task_id": "d90a12bc-…", "status": "done", "…": "…" }
}DELETE /tasks/{task_id}
Scope: tasks:assign. No request body.
{ "success": true, "message": "Task deleted" }Branding
GET /branding
Scope: branding:view (implied)
{
"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
{
"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": ""
}{ "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:
{ "success": true, "connected": false }Connected. domain.cname_record is exactly what to put in your DNS:
{
"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:
{
"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.
{
"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/statusneed the app product/rcs-clients/*,/domain/rcs/statusneed the RCS product
Asking for one you do not carry returns 403:
{ "success": false, "message": "Your account does not include this product.", "missing_product": "rcs" }No scope grants a product. Contact support to have one added.