ravn
Integrations · Python

Ravn from Python, with requests

POST JSON to https://api.askravn.com/v1/tools/<tool> with requests and a Bearer header, then branch on the status field: ok carries results, not_found is a free miss, and a 429 tells you how long to wait in its Retry-After header.

Start free: 1,000 lookups Read the docs

Setup

Before you write code

  1. / 01

    Install requests

    pip install requests. No Ravn SDK is needed; the API is plain JSON over HTTPS.

  2. / 02

    Put the key in the environment

    export RAVN_API_KEY=rw_live_… locally, or your platform’s secret store in production. Never hard-code it.

  3. / 03

    Read the schemas

    GET https://api.askravn.com/v1/tools returns every tool with its JSON Schema, so you can validate inputs before sending.

  4. / 04

    Paste the client below

    It handles the three cases that matter: data, a free miss, and a wait.

Code

ravn_client.py

import os
import time
import requests

API = "https://api.askravn.com/v1/tools/"
SESSION = requests.Session()
SESSION.headers["Authorization"] = "Bearer " + os.environ["RAVN_API_KEY"]


class RavnError(Exception):
    def __init__(self, http, code, message):
        super().__init__(f"{http} {code}: {message}")
        self.http, self.code = http, code


def call(tool, payload, max_wait=300, tries=4):
    """Returns results dict/list, or None on a free not_found miss."""
    for attempt in range(tries):
        r = SESSION.post(API + tool, json=payload, timeout=60)
        if r.status_code == 429:
            wait = int(r.headers.get("Retry-After", "30"))
            if wait > max_wait:  # 5-hour window is full: don't sleep for hours
                raise RavnError(429, r.json()["error"]["code"], f"retry in {wait}s")
            time.sleep(wait)
            continue
        if r.status_code == 503 and attempt < tries - 1:
            time.sleep(2 ** attempt)
            continue
        body = r.json()
        if r.status_code != 200:
            err = body["error"]
            raise RavnError(r.status_code, err["code"], err["message"])
        return body["results"] if body["status"] == "ok" else None
    raise RavnError(r.status_code, "gave_up", "retries exhausted")


if __name__ == "__main__":
    v = call("validate_email", {"email": "jane@acme.com"})
    if v is None:
        print("no verdict")
    elif v["status"] == "valid" and v["risk_score"] in ("A", "B"):
        print("send")
    elif v["status"] == "risky":
        print("review:", v["reason"])  # e.g. catch_all_domain
    else:
        print("drop:", v["reason"])

    for v in call("validate_emails", {"emails": ["a@acme.com", "b@acme.com"]}) or []:
        print(v["email"], v["status"], v["risk_score"])
Recipes

Patterns that hold up

  1. / 01

    Batch, don’t loop

    For lists, send up to 1,000 addresses to validate_emails per call. Duplicates are counted once, and you make far fewer requests.

  2. / 02

    Connect a provider key for people calls

    people search and other people, company and carrier tools run on your own provider key (People Data Labs or Twilio). Connect it with ravn keys add pdl (or twilio) so those calls do not return 403 provider_key_required; our own checks need no key.

  3. / 03

    Log the request id

    Every response has metadata.request_id. Store it with your record; it is the fastest way for us to trace a question about a specific answer.

Good to know

Reading the envelope

Every tool returns the same envelope: status, results and metadata. metadata.fair_use.lookups_remaining_5h tells you how much of the window is left, so a long job can slow itself down before it ever sees a 429. A 403 provider_key_required means a people, company or carrier tool needs your own provider key connected.

Errors come back with a non-200 status and {"error": {"code", "message"}}. The client above turns those into a RavnError with the code attached, so your code can catch provider_key_required differently from invalid_input. The full list is in the error reference, and the limits section covers the 1 MB body cap.

FAQ

Common questions.

All questions →

Can I run calls in parallel threads?

Yes, but a small pool is enough, and batching with validate_emails usually beats threads. If you do go parallel, watch lookups_remaining_5h in the metadata and back off before the window runs out.

Is there an official Python SDK?

No, and you do not need one. Each tool is one POST with a JSON body, and the response shape is the same for all 25 tools, so the client above covers all of them.

How long can Retry-After be?

A short rate limit asks for seconds. If the 5-hour fair-use window is used up, it can be much longer. That is why the client raises instead of sleeping past max_wait; schedule the job to resume, or see how the window works.

Which errors are safe to retry?

Retry 429 after the wait and 503 with backoff. Do not retry 401, 403, 404, 413 or 422: they will fail the same way until you change the API key, connect a provider key, or fix the tool name or input.