API DOCUMENTATION

Calling OceanEmbed from your own code

Two endpoints return a live, fresh reading of the ocean at any point in our coverage: a predicted temperature profile down to 1000 m, plus the surface state it was computed from. Every request and response on this page was run against our real backend, and each was captured on 2026-09-25. Nothing is mocked or hand-written.

Read this first: live requests are slow

These endpoints do not read a prepared table. Each request fetches fresh satellite and reanalysis data from Copernicus Marine and runs our model on it, so a request typically takes 5 to 90 seconds, and can take longer. Most of the time is spent waiting for the data provider.

The five real calls captured for this page took:

EndpointPointTotal time
/api/live-predict/streamBay of Bengal (13.14, 86.75)18.8 s
/api/live-predict/streamArabian Sea (19.36, 58.65)51.3 s
/api/ocean-stateBay of Bengal (13.14, 86.75)53.6 s
/api/ocean-stateArabian Sea (19.36, 58.65)77.5 s
/api/ocean-state/streamBay of Bengal (13.14, 86.75)53.7 s

One of those (/api/ocean-state, Arabian Sea) took 78 seconds, longer than the 90 s figure above. Set your HTTP client timeout to at least 5 minutes and never call these from a page that needs an instant answer. Prefer /api/live-predict/stream if a person is waiting, since it reports progress while the fetch runs.

Basics

  • Base URL. https://oceanembed-backend.azurewebsites.net, the live deployment. Every example on this page uses it.
  • Method. GET for everything, with parameters in the query string.
  • Authentication. None: no API keys.
  • Rate limit. The three live endpoints (/api/live-predict/stream, /api/ocean-state and /api/ocean-state/stream) share one limit of 5 requests per minute per IP address (a rolling 60-second window). Requests over the limit get HTTP 429 with a Retry-After header. Requests rejected for bad parameters do not count.
  • CORS. Open to every origin (access-control-allow-origin: *), so browsers can call it directly.
  • Coverage. Latitude 5°N to 30°N. Longitude 45°E up to (not including) 77°E is the Arabian Sea model; 80°E to 100°E is the Bay of Bengal model. The band from 77°E to 80°E, and everything else, has no model.

Because each live call triggers real, costly upstream fetches, please keep to one request at a time and don't poll.

GET /api/live-predict/stream

Predicts the temperature profile at a point and streams progress as it works, using Server-Sent Events (SSE). The response is text/event-stream. Use this when a person is waiting.

Parameters

NameTypeRequiredMeaning
latnumberyesLatitude in decimal degrees, 5 to 30.
lonnumberyesLongitude in decimal degrees east, 45 to <77 or 80 to 100.

The stream

Each message is one line starting with data: holding a JSON object, followed by a blank line. Progress events look like {"step": "...", "done": false}. The last event has "done": true and either a result or an error. While the backend is silent for a long stretch it sends a comment line starting with : every 15 seconds so proxies don't drop the connection; skip lines that don't start with data: . (Our Arabian Sea capture received 1 such comment.)

The progress messages depend on the region. Bay of Bengal points report SST, sea level, and the nearby-Argo check; Arabian Sea points also report salinity, mixed layer depth, and wind/rotation, because that model uses those extra inputs.

Example: curl

-N turns off curl's output buffering so you see events as they arrive.

Windows PowerShell users: plain curl is an alias for Invoke-WebRequest, not the real curl, and fails with “A parameter cannot be found that matches parameter name 'N'.” Type curl.exe instead of curl.

request
curl -N "https://oceanembed-backend.azurewebsites.net/api/live-predict/stream?lat=13.14&lon=86.75"
real response, Bay of Bengal, captured 2026-09-25, 18.8 s total
data: {"step":"Fetching live sea surface temperature...","done":false}

   ↑ arrived 0.9 s after the request

data: {"step":"Fetching live sea level data...","done":false}

   ↑ arrived 7.2 s after the request

data: {"step":"Checking for nearby recent real Argo measurements...","done":false}

   ↑ arrived 17.4 s after the request

data: {"step":"complete","done":true,"result":{"location":{"lat":13.14,"lon":86.75,"region":"Bay of Bengal"},"mode":"live","data_dates":{"sst":"2026-09-23","ssh":"2026-09-23"},"surface_state":{"sst_c":28.58,"ssh_m":0.151},"profile":{"depths_m":[0,5,10,20,30,50,75,100,125,150,200,300,500,700,1000],"predicted_temp_c":[28.7,28.66,28.63,28.53,28.37,27.85,26.35,23.73,20.67,17.88,14.42,11.74,9.97,8.54,6.7]},"recent_argo_context":{"status":"found","surface_temp_c":29.13,"date":"2026-08-29","days_ago":26,"lat":13.04,"lon":87.03,"distance_km":33}}}

   ↑ arrived 18.8 s after the request

The result object

the last event above, pretty-printed
{
  "step": "complete",
  "done": true,
  "result": {
    "location": {
      "lat": 13.14,
      "lon": 86.75,
      "region": "Bay of Bengal"
    },
    "mode": "live",
    "data_dates": {
      "sst": "2026-09-23",
      "ssh": "2026-09-23"
    },
    "surface_state": {
      "sst_c": 28.58,
      "ssh_m": 0.151
    },
    "profile": {
      "depths_m": [
        0,
        5,
        10,
        20,
        30,
        50,
        75,
        100,
        125,
        150,
        200,
        300,
        500,
        700,
        1000
      ],
      "predicted_temp_c": [
        28.7,
        28.66,
        28.63,
        28.53,
        28.37,
        27.85,
        26.35,
        23.73,
        20.67,
        17.88,
        14.42,
        11.74,
        9.97,
        8.54,
        6.7
      ]
    },
    "recent_argo_context": {
      "status": "found",
      "surface_temp_c": 29.13,
      "date": "2026-08-29",
      "days_ago": 26,
      "lat": 13.04,
      "lon": 87.03,
      "distance_km": 33
    }
  }
}
FieldMeaning
locationThe point you asked for and the region that handled it (Bay of Bengal or Arabian Sea).
modeAlways live for this endpoint.
data_datesThe real date of each input the model used. Different inputs lag by different amounts; see Data freshness.
surface_stateThe inputs at the surface. Bay: sst_c, ssh_m. Arabian Sea adds sss_psu, mld_m, wind_stress_curl, eddy_vorticity.
profiledepths_m (15 depths, 0 to 1000 m) and predicted_temp_c, the model's temperature at each depth.
recent_argo_contextThe closest recent real Argo float measurement, for context. It is not used as a model input and is not a validation of this prediction. It carries status, and when found: surface_temp_c, date, days_ago, lat, lon, distance_km.

Arabian Sea response

real response, Arabian Sea (19.36, 58.65), captured 2026-09-25, 51.3 s total
data: {"step":"Fetching live sea surface temperature...","done":false}

   ↑ arrived 0.8 s after the request

data: {"step":"Fetching live sea level data...","done":false}

   ↑ arrived 7.3 s after the request

data: {"step":"Fetching live salinity data...","done":false}

   ↑ arrived 13.9 s after the request

data: {"step":"Fetching live mixed layer depth...","done":false}

   ↑ arrived 35.2 s after the request

data: {"step":"Fetching live wind data and computing ocean rotation...","done":false}

   ↑ arrived 42.2 s after the request

data: {"step":"Checking for nearby recent real Argo measurements...","done":false}

   ↑ arrived 50.2 s after the request

data: {"step":"complete","done":true,"result":{"location":{"lat":19.36,"lon":58.65,"region":"Arabian Sea"},"mode":"live","data_dates":{"sst":"2026-09-23","ssh":"2026-09-23","sss":"2026-09-18","mld":"2026-09-23","curl":"2026-09-23"},"surface_state":{"sst_c":25.96,"ssh_m":0.009,"wind_stress_curl":2.531e-7,"mld_m":10.9,"sss_psu":35.93,"eddy_vorticity":-0.0000037785055559877914},"profile":{"depths_m":[0,5,10,20,30,50,75,100,125,150,200,300,500,700,1000],"predicted_temp_c":[26.19,26.19,26.02,25.27,24.19,22.66,21.02,19.46,18.08,17.09,15.8,14.02,12.41,11.28,8.94]},"recent_argo_context":{"status":"found","surface_temp_c":26.92,"date":"2026-09-14","days_ago":10,"lat":19.01,"lon":59,"distance_km":55}}}

   ↑ arrived 51.3 s after the request

Example: JavaScript (Node 18+ or a browser)

Read the response body as a stream. This works with any fetch and lets you send headers if you put a gateway in front.

Node (ES module) or browser console
const BASE = "https://oceanembed-backend.azurewebsites.net";

const res = await fetch(`${BASE}/api/live-predict/stream?lat=13.14&lon=86.75`);
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let result = null;

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  const messages = buffer.split("\n\n");
  buffer = messages.pop();
  for (const msg of messages) {
    if (!msg.startsWith("data: ")) continue; // skips ": keep-alive" comments
    const event = JSON.parse(msg.slice(6));
    if (event.error) throw new Error(event.error);
    if (event.done) result = event.result;
    else console.log(event.step);
  }
}

console.log(result.surface_state, result.profile.predicted_temp_c);

Example: JavaScript with EventSource (browser)

browser
const BASE = "https://oceanembed-backend.azurewebsites.net";

const es = new EventSource(`${BASE}/api/live-predict/stream?lat=13.14&lon=86.75`);
es.onmessage = (e) => {
  const ev = JSON.parse(e.data);
  if (ev.done) {
    es.close(); // without this, EventSource would auto-reconnect and re-run the fetch
    console.log(ev.result ?? ev.error);
  } else {
    console.log(ev.step);
  }
};
es.onerror = () => es.close();

Example: Python

requests
import json
import requests

BASE = "https://oceanembed-backend.azurewebsites.net"

with requests.get(
    f"{BASE}/api/live-predict/stream",
    params={"lat": 13.14, "lon": 86.75},
    stream=True,
    timeout=300,
) as r:
    r.raise_for_status()
    for line in r.iter_lines(decode_unicode=True):
        if not line.startswith("data: "):
            continue  # blank separators and ": keep-alive" comments
        event = json.loads(line[6:])
        if event.get("error"):
            raise RuntimeError(event["error"])
        if event["done"]:
            result = event["result"]
        else:
            print(event["step"])

print(result["surface_state"], result["profile"]["predicted_temp_c"])

Outside coverage

A point with no model is not an HTTP error. You get status 200 and a single event, straight away:

request
curl -N "https://oceanembed-backend.azurewebsites.net/api/live-predict/stream?lat=15&lon=78"
real response (0.2 s)
data: {"step": "error", "done": true, "error": "No coverage at this location"}

GET /api/ocean-state

Returns one JSON document with the whole picture at a point: surface state, predicted subsurface profile, a climatology reference (Bay of Bengal only), and derived quantities. There is no progress reporting: the connection stays open with no body until the whole document is ready. The wait is the same 5 to 90+ seconds described above.

Parameters

NameTypeRequiredMeaning
latnumberyesLatitude in decimal degrees.
lonnumberyesLongitude in decimal degrees east.

Example: curl

Windows PowerShell users: plain curl is an alias for Invoke-WebRequest, not the real curl, and fails with “A parameter cannot be found that matches parameter name 'N'.” Type curl.exe instead of curl.

request
curl "https://oceanembed-backend.azurewebsites.net/api/ocean-state?lat=13.14&lon=86.75"
real response, Bay of Bengal, captured 2026-09-25, 53.6 s
{
  "location": {
    "lat": 13.14,
    "lon": 86.75
  },
  "surface": {
    "sst_c": 28.58,
    "sss_psu": 32.92,
    "current_u_ms": 0.276,
    "current_v_ms": 0.259,
    "ssh_m": 0.151,
    "wind_u_ms": 8.23,
    "wind_v_ms": 9.03
  },
  "subsurface": {
    "source": "OceanEmbed",
    "depth_m": [
      0,
      5,
      10,
      20,
      30,
      50,
      75,
      100,
      125,
      150,
      200,
      300,
      500,
      700,
      1000
    ],
    "temperature_c": [
      28.7,
      28.66,
      28.63,
      28.53,
      28.37,
      27.85,
      26.35,
      23.73,
      20.67,
      17.88,
      14.42,
      11.74,
      9.97,
      8.54,
      6.7
    ],
    "valid": true
  },
  "reference": {
    "source": "GLORYS climatology (Bay of Bengal only)",
    "temperature_c_by_depth": [
      29.36,
      29.28,
      29.27,
      29.23,
      29.13,
      28.46,
      26.35,
      23.69,
      20.87,
      18.31,
      14.97,
      11.99,
      10.01,
      8.48,
      6.67
    ],
    "same_depth": true
  },
  "derived": {
    "current_speed_ms": 0.378,
    "current_direction_deg": 46.8,
    "vertical_gradient_c_per_m": [
      -0.008,
      -0.006,
      -0.01,
      -0.016,
      -0.026,
      -0.06,
      -0.1048,
      -0.1224,
      -0.1116,
      -0.0692,
      -0.0268,
      -0.0088,
      -0.0072,
      -0.0061
    ],
    "temperature_anomaly_c": [
      -0.66,
      -0.62,
      -0.64,
      -0.7,
      -0.76,
      -0.61,
      0,
      0.04,
      -0.2,
      -0.43,
      -0.55,
      -0.25,
      -0.04,
      0.06,
      0.03
    ],
    "wind_speed_ms": 12.22,
    "wind_direction_deg": 42.3
  },
  "provenance": [
    "CMEMS Currents",
    "CMEMS Wind",
    "GLORYS",
    "OSTIA",
    "OceanEmbed",
    "SMAP"
  ]
}

Fields

FieldUnit / meaning
surface.sst_cSea surface temperature, °C.
surface.sss_psuSea surface salinity, PSU.
surface.current_u_ms, current_v_msEastward and northward surface current, m/s.
surface.ssh_mSea surface height anomaly, m.
surface.wind_u_ms, wind_v_msEastward and northward wind at the surface, m/s.
subsurfacedepth_m (15 depths) with the model's temperature_c at each. valid is false when no prediction could be made.
referenceGLORYS climatological temperature at the same 15 depths (temperature_c_by_depth). Bay of Bengal only; elsewhere source and the array are null and same_depth is false.
derivedSpeed and direction of current and wind, the vertical temperature gradient (°C per m, 14 values between the 15 depths), and temperature_anomaly_c (prediction minus climatology, so null outside the Bay).
provenanceWhich data sources contributed to this response.

current_direction_deg and wind_direction_deg are compass bearings (0° = north, clockwise) of the direction the vector points toward, computed as atan2(u, v). For wind this is the opposite of the meteorological convention, which names the direction the wind blows from. In the Bay of Bengal example, u = 4.4 and v = 1.58 give 70.2°.

Missing values are null, never guessed

This endpoint never fails because one upstream source is down. Any field it could not fetch comes back null, and its source is left out of provenance. Always check for null. The Arabian Sea response below shows the legitimate case: no climatology reference exists there.

real response, Arabian Sea (19.36, 58.65), captured 2026-09-25, 77.5 s
{
  "location": {
    "lat": 19.36,
    "lon": 58.65
  },
  "surface": {
    "sst_c": 25.96,
    "sss_psu": 35.93,
    "current_u_ms": -0.078,
    "current_v_ms": -0.139,
    "ssh_m": 0.009,
    "wind_u_ms": 3.1,
    "wind_v_ms": 3.45
  },
  "subsurface": {
    "source": "OceanEmbed",
    "depth_m": [
      0,
      5,
      10,
      20,
      30,
      50,
      75,
      100,
      125,
      150,
      200,
      300,
      500,
      700,
      1000
    ],
    "temperature_c": [
      26.19,
      26.19,
      26.02,
      25.27,
      24.19,
      22.66,
      21.02,
      19.46,
      18.08,
      17.09,
      15.8,
      14.02,
      12.41,
      11.28,
      8.94
    ],
    "valid": true
  },
  "reference": {
    "source": null,
    "temperature_c_by_depth": null,
    "same_depth": false
  },
  "derived": {
    "current_speed_ms": 0.159,
    "current_direction_deg": 209.2,
    "vertical_gradient_c_per_m": [
      0,
      -0.034,
      -0.075,
      -0.108,
      -0.0765,
      -0.0656,
      -0.0624,
      -0.0552,
      -0.0396,
      -0.0258,
      -0.0178,
      -0.008,
      -0.0057,
      -0.0078
    ],
    "temperature_anomaly_c": null,
    "wind_speed_ms": 4.64,
    "wind_direction_deg": 41.9
  },
  "provenance": [
    "CMEMS Currents",
    "CMEMS Wind",
    "OSTIA",
    "OceanEmbed"
  ]
}

Example: JavaScript

Node (ES module) or browser console
const BASE = "https://oceanembed-backend.azurewebsites.net";

const res = await fetch(`${BASE}/api/ocean-state?lat=13.14&lon=86.75`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const state = await res.json();

console.log(state.surface.sst_c, state.derived.current_speed_ms);
console.log(state.provenance);

Example: Python

requests
import requests

BASE = "https://oceanembed-backend.azurewebsites.net"

r = requests.get(
    f"{BASE}/api/ocean-state",
    params={"lat": 13.14, "lon": 86.75},
    timeout=300,  # the live fetch can take well over a minute
)
r.raise_for_status()
state = r.json()

print(state["surface"]["sst_c"], state["derived"]["current_speed_ms"])
print(state["provenance"])

Outside coverage

Also not an HTTP error: you get 200 immediately with the same shape but every value null, subsurface.valid set to false, and an empty provenance.

A streaming variant

GET /api/ocean-state/stream?lat=&lon= takes the same parameters and returns the same document, but as SSE with progress events, ending in {"step": "complete", "done": true, "result": {…}}, where result is the JSON above. Parse it exactly as in the streaming examples for /api/live-predict/stream. Real events from the Bay of Bengal point (53.7 s):

progress events only (the final event is the JSON shown above)
data: {"step":"Fetching live sea surface temperature...","done":false}   ← 0.7 s
data: {"step":"Fetching live sea level data...","done":false}   ← 6.8 s
data: {"step":"Checking for nearby recent real Argo measurements...","done":false}   ← 13.4 s
data: {"step":"Fetching live salinity data...","done":false}   ← 14.4 s
data: {"step":"Fetching live ocean current data...","done":false}   ← 23.9 s
data: {"step":"Fetching live wind data...","done":false}   ← 37.2 s
data: {"step":"Checking reference baseline...","done":false}   ← 53.7 s
data: {"step": "complete", "done": true, "result": …}   ← 53.7 s

Data freshness

“Live” means fetched now, not observed now. Near-real-time ocean products lag reality: in the captures above, SST, sea level, mixed layer depth and wind-stress curl were dated 2026-09-23, and salinity 2026-09-18, against a capture date of 2026-09-25. Read data_dates for the real date of each input. A result is a snapshot of the moment it was fetched, and the same request will give different numbers on a later day.

Errors

SituationWhat you get
Point outside coverage, /api/live-predict/streamHTTP 200, one event {"step":"error","done":true,"error":"No coverage at this location"}
Point outside coverage, /api/ocean-stateHTTP 200, all values null, subsurface.valid: false
Missing or non-numeric lat/lonHTTP 422 with a JSON detail array naming the parameter
More than 5 live requests in a minute from one IPHTTP 429, a Retry-After header (seconds) and a JSON detail message. Applies to the streaming endpoints too, before any stream starts.
Failure while a stream is runningA final event with done: true and an error string. Always check for error before result.

Real 429, returned by the sixth live request inside one minute:

real response (only the relevant headers shown)
HTTP/1.1 429 Too Many Requests
retry-after: 58
access-control-allow-origin: *

{"detail":"Rate limit exceeded: at most 5 live requests per minute per IP. Try again in 58 s."}

Real 422, from the request below:

request
curl -i "https://oceanembed-backend.azurewebsites.net/api/ocean-state?lat=abc&lon=86"
real response (date, server and content-length headers left out)
HTTP/1.1 422 Unprocessable Content
content-type: application/json

{"detail":[{"type":"float_parsing","loc":["query","lat"],"msg":"Input should be a valid number, unable to parse string as a number","input":"abc"}]}