# RoofIgnite supply API

For lead sellers. Two questions, answered in one call each:

1. Do you have a contractor in this ZIP who can take a lead right now?
2. Will you pay this much for a lead there?

Base URL: `https://coverage.roofignite.com/api/coverage`
No API key. No account. CORS is open, so it works from a browser, a script, or an agent.
Rate limit: 120 requests per minute per IP.

Nothing about a homeowner ever goes into this API. Do not send names, phones, emails or addresses to it.

---

## 1. Check one ZIP

```bash
curl "https://coverage.roofignite.com/api/coverage?action=check&zip=48505"
```

```json
{ "ok": true, "zip": "48505", "covered": true, "accepting": true, "state": "MI" }
```

- `covered` — a contractor of ours has this ZIP in their service area.
- `accepting` — that contractor can take a lead today, meaning they are not paused and not at their daily cap.

`covered: true, accepting: false` means come back tomorrow, not never.

## 2. Check many ZIPs at once

Up to 200 per call. This is the fast way to map your out-of-area volume against our coverage.

```bash
curl -X POST https://coverage.roofignite.com/api/coverage \
  -H 'Content-Type: application/json' \
  -d '{"action":"bulk","zips":["48505","38632","90210","33024"]}'
```

```json
{ "ok": true, "count": 4, "accepting": 2,
  "results": {
    "48505": { "covered": true,  "accepting": true,  "state": "MI" },
    "38632": { "covered": true,  "accepting": true,  "state": "MS" },
    "33024": { "covered": false, "accepting": false, "state": "FL" },
    "90210": { "covered": false, "accepting": false, "state": null }
  } }
```

## 3. Propose a price

You name the price. We answer accept or decline. We do not publish what we pay, because it varies by ZIP and by which contractor is covering it.

```bash
curl -X POST https://coverage.roofignite.com/api/coverage \
  -H 'Content-Type: application/json' \
  -d '{"action":"quote","zip":"48505","price":60}'
```

```json
{ "ok": true, "quote_id": "q_4cwvf8lr", "zip": "48505", "state": "MI",
  "price_usd": 60, "decision": "accept", "reason": "ok",
  "covered": true, "accepting": true,
  "expires_at": "2026-09-09T19:41:29.691Z", "binding": false }
```

`decision` is `accept` or `decline`. `reason` is one of:

| reason | meaning |
|---|---|
| `ok` | we will buy at that price |
| `not_covered` | no contractor of ours in that ZIP, send it to a marketplace |
| `at_capacity_today` | we cover it, but today's cap is full |
| `above_our_price_here` | covered and open, your price is higher than we pay in that ZIP. Propose lower. |

A quote is **indicative and not binding**. It tells you whether it is worth a conversation. Leads are only accepted from sellers with a signed one-page agreement and an API key, because that is where the consent, returns and payment terms live.

---

## 4. Ping/post: sell us a lead at a per-lead price (accounts with a key)

For sellers with a signed agreement and a key. You ping with the lead's attributes (no personal data), we answer with a bid, you post the full lead with the `bid_id` inside 90 seconds. Full posting spec: https://coverage.roofignite.com/publishers (section 9).

**Ping** `POST https://coverage.roofignite.com/api/ping`

| field | required | notes |
|---|---|---|
| `publisher_key` | yes | your key (also header `X-Publisher-Key`) |
| `zip` | yes | 5 digits |
| `project_type` | no | Replace, Repair, Install, Unsure |
| `home_owner` / `own_home` | no | Yes / No |
| `property_type` | no | Single-family home, Townhouse, Mobile home, Commercial |
| `roof_age` | no | Under 10 years, 10 - 15 years, Over 15 years, Unsure |
| `time_frame` / `timeline` | no | ASAP, Within 1 Week, Within 2 Weeks, Within A Month, Flexible |
| `niche` | no | default `roofing` |
| `phone_sha256` | no | sha256 hex of the 10-digit phone; lets us answer `duplicate` before you post |
| `test` | no | `1` = test ping |

Never send a name, phone, email or street address in a ping: 400 `pii_not_allowed`.

```bash
curl -X POST https://coverage.roofignite.com/api/ping -H 'Content-Type: application/json' \
  -d '{"publisher_key":"pk_yourkey","zip":"49503","project_type":"Replace","home_owner":"Yes","time_frame":"ASAP"}'
```

```json
{ "ok": true, "bid_id": "bid_3f9c...", "bid": 72, "currency": "USD", "mode": "live", "test": false,
  "expires_at": "2026-10-08T19:41:29.691Z", "reason": null }
```

- `bid` > 0: what we pay if you post this lead before `expires_at` (90 s). `bid: 0` = pass.
- `reason` on a pass: `zip_not_covered`, `no_capacity`, `zip_not_in_your_list`, `zip_not_priority`, `daily_cap_reached`, `duplicate`, `blocked_by_rule`, `below_min_bid`, `bidding_off`.
- `mode: "shadow"`: we are measuring; the bid is information only and posts are paid at your agreed fixed price.
- Timing: answered in about 300 ms. A 503 means unknown, not a pass: retry once.

**Post** `POST https://coverage.roofignite.com/api/lead`: the normal lead post plus `"bid_id"`. Same lead as the ping (ZIP, project type, phone if you sent `phone_sha256`). Every normal check still runs. Extra refusals (all 409 unless noted, none paid): `bid_expired`, `bid_required`, `bid_unknown`, `bid_used`, `bid_was_pass`, `bid_lead_mismatch`, 403 `supplier_not_approved`, 422 `consent_not_approved`. Accepted response adds `bid_id` and `price`.

**Sandbox** (no key, nothing stored, deterministic): `POST /api/sandbox/ping` and `POST /api/sandbox/post`, same bodies. Bid = 40 + (sum of ZIP digits mod 40); ZIP 9xxxx = `zip_not_covered`; ZIP ending 0 = `no_capacity`; a post in a ZIP ending 7 = `duplicate`.

```bash
curl -X POST https://coverage.roofignite.com/api/sandbox/ping -H 'Content-Type: application/json' -d '{"zip":"49503","project_type":"Replace"}'
# -> { "ok": true, "sandbox": true, "bid_id": "sbx_49503_...", "bid": 61, ... }
curl -X POST https://coverage.roofignite.com/api/sandbox/post -H 'Content-Type: application/json' \
  -d '{"bid_id":"sbx_49503_...","zip":"49503","first_name":"Jane","phone":"6165551234","email":"jane@example.com","project_type":"Replace","landing_page_url":"https://yoursite.com/roof","trustedform_cert_url":"https://cert.trustedform.com/<40 hex>","consent_text":"... Vetted Home Pros ..."}'
# -> { "ok": true, "sandbox": true, "status": "accepted", "price": 61, ... }
```

---

## Errors

| status | body | meaning |
|---|---|---|
| 400 | `bad_zip` | five digit US ZIP codes only |
| 400 | `bad_price` | price is USD per lead, greater than zero |
| 400 | `too_many` | more than 200 ZIPs in one bulk call |
| 429 | `rate_limited` | back off 60 seconds |
| 503 | `unavailable` | our side is down, retry, do not treat as "not covered" |

Treat a 503 as unknown, never as a decline. That is the one failure mode that would cost you money.

---

## What happens after an accept

1. You tell us the volume and states you have, we send a one-page agreement.
2. We issue an API key and a posting spec, about half a day of work for a developer. A webhook or an emailed batch is fine for a first round.
3. Leads must carry consent text, timestamp and a certificate. We verify the phone on our side too.
4. Returns: 48 hours, defined reasons, reviewed by a person. A homeowner not answering the phone is not a return.
5. Weekly statement, closed Sunday, paid within 7 days. No prepay, no wallet, no card on file.

Contact: the coverage page at https://coverage.roofignite.com

---

## Drop this into Claude Code

Save this file in your project and give your agent this instruction:

> Read `supply-api.md`. Using the RoofIgnite supply API, take my out-of-area ZIP list in `<your file>`, call the bulk endpoint in batches of 200, and tell me which ZIPs they can take leads in today. Then for the ZIPs that come back accepting, call the quote endpoint at the price I currently get from my aggregator plus $15 and report which ones accept. Do not send any homeowner data to this API. Treat a 503 as unknown rather than a decline, and respect the 120 requests per minute limit.

The API is read-only and takes no personal data, so an agent can safely explore it without a key.
