$ DialCode API

One endpoint: send a phone number, get back whether it's real, which country and area it's from, what line type it is, and whether its prefix is currently associated with reported scams. JSON in, JSON out.

Authentication

Every request needs an X-Api-Key header. Get a free key at /api/login - no credit card, no forms beyond an email address for the magic link.

X-Api-Key: dc_your_key

Requests without a valid key get 401 with {"error": "invalid_api_key"}.

Endpoint

GET /v1/phone/{number}

{number} is any phone number in international format. The leading + works as-is in the URL path - no encoding needed (%2B also accepted if your HTTP client encodes it). Numbers sent without the + (e.g. 13214991734) are parsed as international automatically. Numbers without enough digits, or that don't parse to a real dialing plan, return a valid: false result rather than an error - with a hint field saying what would resolve them.

National-format numbers: ?country=

A number written the way people write it locally - 8663982896, 866-398-2896, (866) 398-2896 - carries no country code, so it cannot be resolved on its own. Pass the country and it will be read against that numbering plan:

$ curl "https://dialcode.app/v1/phone/8663982896?country=US" \
    -H "X-Api-Key: dc_your_key"

{ "valid": true, "e164": "+18663982896", "country": { "code": "US", ... } }

country takes an ISO 3166-1 alpha-2 code (case-insensitive) and applies to that one lookup. An unknown code returns 400 invalid_country and does not spend quota. The same option exists for CSV uploads in the dashboard, where it applies to every row of the file.

If all your numbers come from one market, set a default country on your key in the dashboard instead: every lookup and upload that names no country is read against it. An explicit ?country= still wins.

When a number still comes back valid: false, reason says which kind of false it is: needs_country means the digits are a real national number somewhere and candidates lists the countries that accept them; not_a_number means no numbering plan does.

$ curl https://dialcode.app/v1/phone/+639171234567 \
    -H "X-Api-Key: dc_your_key"

{
  "valid": true,
  "e164": "+639171234567",
  "country": { "code": "PH", "dial_code": "+63", "name": "Philippines", "timezone": "Asia/Manila" },
  "formats": { "national": "0917 123 4567", "international": "+63 917 123 4567" },
  "carrier": "Globe",
  "line_type": "mobile",
  "area": { "code": "917", "name": "Manila" },
  "risk": {
    "scam_risk": "high",
    "scam_types": ["wangiri", "romance"],
    "advice_url": "https://dialcode.app/scam/63-scam",
    "complaints": null,
    "signals": []
  }
}

Response fields

FieldTypeDescription
validbooleanWhether the number parses as a real, dialable number.
e164string | nullNormalized E.164 form. Null when valid is false.
reasonstringPresent only when valid is false: needs_country (a real national number, country missing) or not_a_number (no numbering plan accepts it).
candidatesarray of stringsPresent only when valid is false: ISO 3166-1 alpha-2 codes the input resolves under, US first then alphabetical. Empty for not_a_number and whenever country was given.
hintstringPresent only when valid is false: what would make the number resolve, in words. Names the countries to pass when the input looks like a national-format number.
country.codestring | nullISO 3166-1 alpha-2 country code (e.g. PH).
country.dial_codestring | nullInternational dialing code (e.g. +63).
country.namestring | nullCountry name.
country.timezonestring | nullIANA timezone of the country's capital (e.g. Asia/Manila). For countries spanning multiple zones this is the capital's zone, not the number's exact zone.
formats.nationalstring | nullNumber formatted for domestic dialing (e.g. 0917 123 4567). Null when valid is false.
formats.internationalstring | nullHuman-readable international format with spacing (e.g. +63 917 123 4567). Use e164 for machine comparison.
carrierstring | nullCarrier the number range was originally allocated to (e.g. Globe, O2). This is the range holder, not necessarily the current operator - numbers moved via porting keep their original range label. Mostly available for mobile numbers; always null for US/Canada (no public carrier data for the NANP) and most landlines.
line_typestring | nullLine type as detected by number parsing (e.g. mobile, fixed_line).
area.codestring | nullLocal area code, when the country has area-code data and one matches.
area.namestring | nullCity / region for the matched area code.
risk.scam_riskstringlow, medium, or high - the country's overall scam risk rating. unknown when the number does not resolve to a country but a risk prefix matched (e.g. satellite ranges).
risk.scam_typesarray of stringsScam patterns reported for this country (e.g. wangiri, romance). Empty array if none documented.
risk.advice_urlstring | nullLink to the full scam profile page for this country, when one exists.
risk.complaintsobject | nullFTC Do Not Call complaint history for this exact number. Present only for US numbers with at least one reported complaint; null for non-US numbers or numbers with no reports. Fields: total (int), robocalls (int, subset of total), first_reported_on / last_reported_on (date), categories (array of {code, count}, sorted by count - what consumers reported the calls were about; codes: imposter, debt_reduction, medical, dropped_call, warranties, tech_support, lottery, timeshares, energy, home_improvement, home_security, work_from_home, charity, unspecified, other; category tracking starts 2026-07-19, so counts can lag total for numbers first reported earlier), geography (object | null - victim spread across US states: states (int, distinct states that reported this number - a broad spread means a nationwide robocall campaign, a single state a targeted local one) and top (up to 5 {state, count} sorted by report count); null until state tracking has data for the number, tracked since 2026-07-19), source (string).
risk.fcc_complaintsobject | nullFCC consumer complaint history (unwanted-calls dataset) for this exact number - an independent second US government source next to the FTC registry. A number present in both is strongly corroborated. Present only for US numbers with at least one FCC complaint. Fields: total (int), first_reported_on / last_reported_on (date), source (string). Tracked since 2026-07-19; the fcc_reported signal fires at 3+ complaints.
risk.signalsarray of objectsRisk signals detected for this specific number: {code, note} pairs from line-type, known scam prefix ranges, and FTC complaint history. Empty array if none detected. See signal codes below.

Complaint data comes from the FTC's public Do Not Call registry CSVs, ingested daily (weekday publishing schedule - the FTC does not publish weekend files). US numbers only: the Do Not Call registry is a US program.

Signal codes

risk.signals entries come from two layers: line type (detected per-number) and known prefix ranges (detected from the dial code). A number can carry signals from both layers at once.

CodeLayerMeaning
voip_lineline typeVoIP number: cheap to obtain anonymously, common in fraud and SMS-verification bypass.
premium_rate_lineline typePremium-rate number: calling it incurs elevated charges.
shared_cost_lineline typeShared-cost number: partial elevated charges apply.
satellite_rangeprefixSatellite range (+870, +881): very high callback charges, classic wangiri bait.
international_network_rangeprefixInternational network range (+882, +883): ITU non-geographic codes outside normal country dialing plans.
premium_redirect_rangeprefixUK personal numbering range (+4470): missed-call redirect and premium forwarding.
premium_rate_rangeprefixUS 900 premium-rate range (+1900): calls billed at elevated per-minute rates.
ftc_reportedcomplaintsUS number reported to the FTC Do Not Call registry 3 or more times.

Errors

StatuserrorMeaning
401invalid_api_keyMissing or invalid X-Api-Key header.
400unparseable_numberInput is too short (under 4 digits) to attempt a lookup.
400invalid_countrycountry is not an ISO 3166-1 alpha-2 code we know. No quota spent.
429quota_exceededMonthly quota used up for this key.
429rate_limitedPer-minute rate limit exceeded, independent of the monthly quota.

All errors return JSON with a machine code and a human message: {"error": "quota_exceeded", "message": "Monthly quota exhausted. It resets on the 1st (UTC) - see Retry-After."}. Both 429 variants ship a Retry-After header (seconds).

Limits & rate-limit headers

GET /v1/account/usage

Quota state for the calling key. Free - consumes no quota, logs nothing.

$ curl https://dialcode.app/v1/account/usage -H "X-Api-Key: dc_your_key"

{
  "key_prefix": "dc_1a2b3c4d",
  "monthly_quota": 1000,
  "used": 250,
  "remaining": 750,
  "resets_on": "2026-08-01"
}

Examples

curl

curl https://dialcode.app/v1/phone/+639171234567 \
  -H "X-Api-Key: dc_your_key"

JavaScript (fetch)

const res = await fetch("https://dialcode.app/v1/phone/+639171234567", {
  headers: { "X-Api-Key": "dc_your_key" }
});
const data = await res.json();
console.log(data.risk.scam_risk);

Ruby (Net::HTTP)

require "net/http"
require "json"

uri = URI("https://dialcode.app/v1/phone/+639171234567")
req = Net::HTTP::Get.new(uri)
req["X-Api-Key"] = "dc_your_key"

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)
puts data["risk"]["scam_risk"]

Python (requests)

import requests

res = requests.get(
    "https://dialcode.app/v1/phone/+639171234567",
    headers={"X-Api-Key": "dc_your_key"},
)
print(res.json()["risk"]["scam_risk"])

PHP (curl)

$ch = curl_init("https://dialcode.app/v1/phone/+639171234567");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-Api-Key: dc_your_key"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$data = json_decode(curl_exec($ch), true);
echo $data["risk"]["scam_risk"];

Bulk CSV

Validate a whole file at once: upload a CSV (one number per line or first column, 10,000 rows / 1 MB max) in the dashboard. Each parsed row spends one quota request; the result CSV (validity, country, carrier, scam risk, FTC/FCC complaint totals, signals) is ready for download within a minute and kept for 7 days.

Spec & changelog

Get a free API key →