lineverify
API/Overview
Get API key

Overview

The LineVerify API checks US phone numbers and email addresses in real time. A phone lookup returns the carrier, line type, spam risk, do-not-call status, lawsuit risk, the name on the line, best time to call and area code location. An email lookup tells you if mail to that address will arrive.

At a glance
Base URL
https://api.lineverification.com/v1
Speed
Under 800ms for 95% of requests
Uptime
99.9% uptime commitment
Rate limit
100 requests a minute
Phones
US numbers, as E.164 (+14155550123) or national (4155550123)
Emails
Any address, e.g. jane@example.com
Request
curl -X POST \
  https://api.lineverification.com/v1/lookup \
  -H "Authorization: Bearer lv_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"number": "+14155550123"}'
Response200 OK
{
  "type": "phone",
  "number": "+14155550123",
  "status": "valid",
  "line_type": "mobile",
  "carrier": "Verizon",
  "portable": true,
  "ported_date": "2021-04-12",
  "caller_name": "JOHN SMITH",
  "activity_score": 88,
  "spam_score": "low",
  "dnc_status": "clean",
  "dnc_list": null,
  "tcpa_risk": "low",
  "timezone": "Pacific (PT)",
  "location": "San Francisco, CA",
  "credits_used": 1,
  "credits_remaining": 4823
}

Authentication

Send your API key as a Bearer token in the Authorization header on every request. Create and revoke keys in Settings, under API keys. We show a new key once, so copy it when you make it.

Keys
lv_live_
Live. Real data, and each successful lookup uses a check.
lv_test_
Sandbox. Sample results in the same shape, no checks used.

Sandbox results are made up and no carrier is contacted. Each number or email always gets the same answer, and different ones get a mix of valid, invalid, risky and landline, so you can test every path.

Heads up

Never put your API key in browser code. Keep it in an environment variable on your server.

Request
curl https://api.lineverification.com/v1/lookup \
  -H "Authorization: Bearer lv_live_YOUR_KEY" \
  -H "Content-Type: application/json"
POST/v1/lookup

Single lookup

Send one phone number or one email address, get back everything we know about it. Each successful lookup uses 1 check. Fields we could not check come back as null.

What you send
number
A US phone number, as +14155550123 or 4155550123
email
Or an email address. Send one or the other
What you get back for a number
type
"phone"
number
The number in E.164 format
status
valid, invalid, risky or landline
line_type
mobile, landline, voip, toll-free or unknown
carrier
Who the number is with right now
portable
Switched carriers: true if the number has moved
ported_date
When it moved, if it did
caller_name
The name on the line
activity_score
0 to 100. Under 30 means the line is very likely dead
spam_score
low, medium or high
dnc_status
clean, or listed on a do-not-call list
dnc_list
Which list: federal, or a state like TX
tcpa_risk
Lawsuit risk: low, medium, high or critical
timezone
Best time to call, e.g. Pacific (PT)
location
Where the area code is, e.g. San Francisco, CA
What you get back for an email
type
"email"
email
The address, lowercased
status
valid, invalid or risky
domain
The part after the @
deliverability
deliverable, undeliverable, risky or unknown
inbox
What the mail server said, e.g. Mailbox exists
disposable
true for throwaway addresses
did_you_mean
A likely typo fix, e.g. gmail.com for gmial.com
On every lookup
credits_used
1, or 0 when the result is invalid or the key is a sandbox key
credits_remaining
Checks left on your account
Request
curl -X POST \
  https://api.lineverification.com/v1/lookup \
  -H "Authorization: Bearer lv_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"number": "+14155550123"}'
Response200 OK
{
  "type": "phone",
  "number": "+14155550123",
  "status": "valid",
  "line_type": "mobile",
  "carrier": "Verizon",
  "portable": true,
  "ported_date": "2021-04-12",
  "caller_name": "JOHN SMITH",
  "activity_score": 88,
  "spam_score": "low",
  "dnc_status": "clean",
  "dnc_list": null,
  "tcpa_risk": "low",
  "timezone": "Pacific (PT)",
  "location": "San Francisco, CA",
  "credits_used": 1,
  "credits_remaining": 4823
}
Email request
curl -X POST \
  https://api.lineverification.com/v1/lookup \
  -H "Authorization: Bearer lv_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "jane@example.com"}'
Email response200 OK
{
  "type": "email",
  "email": "jane@example.com",
  "status": "valid",
  "domain": "example.com",
  "deliverability": "deliverable",
  "inbox": "Mailbox exists",
  "disposable": false,
  "did_you_mean": null,
  "credits_used": 1,
  "credits_remaining": 4822
}
POST/v1/lookup/bulk

Bulk lookup

Send up to 500 phone numbers and email addresses in one request. Bulk lookups run in the background: you get a batch_id straight away, and the results go to your webhook. You can also fetch them with the batch id.

What you send
numbers
Phone numbers, in either format
emails
Email addresses. Up to 500 numbers and emails in total
webhook_url
Optional. An https URL we post the results to when the batch is done
What you get back
batch_id
Use it to fetch the results or match the webhook to this request
status
processing until the batch is done
total
How many contacts we will check. Repeats are checked once
credits_reserved
Checks held for this batch. 0 with a sandbox key
credits_remaining
Checks left after the hold
Heads up

If one entry can't be read, the whole request is rejected with a 400 or 422 and nothing is charged.

Request
curl -X POST \
  https://api.lineverification.com/v1/lookup/bulk \
  -H "Authorization: Bearer lv_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "numbers": ["+14155550123", "+16285550198", "+15105550147"],
    "emails": ["jane@example.com"],
    "webhook_url": "https://yourapp.com/webhook/lineverify"
  }'
Response202 Accepted
{
  "batch_id": "0b6f3c2e-8d1a-4f5b-9c7e-2a4d6e8f1b3c",
  "status": "processing",
  "total": 4,
  "credits_reserved": 4,
  "credits_remaining": 4819
}
GET/v1/batch/:id

Batch results

Check on a bulk lookup with its batch_id. While it runs you see how far it has got. Once it is done you get every result, in the same shape as a single lookup and in the order you sent them (numbers first, then emails).

What you get back
batch_id
The id you asked about
status
processing or done
total
How many contacts are in the batch
checked
How many are done so far
results
Every result once the batch is done, null until then
credits_remaining
Checks left on your account

If you sent a webhook_url, we post the finished batch to it as JSON: batch_id, status, the total credits_used, and the results, each with its own credits_used. Reply with any 2xx status. If your server doesn't answer, we try twice more, a few seconds apart.

Request
curl https://api.lineverification.com/v1/batch/BATCH_ID \
  -H "Authorization: Bearer lv_live_YOUR_KEY"
Response200 OK
{
  "batch_id": "0b6f3c2e-8d1a-4f5b-9c7e-2a4d6e8f1b3c",
  "status": "done",
  "total": 4,
  "checked": 4,
  "results": [
    {
      "type": "phone",
      "number": "+14155550123",
      "status": "valid",
      "line_type": "mobile",
      ...
    },
    {
      "type": "email",
      "email": "jane@example.com",
      "status": "valid",
      ...
    }
  ],
  "credits_remaining": 4820
}
Webhook
POST https://yourapp.com/webhook/lineverify
User-Agent: LineVerify-Webhook/1.0

{
  "batch_id": "0b6f3c2e-8d1a-4f5b-9c7e-2a4d6e8f1b3c",
  "status": "done",
  "credits_used": 3,
  "results": [
    {
      "type": "phone",
      "number": "+14155550123",
      "status": "valid",
      ...
      "credits_used": 1
    },
    ...
  ]
}

SDKs

No SDK needed. Plain HTTP works from any language. Typed SDKs are on the roadmap.

Switch between curl, Node and Python at the top of the page. Every code sample follows your choice.

Helper
curl -X POST \
  https://api.lineverification.com/v1/lookup \
  -H "Authorization: Bearer lv_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"number": "+14155550123"}'

Agent use

LineVerify is built to be called by AI agents. Use the tool definition with Claude, GPT-4 or any OpenAI-compatible function calling API. Responses are structured for easy parsing, no prompt engineering needed.

Verify before acting. Have your agent call verify_phone_number before it sends a text, makes a call or adds a contact to a CRM. Only go ahead when status is "valid" and dnc_status is "clean".

Agent-safe guarantees
Predictable
The same number always returns the same shape
Typed
Every field is typed, with a fixed set of values
Fast
Under a second, safe to call inside an agent loop
Sandbox
Test with lv_test_ keys, no checks used
Tool definition
{
  "name": "verify_phone_number",
  "description": "Verify a phone number and return carrier, line type, spam risk, DNC status, TCPA risk, caller name, and timezone. Use before making calls or sending SMS to ensure the number is valid and safe to contact.",
  "parameters": {
    "type": "object",
    "properties": {
      "number": {
        "type": "string",
        "description": "Phone number in E.164 format, e.g. +14155550123"
      }
    },
    "required": ["number"]
  }
}

MCP server

Use LineVerify as a Model Context Protocol (MCP) server. Drop the server into Claude Desktop or any MCP-compatible host. It speaks the MCP stdio transport directly, so there is nothing to install besides Node.

Setup
1
Save the server as mcp-server.js
2
Add it to claude_desktop_config.json with your key
3
Restart Claude Desktop and ask it to verify a number
Heads up

The server needs Node 18 or later for built-in fetch. Hosts that support remote MCP servers can skip the file and connect to https://www.lineverification.com/api/mcp with your key as a Bearer token.

mcp-server.js
// mcp-server.js: LineVerify MCP server (stdio transport)
// Implements MCP over stdin/stdout, no SDK required.
// Requires Node 18+ for native fetch.

const KEY = process.env.LV_API_KEY;

async function lvLookup(body) {
  const res = await fetch("https://api.lineverification.com/v1/lookup", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });
  return { ok: res.ok, data: await res.json() };
}

const TOOLS = [{
  name: "verify_phone",
  description: "Verify a US phone number. Returns status, carrier, line type, spam risk, DNC status, TCPA risk, caller name, and timezone.",
  inputSchema: {
    type: "object",
    properties: {
      number: { type: "string", description: "Phone number in E.164 format, e.g. +14155550123" }
    },
    required: ["number"]
  }
}, {
  name: "verify_email",
  description: "Verify an email address. Returns status, deliverability, whether the inbox exists, and whether it is disposable.",
  inputSchema: {
    type: "object",
    properties: {
      email: { type: "string", description: "Email address, e.g. jane@example.com" }
    },
    required: ["email"]
  }
}];

async function handle(msg) {
  switch (msg.method) {
    case "initialize":
      return { protocolVersion: msg.params.protocolVersion, capabilities: { tools: {} },
        serverInfo: { name: "lineverify", version: "1.0.0" } };
    case "ping":
      return {};
    case "tools/list":
      return { tools: TOOLS };
    case "tools/call": {
      const { ok, data } = await lvLookup(msg.params.arguments);
      return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], isError: !ok };
    }
  }
}

process.stdin.setEncoding("utf8");
let buf = "";
process.stdin.on("data", async chunk => {
  buf += chunk;
  const lines = buf.split("\n");
  buf = lines.pop();
  for (const line of lines) {
    if (!line.trim()) continue;
    const msg = JSON.parse(line);
    if (msg.id === undefined) continue; // notifications need no reply
    const result = await handle(msg);
    const response = result
      ? { jsonrpc: "2.0", id: msg.id, result }
      : { jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: "Method not found" } };
    process.stdout.write(JSON.stringify(response) + "\n");
  }
});
claude_desktop_config.json
{
  "mcpServers": {
    "lineverify": {
      "command": "node",
      "args": ["/path/to/mcp-server.js"],
      "env": { "LV_API_KEY": "lv_live_YOUR_KEY" }
    }
  }
}

Errors and limits

Errors use standard HTTP status codes, with a JSON body that holds a code you can match on and a message you can show. You can make up to 100 requests a minute with each key. Errors are never billed.

Status codes
400
invalid_request
The body is missing, not JSON, or breaks a limit
400
invalid_number
The number could not be read
400
invalid_email
The email address could not be read
401
unauthorized
Missing, invalid or revoked API key
402
insufficient_credits
Not enough checks left
404
not_found
No batch with that id
422
unsupported_region
The number is outside the US
429
rate_limit_exceeded
More than 100 requests a minute. Wait and retry
500
upstream_error
None of our data providers answered. Not billed, try again
Request
curl -X POST \
  https://api.lineverification.com/v1/lookup \
  -H "Authorization: Bearer lv_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"number": "+14155550123"}'
Response402 Payment Required
{
  "error": {
    "code": "insufficient_credits",
    "message": "You have no checks left."
  }
}

Credits

Checks are called credits in the API. They never expire, and your balance comes back as credits_remaining in every successful response. To check it on its own, call GET /v1/balance, which returns credits_remaining, plan and sandbox.

Checks used
1
Lookup with a valid, risky or landline result
0
Lookup with an invalid result, or one that errors
1
Bulk lookup, per contact. Held up front, and checks for invalid results come back when it is done
0
Any lookup with an lv_test_ sandbox key
Request
curl -X POST \
  https://api.lineverification.com/v1/lookup \
  -H "Authorization: Bearer lv_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"number": "+14155550123"}'
Response200 OK
{
  "type": "phone",
  "number": "+14155550123",
  "status": "valid",
  "line_type": "mobile",
  "carrier": "Verizon",
  "portable": true,
  "ported_date": "2021-04-12",
  "caller_name": "JOHN SMITH",
  "activity_score": 88,
  "spam_score": "low",
  "dnc_status": "clean",
  "dnc_list": null,
  "tcpa_risk": "low",
  "timezone": "Pacific (PT)",
  "location": "San Francisco, CA",
  "credits_used": 1,
  "credits_remaining": 4823
}