WEMo API

Run WEMo from your own scripts. Send a list of sites, and get back wave height, representative wave energy (RWE), orbital velocity and bed shear stress for each one, as JSON or as CSV, GeoJSON and GeoTIFF files. It is the same model, data and limits as the web app, so you can automate repeat runs, feed results straight into R, Python or a GIS, or build WEMo into your own tools.

Personal API keysMake and revoke them on your projects page. No extra sign-up.
Plain HTTPS and JSONWorks from Python, R, curl, or anything that can send a web request.
Same model as the appGlobal depths, decades of hourly wind, buoys and stations built in.
Free in early accessUp to 50,000 sites a run, like every account.

1. Get an API key

  1. Sign up for a free WEMo account, or log in.
  2. On your projects page, find API keys, give the key a name (for example "Laptop notebook" or "Lab server") and choose Create key.
  3. Copy the key straight away. It starts with wemo_ and is shown only once; WEMo keeps only a scrambled (hashed) copy, so nobody, including us, can show it to you again.

Send the key with every request in an Authorization header:

Authorization: Bearer wemo_your-key-here

Keep keys private, like a password. Anyone with your key can run WEMo as you and read your results. Don't put keys in shared notebooks, emails or public code; read them from an environment variable instead. If a key leaks, revoke it on your projects page; it stops working at once. You can have up to five active keys, one for each computer or script.

2. Your first run

Every run follows the same four steps: send your sites and settings, wait while WEMo fetches depths and wind and runs the model, then read the results or download them as a file. A few sites take under a minute; thousands take a few minutes; a new area also downloads its wind the first time.

Needs the requests package (pip install requests). Set your key first: export WEMO_API_KEY=wemo_... (macOS, Linux) or setx WEMO_API_KEY wemo_... (Windows, then open a new terminal).

import os, time, requests

BASE = "https://wemo.geohorizons.co/api/v1"
H = {"Authorization": "Bearer " + os.environ["WEMO_API_KEY"]}

def call(method, path, **kw):
    r = requests.request(method, BASE + path, headers=H, timeout=120, **kw)
    if not r.ok:
        raise SystemExit(f"{r.status_code}: {r.text}")
    return r

# 1. Your sites: any whole-number id you like, with longitude and latitude
sites = [{"id": 1, "lon": -76.10, "lat": 37.60},
         {"id": 2, "lon": -76.05, "lat": 37.62},
         {"id": 3, "lon": -76.00, "lat": 37.58}]

# 2. Start a run (these are the web app's default settings)
job = call("POST", "/jobs", json={
    "name": "Script run",
    "sites": {"points": sites},
    "model_params": {"n_directions": 56, "max_fetch_m": 100000,
                     "sample_dist_m": 100, "sub_rays": 3},
    "wind_source": {"type": "open-meteo",
                    "start_date": "2014-01-01", "end_date": "2024-12-31"},
}).json()["job_id"]

# 3. Wait for it to finish
while True:
    status = call("GET", f"/jobs/{job}").json()
    if status["status"] in ("completed", "failed"):
        break
    time.sleep(5)
if status["status"] == "failed":
    raise SystemExit(status["error"])

# 4. Results for each site, and the same as a CSV file
for site in call("GET", f"/jobs/{job}/results").json()["results"]:
    print(site["site_id"], round(site["RWE"]), site["avg_wave_height_m"])

csv = call("GET", f"/jobs/{job}/download", params={"format": "csv"})
open("wemo_results.csv", "wb").write(csv.content)

Sites over a whole area

Instead of listing sites, you can send an outline of the area and a spacing, as the web app does when you draw on the map. WEMo lays a regular grid over it, keeps the points in water, and returns them with their depths and a grid_id to run:

# Python, continuing from above: a grid at 500 m spacing inside an outline
grid = call("POST", "/grid/generate", json={
    "polygon": [[-76.20, 37.50], [-75.95, 37.50], [-75.95, 37.70],
                [-76.20, 37.70], [-76.20, 37.50]],     # [lon, lat], closed
    "resolution_m": 500,
}).json()
print(grid["counts"])                                   # points made, inside, in water

job = call("POST", "/jobs", json={
    "name": "Area run",
    "sites": {"grid_id": grid["grid_id"]},
    "model_params": {"n_directions": 56, "max_fetch_m": 100000,
                     "sample_dist_m": 100, "sub_rays": 3},
    "wind_source": {"type": "open-meteo",
                    "start_date": "2014-01-01", "end_date": "2024-12-31"},
}).json()["job_id"]                                     # then wait and read results as above

POST /grid/estimate takes the same outline and spacing and tells you how many sites it would make, and a spacing that fits your limit, without making the grid.

Hourly time series

Add "run_type": "timeseries" to run WEMo hour by hour through the wind record at up to 25 sites, for comparing with buoys and other models. Each site gets its wave height, period, direction, orbital velocity and bed shear stress for every hour, and WEMo can compare a site with a NOAA buoy's measured waves for you. How time series work

# Python, continuing from above: three months at a site next to NOAA buoy 44064
job = call("POST", "/jobs", json={
    "name": "Hourly, Jan-Mar 2023",
    "run_type": "timeseries",
    "sites": {"points": [{"id": 1, "lon": -76.087, "lat": 37.008}]},
    "model_params": {"n_directions": 56, "max_fetch_m": 100000,
                     "sample_dist_m": 100, "sub_rays": 3},
    "wind_source": {"type": "open-meteo",
                    "start_date": "2023-01-01", "end_date": "2023-03-31"},
    "timeseries": {"duration_limited": True,                # waves need time to grow
                   "compare": {"station": "ndbc:44064"}},   # optional
}).json()["job_id"]
# ... wait for it as above, then:

ts = call("GET", f"/jobs/{job}/timeseries", params={"site_id": 1}).json()
print(ts["start"], ts["n"], "hours")                      # 2023-01-01T00:00Z 2160 hours
print(ts["comparison"]["stats"])                          # bias_m, rmse_m, scatter_index, r ...

csv = call("GET", f"/jobs/{job}/download", params={"format": "csv", "table": "details"})
open("wemo_hourly.csv", "wb").write(csv.content)         # one row per site and hour

Endpoints

All paths start with https://wemo.geohorizons.co/api/v1. Requests and replies are JSON unless a file is downloaded.

RequestWhat it does
POST/jobsStart a run. Body: name, sites (either points or a grid_id) and optional settings (below). Replies 202 with a job_id.
GET/jobs/{job_id}Status: queued, fetching_data, running, completed or failed, with progress (sites done of total) and, if it failed, why.
GET/jobs/{job_id}/resultsOne row per site (fields below). Paged: ?page=1&page_size=1000, up to 10,000 rows a page; total_sites says how many there are.
GET/jobs/{job_id}/results/detailsOne row per site and wind direction: fetch, wind speed and share of time, wave height, period, energy, orbital velocity and bed shear stress. Paged the same way; ?site_id= gives one site's rows.
GET/jobs/{job_id}/downloadA file: ?format=csv, geojson or geotiff. Add &table=details (CSV) for the per-direction rows, or &table=structures for a run with coastal structures.
GET/jobs/{job_id}/timeseriesA time series run's hours at one site (?site_id=) as arrays, with the measured waves and statistics if that site was compared.
POST/grid/estimateHow many sites an outline (polygon) at a spacing (resolution_m) would make, and a spacing that fits your limit.
POST/grid/generateMake that grid: points in water with their depths, and a grid_id to pass to /jobs. Optional holes (islands to leave out) and water_level_m.
GET/wind/stationsBuoys and weather stations near a place (?lat=..&lon=..), whose measured wind a run can use instead of reanalysis wind.
POST/uploads/bathymetry, /uploads/wind, /uploads/wavesUpload your own depth survey (GeoTIFF), wind record (CSV) or measured waves to compare a time series with (CSV), and get a file_id to use in a run.

Settings

Everything except name and sites is optional. Leave a setting out and WEMo uses its default. Two defaults differ from the web app's (sample_dist_m and sub_rays), so send the model_params shown in the examples to get the same results as the app.

SettingWhat it means
run_typeclimate (default: exposure over the whole wind record) or timeseries (hour by hour, see above).
timeseries.duration_limitedTime series only: limit each hour's waves by how long the wind has blown (default true).
timeseries.compareTime series only: {"station": "ndbc:44064"} or {"file_id": ...}, optionally with site_id.
model_params.n_directionsWind directions traced from each site (default 56).
model_params.max_fetch_mLongest fetch traced, in metres (default 100,000).
model_params.sample_dist_mSpacing of depth samples along each ray, in metres. The web app uses 100.
model_params.sub_raysRays per direction: 1, 3 or 5. The web app uses 3, which smooths streaks behind headlands.
model_params.water_level_mWater level above the depth datum, for a tide or storm surge (default 0).
model_params.sediment_d50_mmSeabed grain size for bed shear stress (default 0.2 mm, fine sand).
wind_source.typeopen-meteo (hourly wind back to 1940, anywhere) or upload (your own record, with its file_id).
wind_source.start_date, end_dateThe wind period, as YYYY-MM-DD. Ten years or more gives a steady wind climate.
wind_source.monthsOnly these months of each year, for a season: [6, 7, 8] for June to August.
wind_source.top_wind_pctOnly the windiest hours: 90 keeps the windiest 10% (default 0, all hours).
wind_source.wind_pointsTake a wind cell's wind from a buoy or station instead, such as {"cell": {"lon": ..., "lat": ...}, "station": "ndbc:44014"}.
bathymetry_source{"type": "auto"} (default: NOAA's Coastal Relief Model on the US coasts where the server has it, GEBCO elsewhere), {"type": "gebco"} for GEBCO only, or {"type": "upload", "file_id": ...} for your survey. GET /bathymetry/coverage lists where the CRM is installed, and a finished job's bathymetry_info says what it used.
swell_sourceAdd ocean swell from offshore, such as {"type": "open-meteo", "start_date": ..., "end_date": ...}.
vegetationSeagrass, marsh or other beds that slow waves: GeoJSON outlines with canopy height, stem width, density and drag.
structuresSeawalls, revetments, breakwaters and sills, drawn as lines of [lon, lat], each with a type and optional crest_elevation_m (on a seawall or revetment: waves overtop it when model_params.water_level_m comes near the crest), crest_width_m, slope, reflection, transmission and permeable (false for concrete or sheet pile). Results also show the same sites without them.

The full reference lists every setting with its allowed values; How it works explains what each one does to the waves.

Results

Each site comes back with the id you sent it with (as site_id), its longitude, latitude and depth, and:

FieldWhat it is
RWERepresentative wave energy (J/m): the wave energy in one wavelength per metre of crest, summed over directions and weighted by how often the wind blows from each. A relative measure for comparing sites in the same area and wind climate; total_RWE adds swell when the run has it.
avg_wave_height_m, max_wave_height_mWave height at the site under the typical wind from each direction, averaged across directions, and the largest single direction.
direction_of_max_wave_degThe direction the largest waves come from.
mean_wave_direction_degThe energy-weighted mean direction waves come from over the wind record (the map's arrows); total_mean_wave_direction_deg includes swell.
direction_concentrationHow one-sided the exposure is, 0 to 1: 1 when every wave comes from one direction, near 0 when opposite directions balance.
avg_fetch_m, max_fetch_mDistance over open water to the shore, averaged over directions and the longest.
avg_effective_fetch_m, max_effective_fetch_mThe direction-blended fetch used for wave growth: each direction blended with its neighbours, because wind passes energy to the water over a spread of angles.
avg_orbital_velocity_ms, max_...Water speed at the bed under the waves (m/s).
avg_bed_shear_stress_pa, max_...The waves' pull on the seabed (Pa), for sediment and plant stability.
wind_cell_lat, wind_cell_lonWhich wind record the site used.
data_quality.any_ray_off_rastertrue if a ray left the depth grid before reaching shore, so its fetch may be short.

Runs with swell, vegetation or structures add fields for them (for example swell_height_mean_m, vegetation_at_site, RWE_change_pct). The GeoJSON file has every site as a point with all its fields; the GeoTIFF has one band per result, on a grid in the local UTM zone, for GIS. The web app's five exposure classes are simply fifths of the run's sites by RWE, so you can make them yourself (for example with pandas.qcut or dplyr's ntile).

Limits

Need bigger runs, more at once, or WEMo on your own servers? Email us.

Errors

Errors come back as JSON with a short code to check in a script and a message to read: {"detail": {"code": "...", "message": "..."}}.

StatusMeaning
400, 422Something in the request is missing or out of range, such as a site without a lon, two sites with the same id, or a wind period with no data. The message says which.
401No key, a mistyped key, or a revoked key (invalid_api_key).
403The request uses something that isn't turned on for your account (feature_off; details.feature says which), such as API access or a model option. Email us to have it turned on.
404No such run or grid on your account.
409The run has more sites than your limit (job_size_exceeded), or results were asked for before the run finished.
429Another run is still going (run_in_progress), or more than 120 requests in a minute (rate_limited; the Retry-After header says how many seconds to wait).
500, 502, 503A problem on our side, or a data source is briefly down. Wait a minute and try again; if it keeps happening, let us know.

Full reference

The interactive reference lists every endpoint, setting and result field, with allowed values, and lets you try requests in the browser: choose Authorize and paste your key. The same description is available as an OpenAPI file for generating a client in other languages.

Use of the API is covered by the Terms of use; how keys are stored is in the Privacy notice. The API is new; we'll keep /api/v1 working as it is and add to it rather than change it, and give notice by email before any change that could break a script. Questions or ideas: [email protected].