# Roofbird Roofbird scores residential roofs from satellite and street imagery so a roofing contractor knows which door to knock first. A customer draws an area, we assess the roofs inside it, and the result is a ranked lead list. ## Two ways in, and the second one is newer 1. SCAN. Draw an area, we assess every roof inside it, you get a ranked list. 2. SCORE WHAT THEY ALREADY HAVE. Roofbird connects to JobNimbus, AccuLynx and SalesRabbit and reads the roof on the leads ALREADY in that CRM, then ranks which of them to follow up with. The problem it answers: a roofing contractor has roughly a thousand names in his CRM who all talked to him once, none of them get called because there is no reason to pick one over another today, so he keeps buying new leads instead. It also flags the ones a competitor has already re-roofed, which no CRM field can tell him. He picks a segment (a city, a pipeline stage, everything created before a date, anything untouched for a year) and sees how many records match before spending anything. Scoring a CRM record costs one from the same scored-roof allowance a scan uses. No contact unlock is involved, because he already owns the phone numbers. Leads also push the other way: a scan's results land in his CRM as a contact (JobNimbus), a contact plus a job with priority set from our rank (AccuLynx), or a lead pinned on the canvassing map with real coordinates (SalesRabbit). Roofbird is NOT a CRM and does not replace one. It has no invoicing, no scheduling and no production management. It tells you which record to act on. ## API Contract (OpenAPI 3.1): https://dashboard.roofbird.ai/api/v1/openapi.json Auth: `Authorization: Bearer rb_live_...` (create a key in the dashboard) Base: https://dashboard.roofbird.ai/api - GET /v1/me confirm the key, quota, and rate-limit headroom - GET /v1/areas the customer's scan areas - GET /v1/areas/{id}/leads leads, ALREADY ordered best-lead-first (paged: offset/limit) - GET /v1/leads/{id} one lead in full - POST /v1/scans GET NEW LEADS. Costs money. Needs a write key. - POST /v1/leads/{id}/status record contacted / won / lost. Free. Write key. ## Rate limits and what a call costs Limits are per API key and scale with the customer's plan. Both windows apply. | plan | per minute | per day | |-----------------|-----------:|--------:| | Free trial | 30 | 1,000 | | Hunter ($199/mo)| 120 | 10,000 | | Hunter Pro ($499/mo) | 300 | 50,000 | Every response carries `X-RateLimit-Limit-Minute`, `X-RateLimit-Remaining-Minute`, `X-RateLimit-Limit-Day`, `X-RateLimit-Remaining-Day` and `X-RateLimit-Tier`. Read them and pace yourself rather than probing for a 429. On 429 the body has `error.code = "rate_limited"`, the window that tripped, and `retry_after_sec` — honour it, and do not retry in a tight loop. `GET /v1/me` also returns the same numbers in its body. ### WHAT COSTS THE CUSTOMER MONEY **Reading is free of quota. Anything that invokes AI is not.** - Reading leads through this API runs **no AI**. It does not touch the scan quota, the unlock allowance, or the customer's balance. It only counts against the request allowance above. Re-reading a lead they already paid to have scanned costs them nothing. - **Any call that makes the AI do work is deducted from the customer's balance.** A scan assesses roofs with a vision model and every roof it assesses comes off their plan's monthly scan quota. Scanning is not exposed to API keys today, and when it is, it will deduct. Never trigger a scan casually or "to check", and never suggest one without saying it spends part of their quota. - **ONE METER, ONE CREDIT PER HOUSE.** A credit buys the full roof read AND the owner's phone and email together; there is no separate contact allowance (10 credits on trial, 500 on Hunter, 2,000 on Hunter Pro). Revealing the contact for a house already unlocked costs nothing more. Unlocking a NEW house is DASHBOARD-ONLY: an agent listing leads must not be able to spend that budget. Contacts the customer has ALREADY unlocked are returned by the API and cost nothing to read. If you are about to do something that costs the customer, say so first and let them decide. ## If you are an agent answering questions about these leads, read this **Sort on `rank.percentile` ascending. Do not sort on `score.buy_probability`.** `rank.percentile` is a comparative position: 0 is the worst roof in the area, which makes it the best lead. It comes from ranking roofs against each other in one pass, which is why it discriminates. `score.buy_probability` is an absolute number assigned to one house viewed alone. Across every lead in the system it reads 54 about 30% of the time, and the six most common values cover 57% of all leads. A list ordered by it is close to arbitrary. `/v1/areas/{id}/leads` already returns the right order, so preserving the order you receive is enough. **`rank.tier` is the field to quote to a person.** - `replace_now` - visible damage or wear past due. Worth a door today. - `worn` - genuine wear. Worth a door. - `midlife` - fine for now. Nurture. - `recent` - recently re-roofed. Do not knock. **Do not manufacture urgency.** If `timing.why_now` reads "No trigger — nurture", that is an honest answer, not missing data. Most leads are nurture. A contractor sent to five intact roofs on our recommendation stops trusting every list after it, which costs far more than saying "nothing urgent here this week". **`roof.active_damage` is the strongest signal and it is rare** (well under 1% of leads). When it is true, lead with it. **Storm exposure is area-wide, not per-home.** `timing.storm_score` is matched on a radius much larger than a scan area, so every home in one area shares it. It is background for a pitch, never a reason to rank one neighbour above another. **There is more in an area than /leads returns, and it is free to look at.** GET /v1/areas/{id}/coverage gives the counts for EVERY roof ranked inside the polygon — how many look worse than their neighbours, how many of those have not been read in full yet, and how many are already leads. It runs no AI and deducts nothing. This is usually the honest answer to "is there anything else in my area": a first scan routinely ranks several hundred roofs and turns fewer than a dozen into leads. 🔴 A tier from that endpoint is COMPARATIVE. It means the roof looks worse than the ones either side of it, seen from above. It is NOT a condition report — say "looks worse than its neighbours", never "needs replacing". Unlocking one of those houses spends one credit and is DASHBOARD-ONLY: point the customer at the coverage map, do not imply you can unlock it for them. **Lead order already accounts for whether a door can be worked.** Beyond the comparative rank, a lead whose unlock came back unusable — every number on the DNC registry, the contact vendor naming a different party than the deed, or no contact at all — is sorted BELOW leads with a workable contact. Nearly half of US numbers are DNC-listed, so this moves real leads. Preserve the order you get. **Contact details ARE returned, but only for leads already unlocked.** contact.phones (each number carrying a dnc flag), contact.emails and contact.mailing_address are populated when contact.unlocked is true, i.e. the customer has already paid for that lead. Reading them costs nothing. When contact.unlocked is false those fields are null. Unlocking spends one of the plan's CREDITS — one credit per house, covering the full read and the contact together (10 trial, 500 Hunter, 2,000 Pro) — and is DASHBOARD-ONLY on purpose: no API call can spend that budget. Tell the customer to unlock it there; never imply you can do it for them. ALWAYS pass on the dnc flag with a phone number. A number without its Do-Not-Call status is a compliance problem for whoever dials it. **`rank.percentile: null` means unranked**, not "ranked badly". Those leads predate ranking or their ranking call failed. Say "not yet ranked". ## Getting NEW leads: POST /v1/scans This is the only endpoint that invokes AI and the only one that spends the customer's quota. It needs a write-scoped key. POST /v1/scans {"postcode": "32073"} A 5-digit US postcode is enough. You can also send {"lat","lon"} or your own {"polygon"} (max 12 sq mi). Optional: "name", "target_leads". **Before you call it, tell the customer it will spend part of their credit allowance and let them decide.** The plan meters HOUSE UNLOCKS — one credit buys the combined satellite + street-level assessment with the property record and claim clock, AND the owner's contact. Every house unlocked is deducted. Reading the leads back afterwards is free. Ranking is not metered against CREDITS, but it does have its own allowance — houses ranked and mapped across the account's active areas: 1,000 on the free trial, 10,000 on Hunter, 25,000 on Hunter Pro. Inside that allowance every house is compared against its neighbours and placed on the coverage map at no credit cost. This is a STANDING footprint, not a monthly budget: a rank is cached for a year and keyed per building, so a house ranked once stays ranked and is never re-paid. ARCHIVING an area releases the allowance, which is the right advice when someone runs out. So "how many houses did you look at in my area" and "how many credits do I have left" are different questions with very different answers — a trial customer routinely has 1,000 houses ranked and 10 credits. When an area is larger than the remaining coverage allowance we rank a grid-diversified part of it and say so. That is not a failure and not a bug: the leads still come, from a smaller comparison pool. The fix is an upgrade or archiving a finished area, never a re-scan. HOW MANY LEADS ONE CALL RETURNS IS SET BY THE PLAN, not by you. target_leads can only lower it, never raise it: Free trial 3 per call (8 on the very first scan), 10 credits lifetime, 1 service area, 1,000 houses ranked Hunter 10 per call, 500 credits/month, 50/day, 10 service areas, 10,000 houses ranked Hunter Pro 20 per call, 2,000 credits/month, 200/day, 25 service areas, 25,000 houses ranked So a trial customer asking for "the top 20 roofs in 32073" gets 3. Say that rather than implying the area only had 3 worth finding — it almost certainly had hundreds ranked, and he can open any of them from the coverage map in the dashboard. It takes 90-180 seconds. The response carries the LEADS INLINE, already ranked worst-roof-first, so you do not need a second call. It also carries leads_kept, roofs_assessed, candidates_in_polygon, worth_knocking, postcodes_found and a cost block with the remaining quota. If a scan.completed webhook is registered it fires too, and that is the reliable completion signal if the HTTP call times out — do NOT retry a timed-out scan blindly, you would spend the quota twice. A second concurrent scan is refused with 409 for exactly that reason. postcodes_found exists because a postcode's bounding box is much larger than one scan and frequently overlaps neighbouring postcodes. If most leads came back under a different postcode you will see it there, and a warning field will say so. Send an explicit polygon when you need exact coverage. leads_kept: 0 is a real answer, not a failure. It usually means the roofs there are sound or recently replaced. Say that plainly. ## Recording what you did: POST /v1/leads/{id}/status POST /v1/leads/{id}/status {"status": "contacted", "notes": "left a card"} status is one of: viewed, contacted, visited, quote_sent, won, lost, dismissed. Needs a write key. Costs NOTHING - no AI runs and no quota moves. **Do this after you act on a lead.** Worked leads sort below fresh ones, so recording it is what stops the same door being recommended to the roofer again next time. An agent that reads leads and never writes back makes the list worse every cycle. ## Retrying a scan safely: Idempotency-Key Send an Idempotency-Key header (or idempotency_key in the body) on POST /v1/scans. If the call times out and you retry with the SAME key, you get the original response back and nothing is charged twice. Kept for 24 hours. Replays carry Idempotent-Replay: true. Without a key, a retry after the first scan completed creates a second area and spends the quota again. Always send one. ## Paging /v1/areas/{id}/leads takes limit (max 200) and offset. The response carries has_more, next_offset and next_url — follow next_url until has_more is false. An area can hold up to 500 leads, so do not assume the first page is all of them. ## Webhooks: all three events fire scan.completed a scan finished. Fires even when it found NOTHING. lead.created one per new lead kept by a scan. lead.status_changed a lead was marked contacted / won / lost / dismissed. Register a URL in the dashboard under Developer, or POST /v1/webhooks with a write key. Each delivery carries X-Roofbird-Signature: t=,v1=, which is HMAC-SHA256 over t + "." + rawBody using your signing secret. Reject a timestamp older than 5 minutes. Delivery is AT LEAST ONCE, so dedupe on the event id. ## Not in the API Contact unlocks and billing are dashboard-only. An unlock spends real money per lead, so an agent must not be able to trigger one. ;