SwitchID API Documentation

The SwitchID Verification API checks a US phone number against a person or business name that you supply. Phase 1 offers one check, core_phone. It returns the number's carrier and line type, the caller-ID name where one is registered, and a comparison between that caller-ID name and the name you sent. Only US numbers are supported. Results are evidence for your own review: the API does not return approve or deny decisions.

The email, device, IP and card signals described on the SwitchID product page are not yet available through this API. This page documents only what the API returns today, as of contract version 2026-10-15.2.

Base URL and authentication#

Base URL
https://www.switchlabs.dev/api/switchid/v1

Requests and responses are JSON over HTTPS. Responses carry Cache-Control: no-store because they contain consumer data.

Key prefixEnvironmentBehaviour
sid_test_...testRuns against a deterministic sandbox and never reaches a paid provider. Usage is recorded but never invoiced.
sid_live_...livePerforms real carrier and caller-ID lookups. Metered and invoiced monthly.
  • Send the key on every request as Authorization: Bearer sid_test_... or Authorization: Bearer sid_live_.... A missing, unknown, revoked or expired key returns 401 unauthorized.
  • Each key belongs to one account and one environment. Results carry the matching environment, and a key can read only requests and usage from its own account and environment.
  • Keys are issued with the scopes verify:write, requests:read, usage:read. A key without the scope an endpoint needs returns 403 forbidden_scope.
  • Call the API from your server. Do not ship a key in a browser, mobile app or public repository.

Getting a key

Create a sandbox key on the SwitchID sandbox page. For live access, contact us.

Idempotency#

Every check can cost money, so every POST carries a key that identifies one logical check. Retrying with the same key is never billed twice.

  • Idempotency-Key is required on every POST. It must be 1 to 128 printable characters. Without a valid key the request returns 400 missing_idempotency_key before anything runs.
  • Use one key per logical check, for example your application id plus a step name, or a UUID you store with the application.
  • The same key with the same body returns the original result, with the same request_id, and is never billed twice. For this comparison the phone number is compared after normalization, the name ignores case and repeated spaces, and the order of add_ons does not matter.
  • The same key with a different body returns 409 idempotency_conflict.
  • If the first request with a key is still running, a repeat returns 202 with { "request_id": "...", "status": "pending" }. Poll GET /requests/{id} instead of sending a new check.
  • After a network error, a timeout or a 5xx response, retry with the same key. A same-key retry is always safe: it runs the check only if the first attempt never started, and otherwise returns the stored outcome.
  • A stored failure or rejection is final for its key. Replaying the key returns the same error and does not run the check again. Start a new check with a new key only after GET /requests/{id} confirms the failure was not billed and its cause is resolved. Never change keys to get around a pending request or a ledger_unavailable response.
  • Keys are remembered for at least 30 days, separately for each account, environment and endpoint.

POST /verify#

POST/verifyVerify a phone number against a supplied name

Headers

HeaderRequiredValue
AuthorizationYesBearer sid_test_...
Idempotency-KeyYesA unique key for this logical check
Content-TypeYesapplication/json

Body

FieldTypeRequiredDescription
phonestringYesUS number in any common format, such as +12025550100 or (202) 555-0100. A number that cannot be parsed returns 422 invalid_phone. Non-US numbers, including Canada and the Caribbean, return 422 unsupported_country. US territories such as Puerto Rico and Guam are currently treated as non-US.
namestringYesThe person or business name you want compared with the registered caller-ID name. Up to 200 characters, no control characters.
subject_typestringYesperson or business. Selects the name-comparison rules.
reference_idstringNoYour own identifier, up to 128 characters. Stored and echoed back in subject.reference_id, never interpreted.
add_onsarray of stringsNoPhase 1 accepts only an empty list or ["core_phone"]. Any other value returns 403 forbidden_sku. The core_phone check always runs.

Send Content-Type: application/json. Unknown fields are rejected with 400 invalid_request, and the details array names the field.

Example request
curl -sS -X POST https://www.switchlabs.dev/api/switchid/v1/verify \
  -H "Authorization: Bearer sid_test_..." \
  -H "Idempotency-Key: app-2026-000123-phone-1" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+12025550100",
    "name": "John Smith",
    "subject_type": "person",
    "reference_id": "APP-2026-000123"
  }'

The sandbox returns this for +12025550100. Your request_id and timestamps will differ.

Example response (200)
{
  "request_id": "3f2b8c1e-7d4a-4e9b-9c61-5a0d2e8f4b17",
  "environment": "test",
  "created_at": "2026-10-16T14:03:21.418Z",
  "completed_at": "2026-10-16T14:03:21.463Z",
  "product_version": "2026-10-15.2",
  "rule_version": "core-phone.v2",
  "result_state": "completed",
  "subject": {
    "phone_e164": "+12025550100",
    "phone_national": "(202) 555-0100",
    "subject_type": "person",
    "supplied_name": "John Smith",
    "reference_id": "APP-2026-000123"
  },
  "checks": {
    "requested": [
      "core_phone"
    ],
    "completed": [
      "core_phone"
    ]
  },
  "phone": {
    "valid": true,
    "country": "US",
    "line_type": "mobile",
    "line_type_raw": "mobile",
    "carrier_name": "T-MOBILE USA INC",
    "carrier_type": "wireless",
    "ported": false,
    "rate_center": {
      "city": "WASHINGTON",
      "state": "DC"
    }
  },
  "caller_id": {
    "state": "available",
    "name": "SMITH JOHN",
    "error_code": null
  },
  "name_match": {
    "state": "match",
    "score": 98,
    "reasons": [
      "last_name_exact",
      "first_name_exact"
    ]
  },
  "signals": {
    "voip_or_virtual": false,
    "toll_free": false
  },
  "provenance": [
    {
      "provider": "sandbox",
      "sku": "core_phone",
      "cache_hit": false,
      "attempted_at": "2026-10-16T14:03:21.463Z"
    }
  ],
  "billing": {
    "disposition": "billable",
    "sku": "core_phone",
    "unit_price_microdollars": 15000,
    "cache_hit": false
  },
  "warnings": [
    "rate_center_is_not_subscriber_address"
  ]
}

Responses

StatusMeaning
200A completed check, or a result with result_state unavailable for a number reported as not allocated or invalid.
202The same Idempotency-Key is still being processed. The body is { "request_id": "...", "status": "pending" }.
400, 401, 403, 409, 422, 429, 500, 502, 503Error envelope. See Errors for every code.

Reading a result#

Each field group in a 200 result, and how to use it.

name_match.state unavailable means there was no usable caller-ID name to compare (caller_id.state is placeholder, unavailable or error). It is never a mismatch. Treat it as no evidence either way.

FieldValuesHow to read it
result_statecompleted, unavailable, errorcompleted: the check ran and the fields below are filled in. unavailable: the number is reported as not allocated or invalid; it comes back as HTTP 200 with an empty checks.completed and is not billed. error is part of the schema, but a check that fails today is returned as an error response rather than a 200 body.
subjectobjectWhat you sent, normalized: phone_e164, phone_national, subject_type, supplied_name and reference_id.
checks.requested, checks.completed["core_phone"]Which checks were asked for and which finished.
phone.line_typemobile, landline, voip, toll_free, premium, pager, unknownNormalized line type. phone.line_type_raw keeps the original value from the lookup.
phone.carrier_name, phone.carrier_typecarrier_type: wireless, landline, voip, unknownThe carrier the number currently belongs to. carrier_type can be unknown even when line_type is known.
phone.validtrue, false, nullThe lookup's validity flag when one is returned. It is not an activity or reachability signal. When it is missing you also get the provider_validity_not_returned warning.
phone.portedtrue, false, nullWhether the number has moved between carriers. It is not evidence that the subscriber changed.
phone.rate_center{ city, state } or nullThe rate-center location the number is assigned to. It is not the subscriber's address, and it always comes with the rate_center_is_not_subscriber_address warning.
caller_id.stateavailable, placeholder, unavailable, erroravailable: a usable registered name is in caller_id.name. placeholder: the carrier returned a generic string such as WIRELESS CALLER, and caller_id.name is null. unavailable: no caller-ID name is on file. error: the caller-name lookup returned an error code, shown in caller_id.error_code.
caller_id.namestring or nullThe registered name as returned, usually upper case. Caller-ID names hold at most 15 characters, so long names arrive cut off, and many list the last name first (SMITH JOHN).
name_match.statematch, partial_match, mismatch, ambiguous, unavailablematch: the names agree once case, word order, punctuation, common nicknames and initials, legal suffixes such as LLC, and caller-ID truncation are allowed for. partial_match: they agree only in part, for example the same last name with a different first name. ambiguous: the overlap is too weak to call, for example only the first name agrees or two business names share only generic words. mismatch: a usable caller-ID name exists and does not agree with the name you sent. unavailable: there was no usable caller-ID name to compare, or nothing usable was left of the supplied name.
name_match.score0 to 100, or nullSimilarity evidence, not a probability that the person owns the number. null when the state is unavailable.
name_match.reasonsarray of stringsMachine-readable explanations such as last_name_exact, caller_id_truncated_prefix or caller_id_placeholder. Store them with the result; the list of possible reasons is not fixed.
signals.voip_or_virtualtrue, false, nulltrue for VoIP lines. Descriptive only: many legitimate people and businesses use VoIP. null when the line type could not be determined.
signals.toll_freetrue, false, nulltrue for toll-free numbers.
provenance[]provider, sku, cache_hit, attempted_atWhere the facts came from. provider is sandbox for test keys and carrier_lookup for live keys. attempted_at is when the underlying lookup ran; a result served from cache keeps its original time.
billingdisposition: billable, not_billable, pending_reconciliationWhether this request counts toward your bill. unit_price_microdollars is the price that applied, in integer microdollars ($0.015 is 15000). cache_hit is true when the result came from your account's cache. pending_reconciliation means the accounting record is not confirmed yet; keep the request id.
warningsarray of stringsNotes on how to read this result. The values you can see today are listed below.
product_version, rule_version2026-10-15.2, core-phone.v2The API contract version and the matching-rules version that produced the result. Store them with the result.

Warnings

WarningMeaning
rate_center_is_not_subscriber_addressphone.rate_center is present. It is the number's rate-center location, not where the subscriber lives.
voip_is_descriptive_not_fraudThe line is VoIP. That alone is not a fraud signal.
caller_id_placeholder, caller_id_unavailable, caller_id_errorThere is no usable caller-ID name, so name_match.state is unavailable.
provider_validity_not_returnedThe lookup did not return a validity flag, so phone.valid is null.
served_from_tenant_cacheThe result came from your account's cache. No new lookup was made, and the name comparison was recomputed.
free_recheck_within_24hA cached re-check within 24 hours of the original lookup. It is not billed.
provider_reports_invalid_numberThe number is not allocated or not valid. result_state is unavailable and the request is not billed.

GET /requests/{id}#

GET/requests/{id}Fetch a previous request by id

Returns the stored outcome of an earlier POST /verify made by your account in the same environment. Needs the requests:read scope. Use it to poll after a 202, and to inspect any response that carried a request_id before deciding whether to start another check.

StatusMeaning
200A completed request. The body is the same result POST /verify returned.
200A stored-outcome resource for a request that failed, was rejected or is waiting for accounting reconciliation. The lookup itself succeeded; read status and error for the original outcome.
202Still running. The body is { "request_id": "...", "status": "pending" }.
404not_found: no request with that id exists for this key's account and environment.
Example request
curl -sS https://www.switchlabs.dev/api/switchid/v1/requests/7c9e2b14-5a3f-4d8e-a1b6-3e0f9c2d4a85 \
  -H "Authorization: Bearer sid_test_..."

A request to the timeout sandbox number, +12025550106, reads back like this afterwards:

Example response (200, stored failure)
{
  "request_id": "7c9e2b14-5a3f-4d8e-a1b6-3e0f9c2d4a85",
  "status": "failed",
  "error": {
    "code": "provider_unavailable",
    "message": "This Idempotency-Key previously produced an error; the stored outcome is returned."
  },
  "billing_disposition": "not_billable"
}

Stored outcome status

statusMeaning
failedThe check failed and was not billed. Final for its key.
rejectedThe check was refused before any lookup, for example because a spend cap was reached. Not billed. Final for its key.
pending_reconciliationThe check may have completed, but its accounting record is not confirmed. Do not start a duplicate check; contact us with the request id.

GET /usage#

GET/usageUsage summary for your key's account and environment
QueryTypeRequiredDescription
fromdate (YYYY-MM-DD)NoInclusive start date, UTC. Defaults to the first day of the current month.
todate (YYYY-MM-DD)NoInclusive end date, UTC. Defaults to today.
  • Needs the usage:read scope. The summary covers the key's own account and environment, so a test key reports only test usage.
  • With neither parameter, the window is the current UTC month to date. To report on an earlier month, pass both from and to. A window longer than 366 days, or a from after to, returns 400 invalid_request.
  • period.from and period.to are the UTC instants actually covered. period.to is exclusive: the start of the day after to, or the current time when to is omitted.
  • billable_requests counts requests that carried a charge or made a fresh lookup. cache_hits counts every result served from your account's cache, charged or free. Failed requests and unavailable results do not appear.
  • Amounts are integer microdollars. In the example, 42 requests at $0.015 come to 630000 microdollars, or $0.63.
Example request
curl -sS "https://www.switchlabs.dev/api/switchid/v1/usage?from=2026-10-01&to=2026-10-15" \
  -H "Authorization: Bearer sid_test_..."
Example response (200)
{
  "tenant_id": "6c0f2a9e-1b7d-4c3e-8f25-9d4a7e1b3c58",
  "environment": "test",
  "period": {
    "from": "2026-10-01T00:00:00.000Z",
    "to": "2026-10-16T00:00:00.000Z"
  },
  "by_sku": [
    {
      "sku": "core_phone",
      "billable_requests": 42,
      "cache_hits": 5,
      "amount_microdollars": 630000
    }
  ],
  "total_amount_microdollars": 630000
}

Sandbox numbers#

Test keys never reach a paid provider. These numbers, in the reserved fictional 555-01xx block, always produce the same scenario, so you can exercise every branch of your integration. They behave this way only with sid_test_ keys. "Billable" below describes the billing.disposition the result reports; test usage is never invoiced.

NumberScenarioSendResult
+12025550100Person, mobile"John Smith", person200. Mobile line with caller ID SMITH JOHN: caller_id.state available and name_match.state match. Billable.
+12025550101Placeholder caller IDAny name200. Mobile line whose caller ID is WIRELESS CALLER, so caller_id.state is placeholder, caller_id.name is null and name_match.state is unavailable. Billable.
+12025550102Landline, no caller IDAny name200. Landline with no caller-ID name on file: caller_id.state unavailable, name_match.state unavailable. Billable.
+12025550103VoIP person"Jane Doe", person200. Ported VoIP line with caller ID DOE JANE: signals.voip_or_virtual true, the voip_is_descriptive_not_fraud warning, and name_match.state match. Billable.
+12025550104Business landline, truncated caller ID"Acme Roofing LLC", business200. Landline with caller ID ACME ROOFING LL, cut off at 15 characters. The comparison allows for the truncation and the legal suffix and returns match. Billable.
+12025550105Caller-name errorAny name200. Carrier and line type are returned, but the caller-name lookup fails: caller_id.state error, caller_id.error_code CNAM_LOOKUP_FAILED, name_match.state unavailable. Billable, and never served from cache.
+12025550106Provider timeoutAny name502 provider_unavailable with a request_id, after one automatic retry. Not billed. Replaying the same key returns the same stored failure.
+12025550107Invalid numberAny name200 with result_state unavailable, an empty checks.completed, billing.disposition not_billable and the provider_reports_invalid_number warning. Not billed.
+12025550108Toll-freeAny name200. phone.line_type toll_free and signals.toll_free true. The caller ID TOLL FREE CALL is a placeholder, so name_match.state is unavailable. Billable.
+12025550109Provider HTTP 500Any name502 provider_unavailable with a request_id. No automatic retry. Not billed.

Any other US number returns a deterministic synthetic result: the same number always gives the same line type, an invented carrier name, rate center SANDBOX, DC, and a caller-ID name of the form SAMPLE followed by six hexadecimal characters. Because that name is synthetic, comparing it with a real name normally returns mismatch. Validation still applies in the sandbox, so unparseable and non-US numbers return 422.

Errors#

Every error uses the same envelope: { "error": { "code", "message", "request_id?", "details?" } }, with the HTTP status listed below.

Example error (409)
{
  "error": {
    "code": "idempotency_conflict",
    "message": "This Idempotency-Key was already used with a different request.",
    "request_id": "3f2b8c1e-7d4a-4e9b-9c61-5a0d2e8f4b17"
  }
}
  • Branch on error.code. The message is written for people and may change.
  • request_id is present when the request was recorded. Keep it: it is how you and we find the request later.
  • details is present for field validation errors, as a list of { path, message }, and for forbidden_sku, naming the rejected add-ons.
  • Both 429 codes send a Retry-After header in whole seconds.
CodeHTTPMeaningWhat to do
unauthorized401The API key is missing, malformed, unknown, revoked or expired.Check the Authorization: Bearer header and the key. Retrying unchanged will not help.
forbidden_scope403The key lacks the scope this endpoint needs: verify:write, requests:read or usage:read.Use a key with that scope, or contact us for one.
forbidden_sku403The core_phone check is not enabled for this account and environment, or add_ons named something other than core_phone.Remove unsupported add-ons. If the check is not enabled, contact us.
tenant_suspended403The account is suspended.Contact us.
invalid_request400The body or query failed validation: malformed JSON, a missing or unknown field, or a value out of range.Fix the request using details, then send it again.
invalid_phone422The phone number could not be parsed as a valid number.Correct the number. Do not retry it unchanged.
unsupported_country422The number is valid but is not a US number.Do not retry. Only US numbers are supported.
missing_idempotency_key400The Idempotency-Key header is missing or is not 1 to 128 printable characters.Add a valid key and send the request again.
idempotency_conflict409This Idempotency-Key was already used with a different body. request_id names the original request.Resend the original body to read its result, or use a new key for a new check.
rate_limited429The per-key or per-account requests-per-minute limit was reached. Nothing was recorded for this key.Wait the number of seconds in Retry-After, then retry with the same key.
spend_cap_reached429The account's daily request cap or monthly spend cap was reached. The request is stored as rejected and not billed.Honour Retry-After before trying again. The rejection is stored against this key, so a later attempt needs a new key. Contact us if the cap is too low.
enforcement_unavailable503Limits, entitlement or request recording could not be confirmed, so nothing paid ran.Without a request_id, retry with the same key after a short wait. With one, check GET /requests/{id} before starting another check.
provider_unavailable502The lookup timed out or failed upstream. Not billed.The failure is stored against this key, so a same-key retry returns it again. Confirm with GET /requests/{id}, then start a new check with a new key.
provider_unconfigured503Lookups are not available for this environment yet. Not billed.Contact us. The failure is stored against this key.
ledger_unavailable500The check may have completed, but its accounting record could not be confirmed. Replaying the key returns this error until it is reconciled.Keep the request_id and the key, do not start a duplicate check, and contact us.
not_found404No request with this id exists for this key's account and environment.Check the id, and use a key from the environment that created the request.
internal_error500An unexpected error.With a request_id, check GET /requests/{id} before starting another check. Without one, retry with the same key.

Billing and rate limits#

Billing

  • A completed core_phone check is billable even when no usable caller-ID name is on file (caller_id.state placeholder, unavailable or error), because the carrier and line type were still looked up.
  • Provider and system failures are not billed. Neither are rejected requests or numbers reported as invalid (result_state unavailable). The one exception is ledger_unavailable: that check may have completed, and once it is reconciled it is billed at most once.
  • Replaying an Idempotency-Key is never billed twice.
  • Re-checking the same number with the same subject_type within 24 hours of the original lookup is free. The result is served from your account's cache with billing.disposition not_billable and the free_recheck_within_24h warning. A result whose caller-name lookup failed is not cached, so checking that number again runs, and bills, a new lookup.
  • Results are reused from your account's cache for 30 days by default; your agreement can set a different window. After the first 24 hours, a cached result is charged as your agreement sets for cache hits. billing.cache_hit and billing.unit_price_microdollars on each result show what applied. The name comparison is always recomputed against the name you send.
  • Each result reports the unit price that applied in billing.unit_price_microdollars, in integer microdollars: $0.015 is 15000.
  • Test usage (sid_test_ keys) is never invoiced. Test results still report the billing disposition that would apply, and GET /usage shows them for the test environment. Live usage is invoiced monthly.

Rate limits

  • POST /verify is limited per API key and per account in each environment: 120 requests per minute each by default. Going over either returns 429 rate_limited with Retry-After.
  • An account can also have a daily request cap and a monthly spend cap. Reaching one returns 429 spend_cap_reached with Retry-After.

Downloads and links#

OpenAPI document

Download the OpenAPI 3.1 document (contract version 2026-10-15.2). It is the source of truth for every field on this page.

Reference scripts

Two short scripts show a correct integration against the sandbox.

verify.sh
A bash script using curl, with python3 to format the JSON. It sends one check to the sandbox with a fresh Idempotency-Key, then replays the same key to show that a replay returns the stored outcome instead of starting a new paid check. Run it as SWITCHID_API_KEY=sid_test_... bash verify.sh, optionally followed by a phone number, name and subject type.
verify.mjs
A Node.js script (Node 18 or later, built-in fetch). It generates a UUID Idempotency-Key, retries network errors with the same key, reads GET /requests/{id} when a 202, 429 or 5xx response carries a request id, waits out short Retry-After values, and treats name_match.state unavailable as no evidence rather than a mismatch.

Contact us for a copy of either script.

More

Contact

Tell us what you're building and we'll get in touch fast

Ship a proof-of-concept, integrate credit reporting, or hand off the workflow entirely—we respond within one business day and loop in the right Switch Labs partner for your stack.

Response Time
< 24 hours
Delivery Options
Product | Services

By submitting you agree to let Switch Labs contact you about relevant products and services.