Skip to content

API reference/ip

Look up up to 1,000 addresses in one call

POST/api/v1/ip/batch

authentication
Secret API key as a bearer token
billing
Each address bills like a single lookup — 1 credit per 5 fresh answered lookups, prepaid ones used first; reserved and private ranges, unallocated space, unknown addresses and 7-day repeats free — and the call stops cleanly where the balance ends.

Runs the single-address lookup for every item, in input order, under the exact single-lookup billing rules. Every answer comes from local databases and compiled feeds, so a full batch of 1,000 takes well under a second. data.results[] mirrors the single-address response per item and each item's meta carries credits_used, cache_hit and ip_lookups_prepaid. One credit pays for five answered addresses, so most items cost 0: an item that opens a block costs 1 and leaves 4 prepaid, and the lookups the account already held prepaid are used first.

Repeats inside the list are looked up once and answer as cache hits. The list is walked in order and stops where the balance would run out: running out of credits mid-batch returns the paid partial results with meta.stopped_reason: "insufficient_credits" and processed lower than requested, never discarding billed work. Only a batch whose first lookup is refused answers the typed 402. A key with a daily credit cap stops the list the same way once the cap is spent and the lookups its last credit bought are used up: the addresses already paid for come back with meta.stopped_reason: "key_spend_cap", and a call that starts with the cap already spent answers 429 KEY_SPEND_CAP_REACHED and runs nothing. An item that is not an IP address fails validation for the whole request.

A batch counts as one request against the rate limit. privacy applies to every address in the list.

Every item carries index, its zero-based position in the list you sent, and input, the address exactly as you sent it — the answer's ip is its canonical form. A batch stopped early by stopped_reason answers only a prefix of the list, so those two are what line an answer up with the row it came from.

Request body

field type description
ipsrequiredstring[]1 to 1,000 IPv4 or IPv6 addresses.
privacyboolean | nullKeep nothing about these lookups; every address is billed as a fresh lookup.

Example request

curl -X POST https://spaw.co/api/v1/ip/batch \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "ips": [
    "8.8.8.8",
    "103.124.165.2",
    "192.168.1.1"
  ]
}'
const response = await fetch('https://spaw.co/api/v1/ip/batch', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk_live_…',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "ips": [
      "8.8.8.8",
      "103.124.165.2",
      "192.168.1.1"
    ]
  }),
});
const result = await response.json();
import requests

response = requests.post(
    'https://spaw.co/api/v1/ip/batch',
    headers={'Authorization': 'Bearer sk_live_…'},
    json={
        'ips': [
            '8.8.8.8',
            '103.124.165.2',
            '192.168.1.1'
        ]
    },
)
result = response.json()
$ch = curl_init('https://spaw.co/api/v1/ip/batch');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => ['Authorization: Bearer sk_live_…', 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'ips' => [
            '8.8.8.8',
            '103.124.165.2',
            '192.168.1.1'
        ]
    ]),
]);
$result = json_decode(curl_exec($ch), true);
from spaw import Client

client = Client('sk_live_…')
result = client.lookup_ips([
    '8.8.8.8',
    '103.124.165.2',
    '192.168.1.1'
])
import Spaw from 'spaw';

const spaw = new Spaw({ apiKey: 'sk_live_…' });
const result = await spaw.lookupIps([
    '8.8.8.8',
    '103.124.165.2',
    '192.168.1.1'
]);
use Spaw\Client;

$spaw = new Client('sk_live_…');
$result = $spaw->lookupIps([
    '8.8.8.8',
    '103.124.165.2',
    '192.168.1.1'
]);

This endpoint has no console on its page. It bills once per item in the list, so what one run costs depends on what you paste in. The single-value endpoint for the same signal is runnable instead.

Responses

200One result per processed address, plus the batch totals.

{
    "success": true,
    "data": {
        "results": [
            {
                "index": 0,
                "input": "8.8.8.8",
                "data": {
                    "ip": "8.8.8.8",
                    "version": 4,
                    "reason": null,
                    "country": "US",
                    "is_datacenter": true,
                    "risk_score": 40,
                    "risk_level": "medium"
                },
                "meta": {
                    "credits_used": 1,
                    "cache_hit": false,
                    "ip_lookups_prepaid": 4
                }
            },
            {
                "index": 1,
                "input": "103.124.165.2",
                "data": {
                    "ip": "103.124.165.2",
                    "version": 4,
                    "reason": null,
                    "country": "AL",
                    "is_vpn": true,
                    "privacy_service": "Mullvad",
                    "risk_score": 70,
                    "risk_level": "high"
                },
                "meta": {
                    "credits_used": 0,
                    "cache_hit": false,
                    "ip_lookups_prepaid": 3
                }
            },
            {
                "index": 2,
                "input": "192.168.1.1",
                "data": {
                    "ip": "192.168.1.1",
                    "version": 4,
                    "reason": "reserved_range",
                    "country": null,
                    "risk_score": null,
                    "risk_level": null
                },
                "meta": {
                    "credits_used": 0,
                    "cache_hit": false,
                    "ip_lookups_prepaid": 3
                }
            }
        ]
    },
    "meta": {
        "requested": 3,
        "processed": 3,
        "credits_used": 1,
        "credits_remaining": 8,
        "ip_lookups_prepaid": 3,
        "stopped_reason": null,
        "request_id": "req_01m1kgdrtqdvwnks99vfgx2rcw"
    }
}

401The key is missing, malformed, or revoked.

{
    "success": false,
    "error": {
        "code": "UNAUTHENTICATED",
        "message": "Provide a valid API key as a bearer token.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

402The balance is empty. The lookup did not run.

{
    "success": false,
    "error": {
        "code": "INSUFFICIENT_CREDITS",
        "message": "Your credit balance is empty.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

422The request body could not be validated; `error.errors` lists the fields.

{
    "success": false,
    "error": {
        "code": "VALIDATION_FAILED",
        "message": "The email field is required.",
        "errors": {
            "email": [
                "The email field is required."
            ]
        },
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

429Over 5 requests per second for the key. Retry after the limit resets.

{
    "success": false,
    "error": {
        "code": "RATE_LIMITED",
        "message": "Too many requests. Retry after the limit resets.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

Error codes

Failures answer { success: false, error: { code, message, request_id } }. Each code has its own page.

markdown version·openapi.json