Quickstart

This guide gets you from zero to a working Saperly line fast.

The goal is simple: provision one line, receive one inbound interaction, and confirm you can control the response path.

This quickstart takes about 5 minutes for hosted mode, 15 minutes for webhook mode.

Before you start

You need:

  • a Saperly account
  • an API key
  • a public HTTPS URL if you are using webhook mode

Base URL:

$https://saperly.com/api/v1

Auth header:

$Authorization: Bearer sk_live_...

Paste this into your AI coding agent

If you use Claude Code, Cursor, or any AI coding assistant, paste this prompt and it will set everything up:

Copy this entire block into your agent. It has everything needed to give your AI a phone number.

I want to set up Saperly to give my AI agent a phone number.
API base: https://saperly.com/api/v1
Skills file: https://saperly.com/skills.md
Docs: https://docs.saperly.com/quickstart
Steps:
1. Install MCP server: npx @saperly/mcp
2. Create a hosted line:
POST /v1/lines { "name": "my agent", "mode": "hosted", "system_prompt": "You are a helpful assistant." }
3. Make a test call:
POST /v1/calls { "line_id": "<from step 2>", "to_number": "+1YOURPHONE" }
4. Answer your phone, you'll hear the AI agent.

Option A: fastest possible setup, hosted mode

Hosted mode is the easiest onboarding path because you do not need to run a webhook server first.

1. Create a line

$curl -X POST https://saperly.com/api/v1/lines \
> -H "Authorization: Bearer sk_live_..." \
> -H "Content-Type: application/json" \
> -d '{
> "name": "Front desk",
> "mode": "hosted",
> "system_prompt": "You are a friendly receptionist for Acme Corp. Answer questions about hours, location, and pricing. Keep responses short.",
> "begin_message": "Thanks for calling Acme Corp. How can I help?",
> "voice": "21m00Tcm4TlvDq8ikWAM"
> }'

2. Save the returned phone number and line ID

You will use the phone number to place a real test call.

3. Call the number

Verify:

  • the call connects
  • the AI answers
  • the opening message plays
  • the transcript appears on the call record
If the call does not connect, check: (1) your API key is sk_live_ not sk_test_, (2) your balance is above 0, (3) the line status is active. Run GET /api/v1/lines to verify.

4. Inspect the created line

$curl https://saperly.com/api/v1/lines \
> -H "Authorization: Bearer sk_live_..."

5. Inspect the resulting call

$curl https://saperly.com/api/v1/calls \
> -H "Authorization: Bearer sk_live_..."

If this worked, you already have a production-shaped happy path.

Option B: webhook mode, your backend decides responses

Use this when your app already has an agent runtime or business logic you need to keep in your own stack.

1. Create a line

$curl -X POST https://saperly.com/api/v1/lines \
> -H "Authorization: Bearer sk_live_..." \
> -H "Content-Type: application/json" \
> -d '{
> "name": "Webhook line",
> "mode": "webhook",
> "webhook_url": "https://your-server.com/saperly-webhook",
> "status_callback_url": "https://your-server.com/saperly-status"
> }'

2. Add the smallest useful webhook

1import express from "express";
2
3const app = express();
4app.use(express.json());
5
6app.post("/saperly-webhook", (req, res) => {
7 if (req.body.event !== "message") {
8 return res.status(200).end();
9 }
10
11 return res.json({
12 text: `You said: ${req.body.text}`,
13 });
14});
15
16app.listen(3000);

3. Call the number

Say one sentence. You should hear the echo response spoken back.

If your webhook returns non-200 or takes longer than 10 seconds, the caller hears silence. Check GET /api/v1/webhooks/deliveries to see what happened.

4. What your webhook receives

1{
2 "event": "message",
3 "call_id": "2f450669-cb10-488e-9c50-c8fc1ad8116e",
4 "timestamp": 1711900005000,
5 "text": "hi, i need help with my order",
6 "context": [
7 { "role": "caller", "text": "hello?", "timestamp": 1711899995000 }
8 ]
9}

5. What your webhook should return

1{
2 "text": "Sure, what is your order number?"
3}

6. Verify the signature

Every outbound webhook is signed. Verify before you trust the body.

1import express from "express";
2import { verifyWebhook } from "@saperly/sdk";
3
4const app = express();
5app.use(express.raw({ type: "application/json" }));
6
7app.post("/saperly-webhook", async (req, res) => {
8 const rawBody = req.body.toString("utf8");
9 const { line_id } = JSON.parse(rawBody);
10 const secret = await lookupSecretForLine(line_id); // your secret store
11 const result = verifyWebhook(rawBody, secret, req.headers);
12 if (!result.valid) return res.status(401).json({ error: result.reason });
13
14 const event = JSON.parse(rawBody);
15 if (event.event !== "message") return res.status(200).end();
16 return res.json({ text: `You said: ${event.text}` });
17});

See webhook mode — signature verification for the full headers table, replay-defense rules, and secret rotation flow.

Make one outbound call

If the line has compliance_enabled: true, outbound calls require consent first.

Compliance is off by default. When you enable it on a line, outbound calls without an active consent record return 403 with consent_required. See Compliance and Consent to decide whether to turn it on.
$curl -X POST https://saperly.com/api/v1/consent \
> -H "Authorization: Bearer sk_live_..." \
> -H "Content-Type: application/json" \
> -d '{
> "line_id": "LINE_ID",
> "phone_number": "+14155551234",
> "consent_type": "explicit_outbound",
> "source": "signup_form"
> }'

2. Start the call

$curl -X POST https://saperly.com/api/v1/calls \
> -H "Authorization: Bearer sk_live_..." \
> -H "Content-Type: application/json" \
> -d '{
> "line_id": "LINE_ID",
> "to_number": "+14155551234"
> }'

Send one SMS reply

Inbound SMS arrives at your webhook as an sms_received event:

1{
2 "event": "sms_received",
3 "line_id": "line_abc123",
4 "from_number": "+15551234567",
5 "to_number": "+15559876543",
6 "message": "Hello",
7 "message_sid": "SMxxx",
8 "timestamp": 1711900000000
9}
SMS replies must be sent within 24 hours of the last inbound message. After 24 hours, the API returns 403 with conversation_window_expired.

Send a reply:

$curl -X POST https://saperly.com/api/v1/messages \
> -H "Authorization: Bearer sk_live_..." \
> -H "Content-Type: application/json" \
> -d '{
> "line_id": "LINE_ID",
> "to": "+14155551234",
> "text": "Thanks for reaching out. We can help with that."
> }'

What to verify before you keep building

1

Line provisioned

GET /api/v1/lines returns your line with status active.

2

Inbound call works

Call your number, hear the AI or webhook response.

3

Transcript populated

GET /api/v1/calls/{id} shows a transcript array.

4

Balance healthy

GET /api/v1/billing/balance shows the expected credits.

5

Webhook logs clean

GET /api/v1/webhooks/deliveries shows no failures.

7

Outbound call works

POST /api/v1/calls with a consented number succeeds.

8

SMS reply works

Receive an inbound SMS and send a reply within the 24 hour window.

Common first-day mistakes

  • Using the wrong host. Use https://saperly.com/api/v1.
  • Forgetting consent before outbound calling.
  • Returning plain text instead of JSON in webhook mode.
  • Taking too long to reply to a message event.
  • Mixing up hosted mode and webhook mode fields on the same line.

Next reads