API integration guide

How to read CEME data and calculate tariff comparisons from an app, script, or AI agent.

Quick start

The API is JSON over HTTPS. Use the site's origin as the base URL (for example, https://ceme.ocarroeletrico.com). All data routes under /v1/ require authentication. For an app or backend integration, request an API key from the site operator and send it in the X-API-Key header. Do not put a private API key in browser code, a mobile app bundle, a public repository, or an LLM prompt.

curl 'https://ceme.ocarroeletrico.com/v1/cemes' \
  -H 'X-API-Key: YOUR_API_KEY'

Responses are JSON. Successful list endpoints generally wrap results in {"data": [...]}. Errors use {"error":{"code":"...","message":"..."}}. Prices are in EUR; energy prices are EUR/kWh unless a field says otherwise.

Read-only: all documented endpoints only read or calculate data. POST /v1/simulate is still a calculation request; it does not change site data. Never call undocumented write routes.

Contents

Authentication

Recommended for integrations: an operator-provisioned API key. Send it as X-API-Key on every /v1/* request except session creation. Keys can be expired or revoked; a rejected key returns HTTP 401.

POST /v1/session issues an anonymous web-session JWT valid for 24 hours. It is intended for the website's browser, not as a substitute for an app API key. If used, send Authorization: Bearer TOKEN. Session creation is rate-limited and production requires the server's JWT secret to be configured.

curl -X POST 'https://ceme.ocarroeletrico.com/v1/session'

# Then use the returned token:
curl 'https://ceme.ocarroeletrico.com/v1/health' \
  -H 'Authorization: Bearer TOKEN_FROM_RESPONSE'

Use one authentication method per request. Never log or commit credentials. HTTPS is required outside local development.

Read endpoints

Method and pathPurpose / parameters
GET /v1/healthConfirms authentication; reports credential kind and server time.
GET /v1/cemesCommercial energy suppliers (CEMEs).
GET /v1/tariffsTariffs and the current tariff version where one is available.
GET /v1/tarCurrently valid regulated network access tariffs (TAR).
GET /v1/omie/daily?days=60Daily OMIE series, one row per Lisbon calendar day. days is clamped to 7–120; default 60. complete is false for a day that does not yet have all its quarter-hours (typically tomorrow, in the afternoon).
GET /v1/omie/futuresOMIP futures data.
GET /v1/omie/statsMonthly OMIE statistics and reference values.
GET /v1/indexed/quartersQuarter-hour data and calculated prices for an indexed tariff. See below.
POST /v1/simulateCalculates and ranks tariffs for supplied usage inputs. See below.

Exact field sets can evolve with the tariff data. Treat unknown JSON fields as valid, and do not assume every tariff has every optional value (for example, tariff version or split energy/network prices).

Calculate a comparison

POST /v1/simulate accepts a JSON object. All fields are optional; defaults are shown.

curl -X POST 'https://ceme.ocarroeletrico.com/v1/simulate' \
  -H 'X-API-Key: YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  --data '{
    "kwh": 40,
    "region": "CONT",
    "at": "2026-10-18T01:00:00Z",
    "omie": 0.20
  }'
FieldMeaning and accepted values
kwhEnergy to charge, number greater than 0 and at most 1000. Default 40.
regionCONT (mainland), MAD (Madeira), or AZO (Azores). Default CONT.
atOptional valid date/time; ISO 8601 with timezone/UTC offset recommended. Defaults to now.
omieReference OMIE price in EUR/kWh, positive number. Default 0.20.
omieCyclesOptional per-cycle reference prices: {"DIARIO": {"vazio": …, "fora": …}, "SEMANAL": {"vazio": …, "fora": …}}, positive EUR/kWh. Each indexed tariff uses the pair for its own time-of-use cycle (daily or weekly). Recommended — take them from the last row of /v1/omie/daily with complete: true (bd_* = daily cycle, bs_* = weekly, divided by 1000).
useSpotOptional boolean. When true, indexed tariffs are priced at the OMIE of the quarter-hour containing at (and that quarter's loss profile), overriding the omie* references, which remain only as a fallback if that quarter is unavailable. The response then carries omieSpot ({utc, eurKwh}) and omieRef is that quarter's price.
profileOptional. The charge split over time, for a session that lasts long enough to cross the off-peak boundary, several OMIE quarter-hours, midnight or a DST change: [{"at": ISO, "kwh": n}, …], in time order, up to 400 slices, each kwh ≥ 0, total above 0 and up to 1000. Each slice is priced at its own instant: its off-peak/peak period, its quarter-hour OMIE (with useSpot), that quarter's losses and that day's TAR; activation, support and VAT apply once. Split at quarter-hour boundaries (:00, :15, :30, :45 UTC): a slice is priced entirely at its start. When present it wins: kwh is the sum and at the first slice. The response adds profile ({slices, from, to, withSpot}) and, per tariff, kwhByPeriod. Invalid input is 400 bad_profile.
detailOptional boolean, with profile. Each tariff then also carries slices: per slice the instant, kWh, period and final price excl. VAT, plus for indexed tariffs the OMIE used and whether it was the quarter's spot (omieSpot).
omieVazio, omieForaOptional positive EUR/kWh off-peak / peak reference prices applied to every split-period indexed tariff (an explicit override; ignored where omieCycles covers the tariff's cycle).

The response includes at, kwh, region, omieRef, an average and data, an array of tariff quote objects sorted by total cost. A tariff's fields include descriptive metadata, prices and calculated totals; fields vary by tariff type. parts is the bill line by line, in euros (energyTar, plus energy/tar when the split is known, iec, activation, support, base excl. VAT, vatEur); the lines add up to totalEur to the cent. Some invalid/inconsistent tariffs can be omitted from the ranking rather than failing the whole request. This is an estimate for the energy component, not a final charging bill: the Mobi.E charging-point operator (OPC) fee is not included.

Indexed tariff quarter-hours

Use GET /v1/indexed/quarters with query parameters:

curl 'https://ceme.ocarroeletrico.com/v1/indexed/quarters?tariff=INDEXED_ID&region=CONT&date=2026-10-18&fixed=FIXED_ID&tz=Europe%2FLisbon' \
  -H 'X-API-Key: YOUR_API_KEY'

The response includes quarter records, source/estimate metadata, and winner counts. With no valid fixed comparison, winner values are null. The response's UTC timestamp fields remain UTC; displayed clock labels use the requested timezone.

Errors, rate limits, and safe retries

HTTP statusMeaning
400Invalid request input; inspect error.code and error.message.
401Missing, invalid, expired, or revoked credentials.
405Wrong HTTP method; use the method listed above.
413Request body exceeds the 64 KiB limit.
429Rate limit reached. Wait at least the Retry-After seconds in the response.
502Upstream OMIE series are temporarily unavailable; retry later with backoff.

Responses include rate-limit headers such as X-RateLimit-Remaining and X-RateLimit-Reset. The default API allowance is 60 requests per minute per key (the configured per-client limit may differ). For retryable failures, use bounded exponential backoff and respect Retry-After; do not retry a 400 or 401 unchanged.

Guidance for AI agents and code generators

For implementation details and the canonical route/auth design, see the API and auth design notes.