# Keep your CRM's company records current

Refresh every account's size, industry and headquarters on a schedule. Pay the cache price for recent data, and fetch fresh only when you need to.

## How it works

1. Once a week, take the accounts in your CRM that have a website.
2. Look each one up by domain, and say how old an answer may be, for example 30 days. When our copy is
   younger than that, it comes from our cache at the cache price.
3. Every answer carries a fingerprint of the company's data. Compare it with the one you stored last
   time, and write to your CRM only when it changed.
4. Store the company's numeric id as its key. It stays the same when a company renames, and a website, an
   old name and a LinkedIn company URL all lead back to it.

```bash tab="cURL"
curl https://api.happyfetch.net/v1/domain --max-time 120 \
  -H "Authorization: Bearer $HAPPYFETCH_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"domain":"stripe.com"}'
```

```javascript tab="JavaScript"
const res = await fetch("https://api.happyfetch.net/v1/domain", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.HAPPYFETCH_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(), // send the same key when you retry this call
  },
  body: JSON.stringify({
    "domain": "stripe.com"
  }),
  signal: AbortSignal.timeout(120_000),
});
const { data, meta, error } = await res.json();
if (!res.ok) throw new Error(`${error.code}: ${error.message}`);
console.log(meta.request_id, meta.cache.status, data);
```

```python tab="Python"
import os, uuid, requests

res = requests.post(
    "https://api.happyfetch.net/v1/domain",
    headers={
        "Authorization": f"Bearer {os.environ['HAPPYFETCH_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),  # send the same key when you retry this call
    },
    json={
      "domain": "stripe.com"
    },
    timeout=120,
)
body = res.json()
if not res.ok:
    raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["meta"]["request_id"], body["meta"]["cache"]["status"])
```

## The weekly job

```python
import os
import time
import uuid

import requests

API = "https://api.happyfetch.net"
HEADERS = {"Authorization": f"Bearer {os.environ['HAPPYFETCH_API_KEY']}"}
THIRTY_DAYS = 30 * 24 * 3600


def refresh(domain: str, last_hash: str | None) -> dict | None:
    """Returns the company when it changed since last_hash, else None."""
    key = str(uuid.uuid4())  # the same key on every retry, so a retry never pays twice
    for attempt in range(5):
        try:
            res = requests.post(
                f"{API}/v1/domain",
                headers={**HEADERS, "Idempotency-Key": key},
                json={"domain": domain, "max_age": THIRTY_DAYS},
                timeout=120,
            )
            body = res.json()
        except (requests.RequestException, ValueError):
            time.sleep(2**attempt)  # a timeout or a dropped connection: retry with the same key
            continue
        if res.ok:
            meta = body["meta"]
            if meta["content_hash"] == last_hash:
                return None  # nothing changed
            key = meta["canonical"]["id"] or meta["canonical"]["slug"]  # the id never changes
            return {"key": key, "hash": meta["content_hash"], **body["data"]}
        error = body["error"]
        if not error["retryable"]:
            return None  # not_found, invalid_domain, insufficient_credits: see error["code"]
        time.sleep(error.get("retry_after_seconds") or 2**attempt)
    return None
```

Every error says whether retrying helps, and how long to wait when you're over your rate limit. The job
above follows both.

## What it costs

| Lookups a month | Smallest pack (Starter) | Biggest pack (Business) |
|---|---|---|
| 1,000 lookups by website domain | $2.00 | $1.00 |
| 10,000 lookups by website domain | $20.00 | $10.00 |
| 100,000 lookups by website domain | $200.00 | $100.00 |

Every lookup fetched fresh, at 4 credits each. Lookups our cache answers cost half. A confirmed not-found costs the same, and nothing from our cache. Starter: $10 for 20,000 credits. Business: $500 for 2,000,000 credits. Credits never expire.

Accounts we already hold, recent enough for you, come from our cache at half the price:
$0.50 per 1,000 domain lookups on our biggest pack. If your CRM stores each
company's LinkedIn URL, look up by URL instead: $0.50 per 1,000, and
$0.25 from our cache.

## Check before you pay, for free

The [cache check](/docs/cache) tells you how old our copy of a company is, and what a lookup would cost
right now. It's free, so a job can ask first and decide per account.

## Good to know

- **Fresh on demand.** Ask for a fresh fetch for the accounts that matter most this week, like the ones
  in an open deal. Everything else can come from the cache.
- **Any age you like.** There's no limit on how old an answer may be. Accept an older copy and anything we
  hold comes back at the cache price; only what we've never fetched is fetched fresh.
