# Build Click-to-Call Into Your App or CRM in 15 Minutes (API Guide)

> Click-to-call API guide for India: place an agent-first bridged call with POST /v1/click-to-call, receive signed Call Events, verify Standard Webhooks signatures, handle errors and write the recording and summary back to your CRM.

Source: https://edesy.in/blog/click-to-call-api-guide-india-2026

A **click-to-call API** connects two phones on request. With Edesy you send one `POST /v1/click-to-call` with the agent's and the customer's numbers. Edesy rings the **agent first**, and when they answer it dials the customer from your Indian business number and bridges the call. The call is recorded by default, and every stage comes back to you as a signed webhook. It costs **Rs 1.50 per minute**.

Here's the whole integration, from API key to a call result in your CRM.

## What you need

| Item | Where |
|---|---|
| An Edesy account | [voice-app.edesy.in](https://voice-app.edesy.in). New accounts get Rs 20 welcome credit |
| An Indian number | Buy one in the voice app after KYC. It is the caller ID |
| An API key with `calls:write` | Voice app, API keys. See [API keys](https://edesy.in/docs/voice-api/account/workspaces-team-and-api-keys) |
| An HTTPS endpoint | To receive Call Events |

## Minute 0-3: place your first call

```bash
curl -X POST https://voice-api.edesy.in/v1/click-to-call \
  -H "Authorization: Bearer $EDESY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent":    { "phone": "+9198XXXXXXXX" },
    "customer": { "phone": "+9197XXXXXXXX" },
    "correlation": { "lead_id": "L-1042", "crm_call_id": "call_8812" }
  }'
```

Your phone (the agent) rings. Answer it, and the customer's phone rings. The API returned straight away:

```json
{ "data": { "call_id": "...", "status": "initiated" } }
```

The request fields:

| Field | Required | Notes |
|---|---|---|
| `agent.phone` | yes | Rung first |
| `customer.phone` | yes | Dialled after the agent answers; must differ from the agent |
| `caller_id` | no | An active number of your workspace; default is your first active number |
| `record` | no | Default `true` |
| `max_duration_sec` | no | 10 to 7,200, default 3,600 |
| `correlation` | no | Up to 10 of your own keys, echoed on every event |

Full reference: [click-to-call docs](https://edesy.in/docs/voice-api/calls/click-to-call).

## Minute 3-6: register a Call Events endpoint

```bash
curl -X POST https://voice-api.edesy.in/v1/webhooks/subscriptions \
  -H "Authorization: Bearer $EDESY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://app.example.com/edesy/events",
    "format": "edesy.call.v1"
  }'
```

No `event_types` means all eight events. The response includes a `whsec_` signing secret, shown only this once. Store it as `EDESY_WHSEC`.

## Minute 6-12: receive and verify events

Every delivery carries Standard Webhooks headers: `webhook-id`, `webhook-timestamp` and `webhook-signature`. The signature is `v1,` plus a base64 HMAC-SHA256 of `id.timestamp.body`, keyed with the base64-decoded part of your secret after `whsec_`.

```python
import base64, hashlib, hmac, json, os, time
from flask import Flask, request

app = Flask(__name__)
SECRET = os.environ["EDESY_WHSEC"]
KEY = base64.b64decode(SECRET[len("whsec_"):])

def verified(req) -> bool:
    msg_id = req.headers.get("webhook-id", "")
    ts = req.headers.get("webhook-timestamp", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        return False
    signed = f"{msg_id}.{ts}.".encode() + req.get_data()
    want = base64.b64encode(hmac.new(KEY, signed, hashlib.sha256).digest()).decode()
    return any(s.startswith("v1,") and hmac.compare_digest(s[3:], want)
               for s in req.headers.get("webhook-signature", "").split())

@app.post("/edesy/events")
def events():
    if not verified(request):
        return "", 401
    ev = json.loads(request.get_data())
    save_event(ev)          # upsert by ev["call"]["id"]; skip a webhook-id you've seen
    return "", 204
```

Three rules make the handler robust:

1. **Upsert by `call.id`.** Each event carries a full snapshot of the call, so arrival order doesn't matter. Keep the furthest `status` you've seen.
2. **De-duplicate on `webhook-id`.** Retries reuse it.
3. **Answer within 5 seconds.** Queue slow work. Failed deliveries are retried; see [retries and replay](https://edesy.in/docs/voice-api/webhooks/retries-and-replay).

## Minute 12-15: write the result back

The events you'll use most:

| Event | Write to your CRM |
|---|---|
| `call.completed` | `call.outcome` (answered, no_answer, busy, failed, canceled, voicemail), `duration_sec`, `talk_sec` |
| `recording.ready` | `recording.url` |
| `call.analyzed` | `analysis.summary`, `analysis.action_items`, suggested `analysis.disposition` |

Your `correlation.lead_id` comes back on each one, so you know which record to update. For what to do with the summary, see [automatic call summaries in your CRM](https://edesy.in/blog/automatic-call-summaries-in-crm-2026). The event schema is in the [Call Events v1 docs](https://edesy.in/docs/voice-api/webhooks/call-events).

## Add the button

In your app or CRM, the button calls **your** backend, never Edesy directly, so the API key stays on the server:

```js
// Browser
await fetch('/api/call', { method: 'POST', body: JSON.stringify({ leadId }) });

// Server (Node)
app.post('/api/call', async (req, res) => {
  const lead = await db.leads.find(req.body.leadId);
  const agent = await db.users.find(req.user.id);
  const r = await fetch('https://voice-api.edesy.in/v1/click-to-call', {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.EDESY_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      agent: { phone: agent.phone },
      customer: { phone: lead.phone },
      correlation: { lead_id: lead.id },
    }),
  });
  res.status(r.status).json(await r.json());
});
```

To end a call from your UI, `POST /v1/click-to-call/{call_id}/hangup`.

## Errors to handle

| Status | Code | What to show the user |
|---|---|---|
| 400 | `validation_error` | Which field is wrong (`details.field`), for example a non-Indian number |
| 402 | `insufficient_balance` | Ask an admin to top up the wallet |
| 409 | `NO_CALLER_ID` | The workspace needs an active number |
| 429 | | A call limit was reached; try again shortly |

## Using LeadSquared or Zoho CRM?

You don't need to write code. Edesy's [LeadSquared](https://edesy.in/crm-calling-integrations/leadsquared) and [Zoho CRM](https://edesy.in/crm-calling-integrations/zoho-crm) connectors use this same API and write calls into the CRM for you. HubSpot, Salesforce, Freshworks, Bitrix24 and Pipedrive have no connector yet, so this guide is the way to go for them.

Only need a "Call Now" link on your website? The free [click-to-call button generator](https://edesy.in/tools/click-to-call-button-generator) makes one without an API.

## Pricing

**Rs 1.50 per minute**, 60-second pulse, plus number rental. No separate fee for Call Events, transcripts or summaries. Prices exclude 18% GST. See the [click-to-call API page](https://edesy.in/voice-api/click-to-call-api).

## FAQ

### What does the click-to-call API do?

POST /v1/click-to-call rings the agent's phone first, showing your Edesy number. When the agent answers, Edesy dials the customer and bridges the two. The call is recorded by default and reported through Call Events.

### Which numbers does it support?

Indian numbers only, for both the agent and the customer. The caller ID must be an active number in your workspace.

### How do I get the call result?

Register a webhook subscription with format edesy.call.v1. You receive call.initiated, call.ringing, call.answered and call.completed, then recording.ready, transcript.ready and call.analyzed for recorded calls.

### How much does it cost?

Rs 1.50 per minute with a 60-second pulse, plus number rental, excluding 18% GST. Call Events, transcripts and summaries have no separate fee.

### Can I use it with HubSpot or Salesforce?

Yes, from your own backend. There is no native HubSpot or Salesforce connector, but a CRM button or workflow can call your server, which calls the API and writes the Call Events back.
