Ellidiya Business API
Send order updates, one-time codes and offers to your customers on Ellidiya — and get their replies — from your own systems or the software you already use. Version 1.
- Base URL:
https://api.ellidiya.com/api/v1· JSON in, JSON out · HTTPS only. - Get a key in ecom.ellidiya.com → Developers once your business is verified.
- Customers see your verified business name. They never see your API, and you never see their account — only a customer id, and their number if you messaged that number first.
The rules (why customers trust Ellidiya)
- Templates start conversations. To message someone who hasn't written to you in the last 24 hours, use a template our team approved.
- The 24-hour window. When a customer writes to you, you can reply with free text for 24 hours. These replies are free.
- Customers are in control. Anyone can stop your messages with one tap. After that, sending to them fails with
opted_out. If they open your chat again, they're back. - Daily limits grow with trust. Each business starts with 250 new conversations a day. Businesses whose customers don't block or report them move up (1,000 → 10,000 → 100,000).
- Too many blocks or reports pause sending automatically while our team reviews.
- Marketing waits for daytime. Marketing templates go out only from 9 am to 9 pm India time. Single sends outside those hours get
quiet_hours; batches wait and go out at 9 am by themselves. - Only message people who expect to hear from you (customers who ordered, booked or agreed). Never buy lists.
Authentication
Send your key in the Authorization header on every request. Keep keys on your server — never in an app or web page.
Authorization: Bearer ek_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Up to 5 active live keys per business. Revoke a key any time in Developers; it stops working immediately.
Test keys (sandbox)
Try everything before your business is verified, for free:
- In the portal, Developers → Test numbers: add your own phone (it must have Ellidiya). We send a 6-digit code to that phone's Ellidiya chat; type it in to confirm. Up to 5 numbers.
- Developers → API keys: make a test key (
ek_test_…). Up to 2. - Call the API exactly as with a live key. Test messages reach only your confirmed test numbers, cost nothing, don't count against your daily limit, and can use templates still waiting for review. Up to 100 test messages a day.
Webhooks work the same, and test messages carry "test": true. Before verification, they come from "Your name · test"; only your test numbers can open that chat. Sending to any other number fails with not_test_number. GET /me tells you "mode": "test".
Quick start
curl https://api.ellidiya.com/api/v1/messages \
-H "Authorization: Bearer $ELLIDIYA_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+919876543210",
"template": { "name": "order_shipped", "params": ["Arjun", "MS-1042"] },
"client_ref": "order-1042"
}'
// 201 Created
{ "ok": true, "message": {
"id": "msg_9137", "direction": "out", "status": "sent", "kind": "template",
"customer": { "id": "cus_h2k8x0q1z9a7b3c4", "name": "Arjun", "phone": "+919876543210", "opted_out": false },
"text": "Hi Arjun, your order MS-1042 is on its way.", "cost_paise": 15, "client_ref": "order-1042",
"created_at": "2026-10-15T09:12:44+00:00", "updated_at": "2026-10-15T09:12:44+00:00" } }
Send a message
to | Indian mobile number: +919876543210, 9876543210 or 09876543210. Or use customer. |
customer | A customer id (cus_…) from a webhook or an earlier message. Use it for customers who found you through your link. |
template | { "name": "order_shipped", "params": ["…"], "language": "en" } — an approved template. params fill {{1}}, {{2}}… in order; the count must match. language is optional. |
text | Free text (up to 4,000 characters) — only within 24 hours of the customer's last message. |
client_ref | Optional, recommended. Your own id (order number…). If you send the same client_ref again, we return the first message instead of sending twice — so retries are safe. |
buttons | Optional, with text: up to 8 buttons (see Buttons). Templates bring their own. |
list / cards | Optional, with text: a list picker or up to 10 result cards. |
media | Instead of text: {"id": "med_…", "caption": "…"} — a photo, video, voice note or document you uploaded (Photos and files). Within the 24-hour window. |
about | Optional, on any message: {"label": "Sharma Salon", "ref": "L-88412"} — shown as "about Sharma Salon" under your name (About line). |
fallback_after | Optional: seconds (60 to 604800). If the message is still not delivered by then (phone off, app not opened), we send you message.undelivered once, so you can reach the person another way (SMS, email). No polling needed. |
Returns 201 with the message, or 200 with "duplicate": true for a repeated client_ref.
Buttons
Three kinds, on templates (up to 3) and on free-text replies (up to 8). Labels: up to 25 characters.
"Track" or {"type": "reply", "text": "Pizza", "data": "cat:pizza"} | Quick reply. When tapped, it is sent as the customer's message and you get message.received with "button": "cat:pizza" (the data, or the label). |
{"type": "url", "text": "View listing", "url": "https://your-site.in/l/42"} | Opens a page. It must be an https:// page on the website in your business details (or a subdomain of it). Nothing comes back to you. |
{"type": "call", "text": "Call us", "phone": "+919876543210"} | Calls your confirmed business number (portal: Business number). Other numbers are refused. |
{"type": "location", "text": "Share location"} | Asks the customer to share where they are (in the app, and in website chat, where the browser asks the visitor first). If they do, you get message.received with "kind": "location" and "location": {"lat", "lng", "accuracy_m", "label"}. |
{ "customer": "cus_h2k8x0q1z9a7b3c4", "text": "What are you looking for?",
"buttons": [ {"type": "reply", "text": "Pizza", "data": "cat:pizza"}, {"type": "reply", "text": "Salon", "data": "cat:salon"},
{"type": "url", "text": "Open the app", "url": "https://your-site.in/app"} ] }
Lists
For more choices than buttons can hold: one button that opens a list of up to 10 rows, in sections. The pick is sent as the customer's message, and you get message.received with "button" = the row's id and reply_to = your message.
{ "customer": "cus_h2k8x0q1z9a7b3c4", "text": "What are you looking for?",
"list": { "button": "Pick a category", "sections": [
{ "title": "Beauty", "rows": [ {"id": "cat:salon", "title": "Salon", "description": "Hair, beard, facial"}, {"id": "cat:spa", "title": "Spa"} ] },
{ "title": "Food", "rows": [ {"id": "cat:pizza", "title": "Pizza"} ] } ] } }
Button: up to 20 characters. Rows: title up to 24, description up to 72, id up to 64 and unique. Or skip sections: "list": {"button": "Choose", "rows": [...]}.
Result cards
Up to 10 cards in a row the customer swipes through, with the message text above them.
{ "customer": "cus_h2k8x0q1z9a7b3c4", "text": "Salons near you:",
"cards": [ { "title": "Sharma Salon", "subtitle": "MG Road · open till 9 pm", "image_url": "https://your-site.in/img/88412.jpg",
"rating": 4.4, "rating_count": 212, "lat": 12.975, "lng": 77.605,
"buttons": [ {"type": "call", "text": "Call", "phone": "080 2558 1234"}, {"type": "url", "text": "View", "url": "https://your-site.in/l/88412"} ] } ] }
- Photo:
https://, JPG, PNG or WebP, up to 2 MB. We fetch it once when you send and show it from Ellidiya, so customers' phones never contact your server. The same URL is reused for a week. - Directions appear by themselves when you give
latandlng. - Buttons: up to 2 per card.
callmay call any Indian number (the place on the card),urlmust be on your website,replycomes back asmessage.receivedwith itsdata. - Title up to 60 characters, subtitle up to 100. Cards can't be combined with
buttonsorlistin the same message.
About line
One verified sender can write about many places or things (listings, branches, orders). Add about to any message:
"about": { "label": "Sharma Salon", "ref": "L-88412" }
Customers see about Sharma Salon under your verified name. It is never shown as the sender, so your customers always know who wrote. The label (2 to 60 characters) can't say Ellidiya, official, verified, customer care, helpline, bank, UPI, KYC or OTP. ref is yours (up to 64): when the customer replies to that message or taps one of its buttons, message.received carries "reply_to": {"id": "msg_…", "client_ref": "…", "about": {"label", "ref"}}, so you know which listing they mean.
Photos and files
Upload as multipart field file (photos, videos, voice notes, PDF, Office, text, ZIP; same size limits as the app). We read the type from the file. Uploads are kept 30 days.
curl https://api.ellidiya.com/api/v1/media -H "Authorization: Bearer $ELLIDIYA_KEY" -F [email protected] { "ok": true, "media": { "id": "med_812", "kind": "image", "mime": "image/jpeg", "size": 84213, "expires_at": "…" } }
Then send {"customer": "cus_…", "media": {"id": "med_812", "caption": "Shop front"}} (within 24 hours of the customer's last message).
When a customer sends you a photo, voice note, video or document, message.received has "media": {"url": "/v1/media/msg_9142", "mime", "size", "name", "duration"} and text is the caption (or [image]). Download it with your key. Locations come as "location", shared contacts as "contact".
Consent and Mute offers
Every customer object carries what Ellidiya saw: "consent": {"source": "chat" | "link" | "button" | "signin" | "unblock", "at": "…"} (when they first wrote, opened your link, tapped a button, signed in on your website, or allowed you again) and "marketing": true | false. Keep your own record of consent too (for example the order or sign-up it came from).
Customers can tap Mute offers in your chat's menu: marketing templates to them then fail with marketing_muted, while utility and authentication messages still go. You get customer.marketing_muted and customer.marketing_unmuted.
Chat links
Your chat link is https://chat.ellidiya.com/b/<your handle> (see GET /me). Add ?ref= with your own code (letters, numbers, _ and -, up to 64) to know who opened it:
https://chat.ellidiya.com/b/nearfind?ref=u_88412_k3f9
When someone opens it, customer.started carries "ref": "u_88412_k3f9" with their customer id, every time they open a link with a ref. Use a one-time code that you issued, and check it on your side: anyone can change a link. Your number link (chat.ellidiya.com/<number>?ref=…) works the same.
Sign in with Ellidiya
Let people sign in to your website or app with their Ellidiya account. No password to remember, and the mobile number you get is already confirmed. Turn it on in the portal (Developers, Sign in with Ellidiya): add your exact return addresses and you get a client id (ecl_…). Before you are verified, only your test numbers can use it.
- Send the person (a link or a button "Sign in with Ellidiya") to:
https://chat.ellidiya.com/signin?client_id=ecl_…&redirect_uri=https://your-site.in/auth/ellidiya&state=RANDOM&scope=phone
state: a one-time random value you keep in their session.scope=phoneasks for their mobile number too; leave it out for name only. - Ellidiya opens (they sign in to Ellidiya first if needed) and shows your name, your website's address and what you get. They tap Continue, and may also allow your offers.
- We send them back to
redirect_uri?code=esc_…&state=…. Checkstate. If they tapped Cancel you get?error=access_denied&state=…. - From your server, swap the code (one use, 5 minutes):
curl https://api.ellidiya.com/api/v1/signin/token \
-H "Authorization: Bearer ek_live_…" -H "Content-Type: application/json" \
-d '{"code": "esc_…", "redirect_uri": "https://your-site.in/auth/ellidiya"}'
{ "ok": true, "scopes": ["profile", "phone"],
"customer": { "id": "cus_h2k8x0q1z9a7b3c4", "name": "Arjun", "phone": "+919876543210", "phone_verified": true,
"consent": { "source": "signin", "at": "…" }, "marketing": false, "opted_out": false } }
The customer.id is the same id your messages and webhooks use, so you can send order updates to the person who just signed in. People see and remove these sign-ins in Ellidiya (Settings, Privacy, Sites you signed in to); you then get customer.signin_removed. Removing it doesn't delete their account with you, but don't sign them in silently again.
Website chat
A chat bubble for your own website. Set it up in the portal (Website chat): pick colours and words, the websites allowed to show it, and which details visitors give (name, mobile, email, a consent tick-box). Then paste one line before </body>:
<script src="https://chat.ellidiya.com/w.js" data-key="wk_…" async></script>
It loads after the visitor's first tap, scroll or key (or after 8 seconds), so it never slows your page, and it doesn't load for automated browsers. Visitors chat without the app. Each website chat is a normal customer of your business: the same webhooks, the same POST /messages replies (text, buttons, lists, cards, about line), the same hand-off and Inbox. Customers from the website have "channel": "web". Website chats are live: you (and your bot) can write while the visitor is on your website, with the chat bubble on their screen. About a minute and a half after they leave, sending answers 403 visitor_left; if they come back, you can write again. Their history stays in the Inbox, and a returning visitor still sees it.
window.Ellidiya = window.Ellidiya || { q: [] }; // calls before the script loads are kept
Ellidiya.q.push(['setAttributes', { site: 'nearfind', listing_slug: 'mehta-sweets', utm_source: 'google' }]);
Ellidiya.q.push(['identify', { identifier: 'u_88412', name: 'Arjun', phone: '+919876543210', email: '[email protected]',
hash: '<hex HMAC-SHA256 of identifier with your identity secret>' }]);
// later: Ellidiya.open(), Ellidiya.close(), Ellidiya.toggle()
// open with a message typed in (the visitor reads it and taps send; it is never sent by itself):
Ellidiya.open({ text: 'I want to claim the listing Mehta Sweets (L-88412)', attributes: { intent: 'claim', listing_slug: 'mehta-sweets' } });
// any element with data-ellidiya-chat opens it; add data-ellidiya-text="..." to type a message in; ?chat=1 opens it on arrival
<button data-ellidiya-chat data-ellidiya-text="I want to claim this listing">Claim this listing</button>
Ellidiya.open(options?) | Opens the chat. options.text: a message typed in for the visitor (up to 2000 characters). options.attributes: same as setAttributes. |
Ellidiya.close() / Ellidiya.toggle() | Closes, or opens/closes. |
Ellidiya.setAttributes(object) | Up to 20 short key/values, shown to your team and sent in webhooks. Calling it again adds to (or changes) what was set. |
Ellidiya.identify({identifier, name?, phone?, email?, hash}) | A signed-in user; hash = hex HMAC-SHA256 of identifier with your identity secret. |
window.Ellidiya.q | Calls queued before the script loads, as ['open', {text: '…'}]; they run in order once it loads. |
- Attributes (up to 20, short text) go with the chat: shown to your team in the Inbox, in
lead.createdand in everymessage.receivedas"web": {"site", "page", "title", "attributes"}. - Identity: make the identity secret in the portal and keep it on your server. Compute
hash_hmac('sha256', $identifier, $secret)for the signed-in user (for example fromGET /chat/identity). A signed-in user skips the form and is the same customer on every device and on all your websites. A wrong hash is refused, and the visitor chats as a guest. - Photos and PDFs: visitors can send a photo or PDF (up to 10 MB); it arrives as
message.receivedwithmedia, like from the app. Photos and files you send (Photos and files) show in the website chat. - Continue in the Ellidiya app: the visitor can move the chat into the app (menu ⋮). The chat so far comes along, and you get
customer.startedwith"ref"= the website customer's id (cus_…), so you can link the two. In the app the chat is with your verified business account, so for a website with another brand you can switch this off (Website chat → that website → Offer "Continue in the Ellidiya app"). - Away: outside your business hours (portal, Team), or when nobody on your team is online and you have no webhook, the chat shows your away message and visitors must leave a name and mobile. Your team gets an alert.
- Spam: before the first message the visitor's browser solves a small sum (no puzzles to click); your team can block a visitor (and new chats from their network) from the Inbox.
- Which website:
"web"also carries"widget": {"key", "name"}, the website chat the visitor wrote from. - A different bot per website: in the portal (Website chat → a website → Own bot for this website) give that website its own webhook address. Its visitors' events go there, signed with that website's own secret, instead of your business webhook. Leave it empty to use the business webhook. Conversations from a website with its own bot start as
bot. - Events:
conversation.createdandlead.createdwhen a visitor starts a chat (name, mobile, email, site, page, attributes, consent time, identifier), thenmessage.receivedas usual. The portal lists leads and exports them as CSV.
Website visitors from your server
{ "name": "Arjun Shah", "phone": "+919876543210", "email": "[email protected]", "identifier": "u_88412",
"attributes": { "searches_left": 2, "plan": null } }
Update a website visitor after your own check (for example an OTP). Send only the fields you change; null removes an attribute (up to 20, values up to 200 characters, kept as text). identifier links the visitor to your own user id, so Ellidiya.identify() later finds the same chat (409 identifier_taken if another visitor has it). Answers with the customer and its "web" details. Ellidiya app users can't be changed this way (not_web): their name and number are their own.
The customer id (cus_…) stays the same on the same browser across closed tabs and new chats, and on every device once identified. It changes only if the visitor clears their browser's site data or uses a private window.
Inbox and hand-off to people
Each customer conversation has a state: bot (your system answers), waiting (handed to people, nobody has picked it up), open (a team member has it) or resolved. Your team works in the portal's Inbox (add people in Team; they see only the Inbox). Answers from the Inbox are free within 24 hours; after that they start again with a template.
Your team gets an alert when a customer needs a person (and, if you choose, for every new website chat): by email, and as a message in each person's own Ellidiya app. Each person turns these on or off in Team. The Inbox has quick replies for the whole team.
Without a webhook, every conversation starts as waiting. With one, it starts as bot, and every message.received carries "conversation": {"state", "assigned_to"}: when the state is waiting or open, your bot should stay quiet. A resolved conversation starts again when the customer writes.
{ "state": "waiting", "note": "Asked about a refund", "add_labels": ["refund"], "remove_labels": ["new"] }
waiting hands the customer to your team (the note shows in the Inbox), bot takes it back, resolved closes it. People pick conversations up in the Inbox. GET /conversations?state=waiting&label=refund lists conversations, newest activity first; GET /conversations/{customer_id} gives one. Every change sends conversation.updated.
- All fields are optional, but send at least one. A
noteon its own (up to 1000 characters) is a private note: your team sees it in the Inbox, the customer never does. - Labels:
add_labels,remove_labels, orlabelsto replace them all. Up to 10 per conversation, 1 to 30 letters, digits, spaces, - or _, kept in lower case. They show in the Inbox (your team can add and remove them too), and come inconversation.labelsin webhooks. - The conversation object has
"labels"and"handed_over"(it was handed to people at least once).
Chats answered only by your bot
In the portal (Team → Chats answered only by your bot) you can keep chats your bot handles alone private: your team doesn't see them in the Inbox until they are handed to people (then the whole chat shows, so they have the context). You can also have them deleted after 1 to 365 days without a new message: website chats completely (the visitor's window too), and your copy of chats with Ellidiya app users (they keep theirs). The customer id stays, so you still recognise a returning visitor. Chats that were ever handed to people are never deleted this way.
Send in bulk
Up to 1,000 messages per call; we queue them and send at your pace (up to 10 a second). Each item is exactly like a single send. Answers 202 right away.
{ "name": "Diwali offer", "messages": [
{ "to": "+919876543210", "template": { "name": "diwali_offer", "params": ["Arjun"] }, "client_ref": "diwali-1" },
{ "customer": "cus_h2k8x0q1z9a7b3c4", "template": { "name": "diwali_offer", "params": ["Bhavna"] }, "client_ref": "diwali-2" } ] }
{ "ok": true, "batch": { "id": "bat_42", "total": 2, "queued": 0, "sent": 1, "failed": 1, "done": true,
"failures": [ { "index": 1, "client_ref": "diwali-2", "code": "opted_out", "error": "This customer stopped messages from your business." } ] } }
Items waiting for your daily limit or a pause are retried every 10 minutes; marketing items wait for 9 am; anything not sent within 24 hours fails with expired. Statuses of sent messages arrive as webhooks, as usual.
Message status
Statuses move sent → delivered → read. read appears only if the customer shares read receipts. You also get each change as a message.status webhook.
Replies (polling)
If you can't receive webhooks, poll for replies. Results are oldest first; pass the returned next as after next time. Without after you get the last 7 days.
{ "ok": true, "messages": [ { "id": "msg_9140", "direction": "in", "status": "received", "text": "When will it arrive?", "customer": { "id": "cus_h2k8…", … } } ], "next": "msg_9140" }
Templates
Your approved templates with how many params each needs and the price. Create templates in the portal; our team reviews them (usually within a few hours). Categories:
- utility — orders, bookings, deliveries, account updates
- authentication — one-time codes
- marketing — offers, launches, news (approved more strictly)
Up to 3 buttons per template: quick replies, links to your website, or calls to your business number (see Buttons). When a customer taps a quick reply, you get message.received with "button": "Track". You get template.approved or template.rejected when we have reviewed it.
{ "name": "order_shipped", "category": "utility", "language": "en",
"body": "Hi {{1}}, your order {{2}} is on its way.", "buttons": ["Track"], "example": ["Arjun", "MS-1042"] }
Submits a template for review (201, status pending). Follow it with GET /templates?status=all, which also lists pending, rejected (with the reason) and paused ones. Remove one with DELETE /templates/{name}?language=en.
Your business
{ "ok": true, "business": { "id": 12, "name": "Mehta Sweets", "tier": 1, "new_conversations_limit_24h": 250, "new_conversations_used_24h": 37,
"paused": false, "paused_reason": null, "balance_paise": 98500, "link": "https://chat.ellidiya.com/b/mehta_sweets" } }
Set the webhook
{"url": "https://your-server/ellidiya"} (or "" to remove). Returns the signing secret. Partners use this to point a connected business's events to their own server.
Webhook events
Set an https address in the portal (or with POST /webhook). We POST JSON like this, with a 5-second timeout. Answer with any 2xx quickly and do the work afterwards.
Retries: if your server doesn't answer 2xx, we try again after 10 s, 1 min, 5 min, 30 min, 2 h, 6 h and 12 h. Every try carries the same id (evt_…) — skip ids you've already handled. You can see deliveries and resend failed ones in the portal.
{ "id": "evt_5521", "event": "message.received", "business_id": 12, "created": 1792060000,
"data": { "message": { "id": "msg_9140", "direction": "in", "text": "When will it arrive?", "button": null,
"customer": { "id": "cus_h2k8x0q1z9a7b3c4", "name": "Arjun", "phone": "+919876543210", "opted_out": false } } } }
message.received | A customer wrote to you or tapped a button. Opens the 24-hour window. |
message.status | One of your messages became delivered or read. |
message.undelivered | A message sent with fallback_after was still not delivered by then. Sent once; a later message.status can still follow. |
customer.started | Someone opened your chat link or QR. You can now message them using the customer id. With "ref" if the link had one (Chat links). |
customer.opted_out | The customer stopped your messages. Stop sending to them. |
customer.marketing_muted / customer.marketing_unmuted | The customer muted (or allowed again) your offers. Marketing templates to them fail while muted. |
customer.opted_in | The customer allowed your messages again. |
customer.signin_removed | The person removed your website from "Sites you signed in to" (Sign in with Ellidiya). |
conversation.created / lead.created | A visitor started a chat on your website (Website chat): {"conversation": {"customer", "channel": "web", "state"}} and {"lead": {"customer", "name", "phone", "email", "site", "page", "title", "attributes", "consent_at", "identifier"}}. |
conversation.updated | The conversation's state or assignee changed (Inbox): {"conversation": {"customer", "state", "assigned_to", "state_at", "last_message_at", "window_open"}}. |
template.approved / template.rejected / template.paused | Our review of one of your templates: {"template": {"name", "language", "category", "status", "reason"}}. |
balance.low | Your balance dropped below your warning amount (set in Credits; ₹50 by default): {"balance_paise", "threshold_paise"}. Once, until you top up. |
ping | Sent by "Send test event" in the portal. |
Verify the signature
Every request has X-Ellidiya-Signature: t=<unix time>,v1=<hex>. v1 is HMAC-SHA256 of "<t>.<raw body>" with your signing secret (whsec_…). Compare in constant time, use the raw body (before JSON parsing), and reject timestamps older than 5 minutes.
// Node.js
const crypto = require('crypto');
function verify(raw, header, secret) {
const m = /t=(\d+),v1=([0-9a-f]{64})/.exec(header || '');
if (!m || Math.abs(Date.now() / 1000 - m[1]) > 300) return false;
const want = crypto.createHmac('sha256', secret).update(m[1] + '.' + raw).digest('hex');
return crypto.timingSafeEqual(Buffer.from(want, 'hex'), Buffer.from(m[2], 'hex'));
}
// PHP
$raw = file_get_contents('php://input');
preg_match('/t=(\d+),v1=([0-9a-f]{64})/', $_SERVER['HTTP_X_ELLIDIYA_SIGNATURE'] ?? '', $m);
$ok = $m && abs(time() - (int) $m[1]) <= 300 && hash_equals(hash_hmac('sha256', $m[1].'.'.$raw, $secret), $m[2]);
# Python
import hmac, hashlib, re, time
def verify(raw: bytes, header: str, secret: str) -> bool:
m = re.search(r"t=(\d+),v1=([0-9a-f]{64})", header or "")
if not m or abs(time.time() - int(m[1])) > 300: return False
want = hmac.new(secret.encode(), m[1].encode() + b"." + raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, m[2])
For partners: Connect with Ellidiya
Build software that many businesses use? Sign up as a software partner, add your product details and redirect URLs, and we review you. Then each business connects in two clicks — no keys to copy:
- Your product opens
https://ecom.ellidiya.com/#connect?client_id=ecp_…&redirect_uri=https://your-app/callback&state=RANDOM(pop-up or same tab). - The business signs in (or signs up) and sees exactly what your product will be able to do. They click Allow.
- We send them to
redirect_uri?code=ecc_…&state=RANDOM. Checkstatematches. (If they cancel:?error=access_denied&state=….) - Your server swaps the code for that business's API key (below), and stores it encrypted.
The business can disconnect your product any time; its key then stops working (401). New businesses can connect before they're verified: GET /me shows their status, and sending works once verified.
Swap the code for the API key
{ "client_id": "ecp_…", "client_secret": "ecps_…", "code": "ecc_…", "redirect_uri": "https://your-app/callback" }
// 200
{ "ok": true, "api_key": "ek_live_…", "connection_id": "con_77", "business": { "id": 12, "name": "Mehta Sweets", "status": "verified" } }
Codes work once and expire after 10 minutes; redirect_uri must be the one used in step 1. Then use the key like any business key — including POST /webhook to send its events to you.
Your businesses
Send Authorization: Bearer ecps_… (your client secret). Lists every business connected to you, their status, and whether they disconnected.
Errors
Errors look like {"ok": false, "error": {"code": "outside_window", "message": "…"}}.
| HTTP | code | What to do |
|---|---|---|
| 401 | unauthorized | Key missing, wrong or revoked. |
| 402 | no_credits | Add credits in the portal. |
| 403 | not_verified | Business not verified, or suspended. |
| 403 | paused | Sending paused (quality or by our team). See the reason; talk to support. |
| 403 | opted_out | The customer stopped your messages. Don't retry. |
| 403 | outside_window | Use a template — the customer hasn't written in 24 hours. |
| 403 | visitor_left | A website visitor has left your website. Website chats are live; you can write again if they come back. |
| 403 | quiet_hours | Marketing templates go out 9 am to 9 pm India time. Send then, or use a batch (it waits). |
| 403 | not_test_number | Test keys send only to your confirmed test numbers. |
| 403 | marketing_muted | The customer muted your offers. Send utility messages only. |
| 404 | unknown_media | No upload with that id, or it is older than 30 days. |
| 404 | not_on_ellidiya | That number isn't on Ellidiya. Reach them another way. |
| 404 | unknown_template / unknown_customer / not_found | Check the name or id. |
| 422 | bad_number / bad_params / bad_text / bad_buttons / bad_fallback / bad_list / bad_cards / bad_about / bad_media / bad_state / too_large | Fix the request. |
| 400 | invalid_code | A sign-in (or partner) code that is wrong, already used, expired, or sent with a different redirect_uri. Send the person through sign-in again. |
| 429 | rate_limited / daily_limit | Slow down; or wait for your 24-hour new-conversation limit to free up. |
| 429 | lookup_limit | Too many numbers that aren't on Ellidiya today. To protect people's privacy, numbers you haven't messaged before are refused until tomorrow. |
Limits & pricing
- API: 1,200 calls a minute; sending: 600 messages a minute per business.
- New conversations (customers you message without them writing first) per 24 hours: 250 / 1,000 / 10,000 / 100,000 by tier.
- Numbers not on Ellidiya: up to half your daily new-conversation limit (at least 100) a day. Keep your contact lists clean.
- Test keys: 100 messages a day, only to up to 5 confirmed test numbers, free.
- Prices per template message: utility ₹0.15, authentication ₹0.15, marketing ₹0.50. Replies within 24 hours: free. New businesses get ₹100 to try it out.
SDKs
Single-file clients with retries (only where safe) and webhook verification:
- PHP — ellidiya-business.php (PHP 7.4+, curl)
- Node.js — ellidiya-business.js (Node 18+)
$ell = new EllidiyaBusiness(getenv('ELLIDIYA_KEY'));
$msg = $ell->sendTemplate('+919876543210', 'order_shipped', ['Arjun', 'MS-1042'], 'order-1042');
Moving from another provider
- Recreate your templates in the portal with the same names. Use
{{1}}-style blanks — usually no change needed. - Replace the send call with
POST /messages. Keep using your order id asclient_ref. - Point your incoming-message webhook to the same handler; map
message.receivedandmessage.status. Check the signature as above. - Customers who aren't on Ellidiya return
not_on_ellidiya— keep your SMS fallback for them, and invite them with your chat link.
Questions? Open a ticket in the portal. © Ellidiya · Made in India.