Step 1

Register and retrieve your API key

Registration creates your account and sends a verification email. After clicking the link in that email your account is active and your API key is shown. No password required — your API key is your sole credential.

Note: You only need to register once. Keep your API key safe — it authenticates every request. If you lose it, use Step 2 to recover it by email.

register.py
import requests

BASE_URL = "https://otl.teromovigo.com"

r = requests.post(
    f"{BASE_URL}/v1/auth/register",
    json={"email": "johndoe@anywhere.com"},
)
print(r.json())

# Check your inbox, click the verification link.
# Your API key is displayed on that page — copy and save it.
output
{'message': 'Account created. A verification email has been sent to johndoe@anywhere.com. Click the link in the email to activate your account and receive your API key.'}

Step 2

Forgot your API key? Recover it by email

If you have lost your API key — or never saved it — just send your registered email address to POST /v1/auth/recover. Your API key will be emailed to you immediately. No password needed.

This also works if you forgot your password. Since the API key is all you need to use the service, recovering the key by email is sufficient to regain full access.

recover.py
import requests

BASE_URL = "https://otl.teromovigo.com"

r = requests.post(
    f"{BASE_URL}/v1/auth/recover",
    json={"email": "johndoe@anywhere.com"},
)
print(r.json())
output
{'message': 'If that email address is registered, your API key has been sent to it.'}
Note: The response is always the same whether or not the email is registered — this prevents anyone from using this endpoint to discover which email addresses have accounts.

Step 3

Compute OTL — JSON output

Submit one or more stations and receive a structured JSON response with amplitudes in millimetres and phases in degrees for all 11 tidal harmonics and three displacement components. Component order: amp[0] = radial (positive up), amp[1] = NS tangential (positive south), amp[2] = EW tangential (positive west).

Station coordinates can be given as geodetic lat / lon (degrees) or as ECEF Cartesian X / Y / Z (metres). The two forms are mutually exclusive; providing both is an error. When X/Y/Z is used the server converts to geodetic on the WGS84 ellipsoid and returns both the computed lat/lon and the derived h in the response. An optional ellipsoidal height h (metres) may also be supplied alongside lat / lon; it is written into the station coordinate line of the BLQ output and echoed back in JSON responses.
Two optional parameters worth knowing — both have sensible defaults:
  • earth_model — the Green's function / Earth structure model used for the elastic loading computation. Options: "PREM", "PREM_disp", "STW105", "STW105_disp". Default: "STW105_disp" (anelastic STW105 — recommended for high-accuracy geodetic work).
  • cmc — Centre of Mass Correction (boolean, default true). Adds a rigid-body geocentre displacement derived from the degree-1 harmonic of the ocean tide field. This depends only on the tide model (not the Earth model) and is required by most geodetic software. Set to false to obtain the raw loading values without CMC. See IERS Conventions, Chapter 7 for the authoritative definition.
compute_json.py
import requests

BASE_URL = "https://otl.teromovigo.com"
API_KEY  = "otl_your_api_key_here"

r = requests.post(
    f"{BASE_URL}/v1/compute/",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={
        "stations": [
            {"name": "ONSA", "lat": 57.3958, "lon": 11.9253, "h": 10.8},   # Onsala, Sweden; h optional
            # or ECEF:  {"name": "ONSA", "X": 3370658.4, "Y": 711917.2, "Z": 5349786.0}  (h derived)
            {"name": "COIM", "lat": 40.2033, "lon": -8.4103},  # Coimbra, Portugal
        ],
        "tide_model":  "FES2014b",     # "FES2014b" | "FES2022b" | "GOT5.5" | "TPXO10_atlas_v2"
        "earth_model": "STW105_disp",  # default — omit to use this
        "cmc":         True,            # available for all four tide models
        "format":      "json",
    },
)
r.raise_for_status()
data = r.json()

info = data["info"]
print(f"Created:           {info['created']}")
print(f"Tide model:        {info['tide_model']}")
print(f"Earth model:       {info['earth_model']}")
print(f"CMC:               {info['cmc']}")
print(f"Message:           {info['message']}")
print(f"Stations used:                  {data['stations_used']}")
print(f"Weekly quota remaining:         {data['weekly_quota_remaining']}")

# amp[0]/pha[0] = radial (positive up)    mm / degrees
# amp[1]/pha[1] = NS tangential (positive south)
# amp[2]/pha[2] = EW tangential (positive west)
for station in data["stations"]:
    name = station["name"]
    otl  = station["otl"]
    print(f"\n{name}  (lat={station['lat']}, lon={station['lon']})")
    for harm in info["harmonics"]:
        amp = otl[harm]["amp"]
        pha = otl[harm]["pha"]
        print(f"  {harm.upper():<4}  "
              f"U: {amp[0]:6.3f} mm / {pha[0]:7.2f}°   "
              f"N: {amp[1]:6.3f} mm / {pha[1]:7.2f}°   "
              f"E: {amp[2]:6.3f} mm / {pha[2]:7.2f}°")
output — data["info"]
{
  "service":          "OTL Provider — https://otl.teromovigo.com",
  "created":          "2026-06-14 16:42:11 UTC",
  "tide_model":       "FES2014b",
  "earth_model":      "STW105_disp",
  "cmc":              true,
  "harmonics":        ["m2", "s2", "n2", "k2", "k1", "o1", "p1", "q1", "mf", "mm", "ssa"],
  "amp_units":        "mm",
  "pha_units":        "degrees",
  "components":       ["amp[0]/pha[0]: radial, positive up",
                        "amp[1]/pha[1]: NS tangential, positive south",
                        "amp[2]/pha[2]: EW tangential, positive west"],
  "phase_convention": "lag relative to Greenwich, positive lag",
  "seawater_density": "1030 kg/m³",
  "message":          "The early bird gets the worm, but the second mouse gets the cheese."
}
output — printed
Created:           2026-06-14 16:42:11 UTC
Tide model:        FES2014b
Earth model:       STW105_disp
CMC:               True
Message:           The early bird gets the worm, but the second mouse gets the cheese.
Stations used:                  2
Weekly quota remaining:         4998

ONSA  (lat=57.3958, lon=11.9253)
  M2    U:  5.728 mm /   97.34°   N:  1.832 mm /  106.52°   E:  1.254 mm /   43.11°
  S2    U:  1.748 mm /  124.18°   N:  0.561 mm /  133.44°   E:  0.382 mm /   69.87°
  ...

COIM  (lat=40.2033, lon=-8.4103)
  M2    U: 44.671 mm /  122.58°   N:  9.341 mm /  108.73°   E:  5.217 mm /   57.29°
  S2    U: 13.621 mm /  149.34°   N:  2.847 mm /  135.61°   E:  1.590 mm /   84.15°
  ...

Step 4

Compute OTL — BLQ output

Pass "format": "blq" to receive the result as plain-text BLQ format, ready for use in geodetic software such as GIPSY, BERNESE, GAMIT, or CSRS-PPP. The response is plain text, not JSON.

compute_blq.py
import requests

BASE_URL = "https://otl.teromovigo.com"
API_KEY  = "otl_your_api_key_here"

r = requests.post(
    f"{BASE_URL}/v1/compute/",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={
        "stations": [
            {"name": "ONSA", "lat": 57.3958, "lon": 11.9253},
            # or ECEF:  {"name": "ONSA", "X": 3370658.4, "Y": 711917.2, "Z": 5349786.0}
            {"name": "COIM", "lat": 40.2033, "lon": -8.4103},
        ],
        "tide_model":  "FES2014b",     # "FES2014b" | "FES2022b" | "GOT5.5" | "TPXO10_atlas_v2"
        "earth_model": "STW105_disp",  # default — omit to use this
        "cmc":         True,            # available for all four tide models
        "format":      "blq",
    },
)
r.raise_for_status()

# Print BLQ to screen
print(r.text)

# Save to file
with open("ocean_loading.BLQ", "w") as f:
    f.write(r.text)
print("Saved to ocean_loading.BLQ")
output — ocean_loading.BLQ
$$ Ocean loading displacement
$$
$$ OTL Provider (https://otl.teromovigo.com)
$$ Created by Bos and Scherneck
$$ 2026-06-14 16:42:11 UTC
$$
$$ "The early bird gets the worm, but the second mouse gets the cheese."
$$
$$ COLUMN ORDER:  M2  S2  N2  K2  K1  O1  P1  Q1  MF  MM SSA
$$
$$ ROW ORDER:
$$ AMPLITUDES (m)
$$   RADIAL
$$   TANGENTL    EW
$$   TANGENTL    NS
$$ PHASES (degrees)
$$   RADIAL
$$   TANGENTL    EW
$$   TANGENTL    NS
$$
$$ Displacement is defined positive in upwards, South and West direction.
$$ The phase lag is relative to Greenwich and lags positive. The STW105_disp
$$ Green's function is used.
$$
$$ CMC:  YES (corr.tide centre of mass)
$$
$$ A constant seawater density of 1030 kg/m^3 is used.
$$
$$ FES2014b: m2 s2 n2 k2 k1 o1
$$ FES2014b: p1 q1 Mf Mm Ssa
$$
$$ CMC start : geocentre displacement coefficients
$$ CMC format: (harm, source, Gz_U, Gz_V, Gx_U, Gx_V, Gy_U, Gy_V) [metres]
$$ CMC conv  : displacement = U*cos(wt) + V*sin(wt)
$$ CMC frequ : M2   FES2014b    -1.2626E-03 -1.6550E-03  -1.3193E-03  8.8426E-04   1.2647E-03  1.5946E-04
$$ CMC frequ : S2   FES2014b    ...
$$ CMC end   :
$$
$$ END HEADER
$$
  ONSA
$$ FES2014b ID:2026-06-14 16:42:11
$$ Computed using compute_otl_fft via REST API
$$ ONSA                    RADI TANG  lon/lat:  11.9253   57.3958    10.800
  .00573 .00175 .00103 .00047 .00174 .00107 .00057 .00024 .00010 .00007 .00005
  .00125 .00038 .00022 .00010 .00050 .00031 .00016 .00007 .00003 .00002 .00001
  .00183 .00056 .00033 .00015 .00063 .00039 .00021 .00009 .00004 .00003 .00002
   97.34 124.18  79.52 127.21 -26.44 -37.82 -25.61 -53.17  87.34  91.22  58.14
   43.11  69.87  25.09  72.08 -80.21 -91.64 -79.38 -106.99  33.52  37.41   4.33
  106.52 133.44  88.71 136.44 -17.33 -28.71 -16.49 -44.06  96.43 100.31  67.23
$$
  COIM
  ...

Step 5

Check quota and usage history

The account endpoint returns your remaining weekly quota and the last 50 compute requests. Every account receives a quota for maximal 5,000 station computations per week, which resets automatically every Monday at 00:00 UTC.

account.py
import requests

BASE_URL = "https://otl.teromovigo.com"
API_KEY  = "otl_your_api_key_here"

r = requests.get(
    f"{BASE_URL}/v1/account/",
    headers={"Authorization": f"Bearer {API_KEY}"},
)
r.raise_for_status()
data = r.json()

print(f"Email:             {data['email']}")
print(f"Member since:      {data['member_since'][:10]}")
print(f"Stations remaining this week: {data['stations_remaining_this_week']}")

print("\nRecent usage:")
for entry in data["recent_usage"]:
    print(f"  {entry['date'][:10]}  "
          f"{entry['n_stations']:>4} station(s)  "
          f"{entry['tide_model']}")
output
Email:             johndoe@anywhere.com
Member since:      2026-06-14
Stations remaining this week: 4996

Recent usage:
  2026-06-14     2 station(s)  FES2014b
  2026-06-14     2 station(s)  FES2014b
Quota reset: The weekly quota resets automatically every Monday at 00:00 UTC. If you need more than 5,000 station computations per week, contact us at info@teromovigo.com.

Tip

Try the API interactively in Swagger

Every endpoint can be tested directly in your browser at /docs (Swagger UI) — no Python or terminal required. Below is a step-by-step walkthrough using the same three operations covered in this tutorial.

A

Set your API key

At the top-right of the Swagger page you will see an Authorize button (it may look like a lock icon 🔒). Click it. A dialog appears with an HTTPBearer field — paste your API key there (e.g. otl_aBcDe…, without any Bearer prefix). Click Authorize, then Close. All protected endpoints will now send your key automatically.

B

Register — POST /v1/auth/register

Find the auth section and click POST /v1/auth/register to expand it. Click Try it out, enter your email address in the request body, and click Execute. A verification email will be sent; click the link in it to activate your account and reveal your API key.

C

Recover a lost API key — POST /v1/auth/recover

Find the auth section, expand POST /v1/auth/recover, and click Try it out. Enter your email address in the request body and click Execute. Your API key will be emailed to you. No password required.

D

Compute OTL — POST /v1/compute

Find the compute section, expand POST /v1/compute, and click Try it out. Paste the JSON below into the request body field and click Execute. The response will contain amplitudes and phases for all 11 tidal harmonics.

Important — Python vs JSON: The code examples above use Python syntax. Before pasting into Swagger you must:
  • Remove trailing commas after the last item in a list or object ({"name": "COIM", …}, → no comma before ])
  • Remove # comment lines — JSON does not support comments
compute — request body
{
    "stations": [
        {"name": "ONSA", "lat": 57.3958, "lon": 11.9253, "h": 10.8},
        {"name": "COIM", "lat": 40.2033, "lon": -8.4103}
    ],
    "tide_model":  "FES2014b",
    "earth_model": "STW105_disp",
    "cmc":         true,
    "format":      "json"
}
response (abbreviated)
{
  "info": {
    "service":          "OTL Provider — https://otl.teromovigo.com",
    "created":          "2026-06-14 16:42:11 UTC",
    "tide_model":       "FES2014b",
    "earth_model":      "STW105_disp",
    "cmc":              true,
    "harmonics":        ["m2", "s2", "n2", "k2", "k1", "o1", "p1", "q1", "mf", "mm", "ssa"],
    "amp_units":        "mm",
    "pha_units":        "degrees",
    "components":       ["amp[0]/pha[0]: radial, positive up",
                          "amp[1]/pha[1]: NS tangential, positive south",
                          "amp[2]/pha[2]: EW tangential, positive west"],
    "phase_convention": "lag relative to Greenwich, positive lag",
    "seawater_density": "1030 kg/m³",
    "message":          "The early bird gets the worm, but the second mouse gets the cheese."
  },
  "credits_used":      2,
  "credits_remaining": 98,
  "stations": [
    {
      "name": "ONSA", "lat": 57.3958, "lon": 11.9253,
      "otl": {
        "m2": {"amp": [5.728, 1.832, 1.254], "pha": [97.34, 106.52, 43.11]},
        "s2": {"amp": [1.748, 0.561, 0.382], "pha": [124.18, 133.44, 69.87]},
        ...
      }
    },
    ...
  ]
}
E

Check credits — GET /v1/account

Find the account section, expand GET /v1/account, and click Try it out, then Execute (no body needed — your API key from step A is sent automatically). The response shows your current credit balance and recent usage history.