Skip to content
VPNDetection

Batch

post/batch

Answers up to 1000 addresses in one call. Each distinct string in ips is one lookup: it costs exactly what GET /{ip} costs for that address and comes back with exactly the fields that call would carry for your plan. Exact duplicates collapse to one entry and one lookup.

Both maps in the answer are keyed by the string you sent, so nothing has to be lined up by position; the ip inside each result is the normalized form. An address that could not be answered sits in errors with the status and message the single lookup would have given, and never disturbs the others: a string that is not an address is a 400 there, and an overage cap reached part way through leaves the remaining entries as 429s.

The call itself fails only for the reasons below, and a 429 on the call always carries Retry-After: the batch is admitted or refused whole by the rate limit, so a per-entry 429 is always the overage cap and never a throttle.

A batch needs a paid plan: without a key the call is refused with 401, and with a key on the free plan with 402, unless it names one address, which is answered as GET /{ip} would answer it, under the same rate limit. Unlimited looks up one address at a time.

Authorization

apiKeyQueryapikey in query

?apikey=<key>. Convenient in a browser; prefer a header elsewhere.

apiKeyHeaderX-Api-Key in header

X-Api-Key: <key>.

bearerAuthbearer token

Authorization: Bearer <key>.

Request body

application/json

ipsrequiredarray<string>

The addresses to classify, 1 to 1000 per call, counted before duplicates collapse. Each distinct string is one lookup.

Example ["1.1.1.1","2606:4700:4700::1111"]

Responses

200application/json

One entry per distinct input string, in results or in errors. Present fields within a result are those your plan includes; see the tier table in the API description.

resultsrequiredobject

One answer per input string that was classified, keyed by the string as you sent it; the ip inside is the normalized form. Each value is exactly what GET /{ip} answers for your plan.

errorsrequiredobject

One entry per input string that could not be classified, keyed the same way. Empty when every entry was answered.

TypeScript type
ts
{
  results: Record<string, {
    ip: string;
    is_vpn: boolean;
    is_hosting?: boolean;
    is_relay?: boolean;
    is_tor?: boolean;
    is_cdn?: boolean;
    is_resproxy?: boolean;
    is_dcproxy?: boolean;
    is_mobproxy?: boolean;
    is_pubproxy?: boolean;
    vpn?: {
      provider?: string;
      last_seen?: string;
      confidence?: string;
      method?: string;
    };
    hosting?: {
      provider?: string;
      confidence?: string;
      last_seen?: string;
    };
    relay?: {
      provider?: string;
      confidence?: string;
      last_seen?: string;
    };
    tor?: {
      provider?: string;
      confidence?: string;
      last_seen?: string;
    };
    cdn?: {
      provider?: string;
      confidence?: string;
      last_seen?: string;
    };
    resproxy?: {
      provider?: string;
      first_seen?: string;
      last_seen?: string;
      hits?: number;
      hits_days_pct?: number;
      providers_num?: number;
    };
    dcproxy?: {
      provider?: string;
      first_seen?: string;
      last_seen?: string;
      hits?: number;
      hits_days_pct?: number;
      providers_num?: number;
    };
    mobproxy?: {
      provider?: string;
      first_seen?: string;
      last_seen?: string;
      hits?: number;
      hits_days_pct?: number;
      providers_num?: number;
    };
    pubproxy?: {
      entry?: boolean;
      exit?: boolean;
      protocols?: string[];
      num_ports?: number;
      num_proxies?: number;
      last_seen?: string;
      last_usable?: string;
    };
    location?: {
      continent: string;
      continent_name: string;
      country: string;
      country_name: string;
      timezone: string;
      currency_code: string;
      currency_name: string;
      currency_symbol: string;
      flag_emoji: string;
      flag_unicode: string;
    };
    asn?: {
      asn: number;
      name: string;
      domain: string;
      type: string;
      prefix: string;
    };
    trial_expired?: boolean;
  }>;
  errors: Record<string, {
    status: number;
    error: string;
  }>;
}
400application/json

The body is not a batch, or names too few or too many addresses.

errorrequiredstring

A human-readable explanation of the failure.

Example not a valid IP address

TypeScript type
ts
{
  error: string;
}
401application/json

No API key was presented for more than one address, or the one presented is unknown, revoked, or expired.

errorrequiredstring

A human-readable explanation of the failure.

Example not a valid IP address

TypeScript type
ts
{
  error: string;
}
402application/json

The key is on the free plan and the batch names more than one address: a batch needs a paid plan.

errorrequiredstring

A human-readable explanation of the failure.

Example not a valid IP address

TypeScript type
ts
{
  error: string;
}
403application/json

The key restricts which source addresses may use it and this request came from none of them, or the key was sent over plain HTTP, where anyone on the network path could read it. Rotate a key that was.

errorrequiredstring

A human-readable explanation of the failure.

Example not a valid IP address

TypeScript type
ts
{
  error: string;
}
413application/json

The body is larger than any batch of 1000 addresses can be.

errorrequiredstring

A human-readable explanation of the failure.

Example not a valid IP address

TypeScript type
ts
{
  error: string;
}
429application/json

The rate limit refused the whole call. Always transient and always with Retry-After; a reached overage cap is reported per entry in a 200 instead.

errorrequiredstring

A human-readable explanation of the failure.

Example not a valid IP address

TypeScript type
ts
{
  error: string;
}