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:
| Endpoint | Point | Total time |
|---|---|---|
/api/live-predict/stream | Bay of Bengal (13.14, 86.75) | 18.8 s |
/api/live-predict/stream | Arabian Sea (19.36, 58.65) | 51.3 s |
/api/ocean-state | Bay of Bengal (13.14, 86.75) | 53.6 s |
/api/ocean-state | Arabian Sea (19.36, 58.65) | 77.5 s |
/api/ocean-state/stream | Bay 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.
GETfor everything, with parameters in the query string. - Authentication. None: no API keys.
- Rate limit. The three live endpoints (
/api/live-predict/stream,/api/ocean-stateand/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 aRetry-Afterheader. 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
| Name | Type | Required | Meaning |
|---|---|---|---|
lat | number | yes | Latitude in decimal degrees, 5 to 30. |
lon | number | yes | Longitude 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.
curl -N "https://oceanembed-backend.azurewebsites.net/api/live-predict/stream?lat=13.14&lon=86.75"
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 requestThe result object
{
"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
}
}
}| Field | Meaning |
|---|---|
location | The point you asked for and the region that handled it (Bay of Bengal or Arabian Sea). |
mode | Always live for this endpoint. |
data_dates | The real date of each input the model used. Different inputs lag by different amounts; see Data freshness. |
surface_state | The inputs at the surface. Bay: sst_c, ssh_m. Arabian Sea adds sss_psu, mld_m, wind_stress_curl, eddy_vorticity. |
profile | depths_m (15 depths, 0 to 1000 m) and predicted_temp_c, the model's temperature at each depth. |
recent_argo_context | The 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
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 requestExample: 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.
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)
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
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:
curl -N "https://oceanembed-backend.azurewebsites.net/api/live-predict/stream?lat=15&lon=78"
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
| Name | Type | Required | Meaning |
|---|---|---|---|
lat | number | yes | Latitude in decimal degrees. |
lon | number | yes | Longitude 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.
curl "https://oceanembed-backend.azurewebsites.net/api/ocean-state?lat=13.14&lon=86.75"
{
"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
| Field | Unit / meaning |
|---|---|
surface.sst_c | Sea surface temperature, °C. |
surface.sss_psu | Sea surface salinity, PSU. |
surface.current_u_ms, current_v_ms | Eastward and northward surface current, m/s. |
surface.ssh_m | Sea surface height anomaly, m. |
surface.wind_u_ms, wind_v_ms | Eastward and northward wind at the surface, m/s. |
subsurface | depth_m (15 depths) with the model's temperature_c at each. valid is false when no prediction could be made. |
reference | GLORYS 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. |
derived | Speed 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). |
provenance | Which 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.
{
"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
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
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):
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 sData 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
| Situation | What you get |
|---|---|
Point outside coverage, /api/live-predict/stream | HTTP 200, one event {"step":"error","done":true,"error":"No coverage at this location"} |
Point outside coverage, /api/ocean-state | HTTP 200, all values null, subsurface.valid: false |
Missing or non-numeric lat/lon | HTTP 422 with a JSON detail array naming the parameter |
| More than 5 live requests in a minute from one IP | HTTP 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 running | A 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:
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:
curl -i "https://oceanembed-backend.azurewebsites.net/api/ocean-state?lat=abc&lon=86"
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"}]}