Oscar Property API

Look up a property by address. Get our measurements of it, and a map of the property with those areas highlighted to show on your own website.

Base URLhttps://api.dev.oscardata.com

Guide

Get a key and make your first call. The rules here apply to every route.

Getting started

  1. Create an API key. In Oscar, open Company → API keys and create a key. A company admin or head can do this. Copy the key: it is shown once. If your company has no admin in Oscar, ask Oscar support for a key.

  2. Call the API from your server. Send the key in the Authorization header. These examples read it from the OSCAR_API_KEY environment variable.

    curl -G https://api.dev.oscardata.com/api/v1/property \
      -H "Authorization: Bearer $OSCAR_API_KEY" \
      --data-urlencode "address=123 Example Street, Anytown, NY 10001" \
      -d fields=lawn \
      -d layers=lawn,property
    // Node.js 18 or later, as an ES module.
    const params = new URLSearchParams({
      address: "123 Example Street, Anytown, NY 10001",
      fields: "lawn",
      layers: "lawn,property",
    });
    const res = await fetch(`https://api.dev.oscardata.com/api/v1/property?${params}`, {
      headers: { Authorization: `Bearer ${process.env.OSCAR_API_KEY}` },
    });
    if (!res.ok) throw new Error(`Oscar API returned ${res.status}`);
    const result = await res.json();
    console.log(result.metrics.lawn.area, result.embed.url);
    # pip install requests
    import os
    
    import requests
    
    res = requests.get(
        "https://api.dev.oscardata.com/api/v1/property",
        headers={"Authorization": f"Bearer {os.environ['OSCAR_API_KEY']}"},
        params={
            "address": "123 Example Street, Anytown, NY 10001",
            "fields": "lawn",
            "layers": "lawn,property",
        },
        timeout=10,
    )
    res.raise_for_status()
    result = res.json()
    print(result["metrics"]["lawn"]["area"], result["embed"]["url"])

    The response:

    {
      "property_id": "3f9c2a51-7d4e-4b8a-9c1f-2e6d8b0a4c17",
      "matched_address": "123 EXAMPLE ST, Anytown, NY 10001",
      "match": { "quality": "exact", "reasons": [] },
      "metrics": {
        "lawn": {
          "area": 5200,
          "unit": "sqft",
          "percent_of_property": 38.5
        }
      },
      "embed": {
        "url": "https://dev.oscardata.com/embed/3f9c2a51-…?t=…",
        "expires_at": "2026-10-06T17:00:00Z"
      }
    }
  3. Show the map. Put embed.url from the response in an iframe on your page:

    <iframe src="EMBED_URL" width="640" height="420" title="Property map"></iframe>

    If you add a sandbox attribute, include allow-scripts allow-same-origin. Without allow-same-origin the map cannot load.

API keys

  • Keep keys on your server. Never put a key in a web page or an app.
  • A key can read the properties in your company's ZIP codes, nothing else.
  • Revoke a key on the same page. It stops working at once, and so do the map links it made.
  • A key works only on https://api.dev.oscardata.com.

Rate limits

LimitCountsOver the limit
60 requests a minuteEach API key429 with a Retry-After header: the number of seconds to wait.
300 requests a minuteEach IP address 429 without Retry-After.
120 loads a minuteEach map link (embed.url) The map shows an error and sends an error message with status: 429.

Plan for 60 requests a minute per key. Do not depend on getting more.

Handling a 429

  • If the response has Retry-After, wait that many seconds, then send the same request again.
  • If it has no Retry-After, wait 1 second, then 2, then 4, and so on, up to 30 seconds. Add a random part to each wait, so that your servers do not all try again at the same time.
  • Try again on 429 and 503 only. Other errors do not go away when you try again: fix the request.
  • Stop after a few tries and show your user a message.
# --retry tries again on 429 and 503, and waits for Retry-After.
curl -G https://api.dev.oscardata.com/api/v1/property \
  --retry 4 --retry-max-time 60 \
  -H "Authorization: Bearer $OSCAR_API_KEY" \
  -d property_id=PROPERTY_ID \
  -d fields=lawn
async function getProperty(params, tries = 5) {
  const url = `https://api.dev.oscardata.com/api/v1/property?${new URLSearchParams(params)}`;
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${process.env.OSCAR_API_KEY}` },
    });
    const retry = res.status === 429 || res.status === 503;
    if (!retry || attempt === tries) {
      if (!res.ok) throw new Error(`Oscar API returned ${res.status}`);
      return res.json();
    }
    const retryAfter = Number(res.headers.get("Retry-After"));
    const backoff = Math.min(30, 2 ** (attempt - 1)) * (0.5 + Math.random());
    const seconds = retryAfter > 0 ? retryAfter : backoff;
    await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  }
}
import os
import random
import time

import requests


def get_property(params, tries=5):
    for attempt in range(1, tries + 1):
        res = requests.get(
            "https://api.dev.oscardata.com/api/v1/property",
            headers={"Authorization": f"Bearer {os.environ['OSCAR_API_KEY']}"},
            params=params,
            timeout=10,
        )
        if res.status_code not in (429, 503) or attempt == tries:
            res.raise_for_status()
            return res.json()
        retry_after = res.headers.get("Retry-After", "")
        if retry_after.isdigit():
            seconds = int(retry_after)
        else:
            seconds = min(30, 2 ** (attempt - 1)) * random.uniform(0.5, 1.5)
        time.sleep(seconds)

Staying under the limits

  • Store property_id after the first lookup, and send it next time instead of address.
  • Call the API when your user asks for a property, not on every page view.
  • An embed.url works for one hour. Reuse it in that hour instead of asking for a new one.
  • For a batch job, send at most one request a second per key.

Errors

An error returns an HTTP status and a JSON body with detail. Usually detail is a sentence; for some errors it is an object with a code you can check and a message:

{ "detail": "Invalid API key" }
{ "detail": { "code": "account_archived", "message": "Your company's Oscar account is archived." } }

These can come from any route. Each route lists its own errors too.

StatusMeaning
401The API key is missing, wrong or revoked.
403Your company's Oscar account is archived (account_archived).
429Too many requests. Wait, then try again. See Rate limits.
503A service we depend on is not available. Try again later.

Routes

Each route: what you send, what comes back, and its own errors.

GET /property

GEThttps://api.dev.oscardata.com/api/v1/property

One property: the measurements you ask for in fields, and a map link for the layers you ask for in layers.

Request

This route takes a header and query parameters. It has no path parameters and no body.

Headers
NameDescription
Authorization
required
Bearer and your API key: Authorization: Bearer osk_…. See API keys.
Query parameters

Give exactly one of address or property_id, and at least one of fields or layers.

NameDescription
address
string
A US street address, for example 123 Example Street, Anytown, NY 10001.
property_id
string
property_id from an earlier response.
fields
string
Comma-separated measurements to return. Available: lawn. See Fields.
layers
string
Comma-separated layers for the map. Available: lawn, property. See Layers.
units
string
Units for areas: sqft (default) or m2.
Fields

fields chooses which measurements come back as JSON data in the response body. Each name you send comes back as its own object in metrics:

{
  ...
  "metrics": {
    "FIELD_NAME": { /* the field's properties, listed below */ }
  }
}

We add fields over time. These are available today.

lawn

If we have no lawn measurement for the property, the request fails with 422.

Response propertyDescription
area
number
The lawn area of the property, in unit: the property minus buildings, driveways, sidewalks and other paving. It includes grass under trees, so the true grass area can be smaller. Whole square feet, or square metres to one decimal place.
unit
string
The unit of area: sqft (square feet) or m2 (square metres). It is what you sent in units, or sqft if you did not send units.
percent_of_property
number
How much of the property is lawn, as a percent from 0 to 100, to one decimal place. For example, 38.5 means the lawn covers 38.5 % of the property's area.
Layers

layers chooses what the map shows. Layers do not come back as data: they are drawn on the map. When you send layers, the response has embed, a link to a map of the property with those layers on it. See Embedded map. To get a measurement as data, use Fields.

We add layers over time. These are available today.

lawn

The lawn, as a filled lime-green area. If we have no lawn measurement for the property, the request fails with 422.

property

The property boundary, as a magenta outline. It can include the strip of land out to the road.

Response

Body

A JSON object. property_id and matched_address always come back. The other sections come back only when you ask for them, as each row says. For address=…&fields=lawn&layers=lawn,property:

{
  "property_id": "3f9c2a51-7d4e-4b8a-9c1f-2e6d8b0a4c17",
  "matched_address": "123 EXAMPLE ST, Anytown, NY 10001",
  "match": { "quality": "exact", "reasons": [] },
  "metrics": {
    "lawn": {
      "area": 5200,
      "unit": "sqft",
      "percent_of_property": 38.5
    }
  },
  "embed": {
    "url": "https://dev.oscardata.com/embed/3f9c2a51-…?t=…",
    "expires_at": "2026-10-06T17:00:00Z"
  }
}
FieldDescription
property_id
string (UUID)
Our ID for the property. It never changes for the same property. Store it and send it as property_id next time: you get the same property back, without an address lookup.
matched_address
string
The property's address in our records, for example 123 EXAMPLE ST, Anytown, NY 10001. It can be written differently from the address you sent (capitals, abbreviations, town name). Show it to your user so they can check we found the right property.
match
object
How well the address you sent matches the property we found. Only when you send address, not property_id.
match.quality
string
exact, approximate or low. With exact, every check passed. With approximate or low, ask your user to confirm the property. See Match quality.
match.reasons
array of strings
The checks that failed, for example ["number_mismatch"]. Empty when match.quality is exact. See Match reasons for each value.
metrics
object
The measurements you asked for in fields, one object for each name. Only when you send fields. Each field in Fields lists the properties of its object.
embed
object
A link to a map of the property that shows the layers you asked for. Only when you send layers. See Embedded map.
embed.url
string (URL)
The map page. Put it in the src of an <iframe> on your page. It needs no API key, so it is safe to use in a browser.
embed.expires_at
string (date-time, UTC)
When embed.url stops working: one hour after your request, for example 2026-10-06T17:00:00Z. After that the map shows "This map link has expired." Call the API again for a new link.
Match quality

When you send address, we always return our best match in your ZIP codes and grade it in match.quality:

QualityMeaning
exactEvery check passed.
approximateWe found a property, but some checks failed. match.reasons lists them. It is often a neighbouring property: ask your user to confirm before you use the numbers.
lowFound by our own address search. Ask your user to confirm before you use the numbers.

Show matched_address so your user can check it. Store property_id and send it instead of address next time: it always returns the same property, and it skips the address lookup.

Match reasons
ReasonMeaning
partial_matchOnly part of the address matched.
interpolatedThe address is placed along the street, not on a building.
no_house_numberThe address has no house number.
number_mismatchThe house number of the property we found is different.
postal_mismatchThe ZIP code of the property we found is different.
nearest_lotThe address is not inside a property, so we took the nearest one (within 50 m).
text_searchFound by our own address search, with the same house number. Always comes with low, and with postal_mismatch when the ZIP differs.
unitThe address is a unit in a building, such as an apartment or condo. A building can hold many units, so we may have found another unit in the same building. A unit has no lawn of its own.
Embedded map

When you send layers, the response has embed.url. It shows one property, read-only, for one hour. Call the API again for a new link. The map shows the layers you asked for, with a legend.

Messages to your page

The map page sends messages to your page with window.postMessage:

window.addEventListener("message", (event) => {
  if (event.data?.source !== "oscar") return;
  // { source: "oscar", v: 1, type: "ready", propertyId }
  // { source: "oscar", v: 1, type: "measurement", layer: "lawn", area, unit }
  // { source: "oscar", v: 1, type: "error", status }  (410: the link expired)
});

A map load ends in one ready or one error, so you can hide a loading state on either. ready comes once the map has drawn. Browsers draw an iframe only while it is on screen, so for a map below the fold, in a closed tab or hidden, ready comes when your user first sees it. measurement does not wait for that: it comes as soon as the map's data has loaded, before ready. status is 410 when the link expired, another HTTP status when the map data could not load, and 0 when the link is broken or the map itself could not load.

Errors

Besides the errors every route can return:

StatusMeaning
400Bad parameters: an unknown field, layer or unit, both or neither of address and property_id, or no fields and no layers.
404We found no property in your company's ZIP codes.
422We found the property, but we have no measurement for a field or layer you asked for. detail.code is not_measured, detail.missing lists the names, and detail.property_id is the property we found.

For API tools such as Postman, the OpenAPI schema is at /api/v1/openapi.json.