Telescope Energy
Get forecasts

Webhooks

Receive forecast updates, verify incoming messages and fetch the data you need.

Webhooks let Telescope notify your application when a new forecast version is published. You choose the products and forecast horizon; Telescope sends an HTTPS POST to your endpoint. Your application can then fetch the entire horizon with one API request.

Use webhooks to refresh a forecast cache, start a calculation or update your own dashboard without repeatedly checking for changes.

Set up your first webhook

  1. Prepare a public HTTPS endpoint on port 443 that accepts JSON POST requests. It needs a valid TLS certificate and must respond directly; redirects and private network addresses are not supported.
  2. Open Webhooks in your dashboard, then select Create webhook.
  3. Enter a recognizable name, your endpoint URL, the products you need and a forecast horizon.
  4. Open the saved webhook and copy its signing secret into your server's secret store. Configure your receiver to verify incoming requests as described below.
  5. Select Send test. Check that the test appears as Delivered, then confirm your application actually accepted it.

A test has type webhook.test and data.message. It has no forecast updates or dataUrl, and does not change forecast data. It uses the same endpoint and signing secret as real events.

The signing secret verifies messages from Telescope. Your customer API key, with forecasts:read, authorizes data requests to Telescope. These are separate credentials. Do not put either one in the endpoint URL or client-side code.

Webhooks belong to the account that creates them. You can configure up to 10 endpoints, with up to 20 products each. Separate production and staging endpoints have separate signing secrets.

Choose a forecast horizon

The horizon determines which delivery days can trigger a notification and which days the event's data link covers. It moves forward each day in the product's timezone.

SelectionDays included
TodayToday's local delivery day
TomorrowTomorrow's local delivery day
Today + next 3 daysToday and the following 3 days: 4 days in total
Today + next 7 daysToday and the following 7 days: 8 days in total

For example, on 12 October, Today + next 7 days covers 12–19 October inclusive. An update for 20 October does not trigger this webhook. The horizon does not extend a product's forecast availability: some selected days may not have a published forecast yet.

The event includes the actual dates and timezone. Use these dates when processing a delayed event; do not recalculate the horizon from the time you receive it. Local days can contain 23 or 25 hours around daylight saving changes.

Verify incoming requests

Telescope follows the Standard Webhooks signature format. Every request contains:

HeaderPurpose
webhook-idMatches the body's event id.
webhook-timestampTime of this delivery attempt, in Unix seconds.
webhook-signaturev1, followed by a Base64 HMAC-SHA256 signature.

Verify the original request bytes before parsing JSON. JSON parsing and re-serializing can change whitespace or encoding and invalidate the signature. The signed content is webhook-id.webhook-timestamp.rawBody, using the Base64-decoded part of the signing secret after whsec_ as the key.

These Node.js and Python examples accept timestamps within 5 minutes of the receiver's clock and use a constant-time signature comparison. Your server's clock should be synchronized.

import { createHmac, timingSafeEqual } from 'node:crypto'

function verifyWebhook(rawBody: Buffer, headers: Headers, secret: string) {
  const id = headers.get('webhook-id')
  const timestamp = headers.get('webhook-timestamp')
  const signatures = headers.get('webhook-signature') ?? ''
  if (!id || !timestamp || !/^\d+$/.test(timestamp) || !secret.startsWith('whsec_')) {
    throw new Error('Missing or invalid webhook credentials')
  }
  const seconds = Number(timestamp)
  if (!Number.isSafeInteger(seconds) || Math.abs(Date.now() / 1000 - seconds) > 300) {
    throw new Error('Expired webhook timestamp')
  }
  const expected = createHmac('sha256', Buffer.from(secret.slice(6), 'base64'))
    .update(`${id}.${timestamp}.`)
    .update(rawBody)
    .digest()
  const valid = signatures.split(' ').some(entry => {
    const [version, encoded] = entry.split(',')
    if (version !== 'v1' || !encoded) return false
    const received = Buffer.from(encoded, 'base64')
    return received.length === expected.length && timingSafeEqual(received, expected)
  })
  if (!valid) throw new Error('Invalid webhook signature')
  const event = JSON.parse(rawBody.toString('utf8'))
  if (event.id !== id) throw new Error('Mismatched event ID')
  return event
}

For Python, pass the framework's raw body bytes and case-insensitive headers; convert verification failures into 400 or 401 responses.

Configure your framework to preserve the raw request body. For a Web Request handler, read it with Buffer.from(await request.arrayBuffer()) and pass request.headers. With Express, use a raw-body middleware for this route before a global JSON parser.

Reject invalid signatures with 400 or 401. Never log the signing secret. To replace a compromised secret, create a replacement webhook, configure your receiver with its new secret and delete the old webhook.

Understand the event

This example uses illustrative IDs and dates. The webhook body is JSON; explanatory comments are kept outside the payload.

{
  "id": "evt_example_123",
  "type": "forecast.published",
  "schemaVersion": "1",
  "occurredAt": "2026-10-12T10:00:29.999Z",
  "data": {
    "product": "day-ahead-price-de-lu",
    "horizon": {
      "from": "2026-10-12",
      "through": "2026-10-19",
      "timezone": "Europe/Berlin"
    },
    "updates": [
      {
        "deliveryDay": "2026-10-13",
        "forecastVersionId": "example_version_13",
        "version": 4,
        "kind": "preliminary"
      },
      {
        "deliveryDay": "2026-10-14",
        "forecastVersionId": "example_version_14",
        "version": 2,
        "kind": "preliminary"
      }
    ],
    "dataUrl": "/api/v2/customer/forecasts/day-ahead-price-de-lu/data?deliveryFrom=2026-10-12&deliveryTo=2026-10-19&asOf=2026-10-12T10:00:29.999Z&limit=8"
  }
}
FieldHow to use it
idIdentifies this event. Retries and manual resends keep the same ID. Use it to recognize duplicates.
typeforecast.published carries forecast updates; webhook.test checks your receiver.
schemaVersionPayload format version, currently "1". Allow additional fields so compatible additions do not break your receiver.
occurredAtUTC snapshot time at the end of the collection window. It stays unchanged on retries.
data.productThe product variant's offering key. Each event concerns one product.
data.horizonInclusive local delivery dates and the product's timezone.
data.updatesOnly the delivery days updated in this event. It is not the complete forecast horizon.
forecastVersionIdThe exact published version for this delivery day. Keep it for reproducibility or fetch that individual version.
versionIncreasing version number for this product and delivery day. Compare it only within that same product/day.
kindPublication stage: preliminary, rolling or final. All three can trigger notifications.
data.dataUrlRelative API path to the whole selected horizon, including unchanged days. It fixes asOf so later updates do not silently change the snapshot.

Accept promptly and handle duplicates

A reliable receiver follows this order:

  1. Verify the signature and timestamp.
  2. Check the event type and schema version. Handle tests separately; safely acknowledge event types you intentionally ignore.
  3. Durably store accepted work with a unique constraint on the event ID. If you already accepted the same event, return success again.
  4. Respond with any 2xx status within 10 seconds. Fetch data and run calculations in a background worker.

If you cannot persist the work, return a server error so Telescope can retry. Avoid returning success before you have saved the event somewhere reliable.

Retries and manual resends can deliver the same event more than once. Dedupe by event ID; when several webhook endpoints feed the same application, also use (product, deliveryDay, forecastVersionId) to avoid processing the same forecast twice across subscriptions. For a cache of the latest forecast, only replace a product/day with a newer version—a delayed older event must not overwrite newer data.

Fetch the whole horizon

After verifying and accepting an event, your background worker can follow dataUrl using its customer API key. Resolve the relative path against https://telescope.energy, not against your own receiver URL or the /api/v2 base path.

// event is an authenticated forecast.published event from your work queue.
const origin = 'https://telescope.energy'
const url = new URL(event.data.dataUrl, origin)
if (url.origin !== origin || !url.pathname.startsWith('/api/v2/customer/forecasts/')) {
  throw new Error('Unexpected forecast data URL')
}
const response = await fetch(url, {
  headers: { Authorization: `Bearer ${process.env.TELESCOPE_API_KEY}` },
  redirect: 'error',
  signal: AbortSignal.timeout(30_000),
})
if (!response.ok) throw new Error(`Forecast request failed: ${response.status}`)
const forecast = await response.json()
// Inspect forecast.deliveryDays for availability and version IDs.
// Process forecast.values using validTime, seriesKey and outputKey.

You do not need one request per entry in updates. The included link requests up to 8 complete days in a single response. Normal API access rules still apply; receiving a webhook does not grant additional permissions.

A day with no publication at that snapshot is returned with available: false and reason NO_VERSION_AT_AS_OF. It is not a zero forecast. Do not repeatedly retry the same snapshot expecting that day to appear. A later publication will produce a new event when it is inside your horizon.

Store the returned version IDs with your calculations. See Historical data for the response format. Retry temporary data-fetch failures in your own worker: once your receiver returns 2xx, Telescope considers the notification accepted.

When notifications arrive

All newly published forecast versions within the selected horizon are eligible, including preliminary, rolling and final versions. A calculation that produces no new published version does not trigger an event.

Updates are collected in a short, approximately 30-second window. Several changed delivery days for one product appear together in one message. If the same day changes more than once within that window, the message includes the latest version for that day. This makes the webhook useful for refreshing your application; it is not a separate notification for every intermediate version.

If you subscribe to several products, each product sends a separate event to your endpoint. There is no guaranteed delivery order, including between two events for the same product.

Creating a webhook starts notifications for new publications. It does not send existing forecasts or create an event merely because the date changed. Fetch your initial data through the Customer API.

Retries and delivery history

Open a webhook to see its latest 50 events from the last 30 days, including payload, last response status, attempt count and next scheduled attempt.

StatusMeaning
WaitingQueued for its first attempt or a retry.
SendingA request is in progress.
DeliveredYour receiver returned 2xx. This does not confirm your background processing succeeded.
FailedNo further automatic attempt is scheduled. Inspect the last error and fix the cause.
StoppedDelivery was canceled because the webhook, product or account access changed.

Network failures, timeouts, HTTP 408, 429 and 5xx responses trigger bounded retries. Delays start at about 30 seconds, then grow through minutes to hours, with at most 10 attempts and no automatic retries beyond 72 hours after the event was created. Delays include jitter and may increase with load. Retry-After is respected up to 24 hours per attempt.

Other HTTP errors and redirects are not retried automatically. Fix the endpoint, then select Resend for a completed event. Resends use the current endpoint URL and signing secret but keep the original body, event ID and snapshot. They have a fresh signature timestamp. There is a limit of 5 manual resends per event and 3 test/resend requests per minute per account. A resend after the original 72-hour retry window gets one attempt.

Webhooks provide timely notifications, not guaranteed delivery of every update. Monitor failures and use the API to catch up after an outage or a long pause. For important workflows, periodically reconcile your stored forecast versions with the latest API data.

Pause, edit or delete

  • Pause stops new notifications and pending deliveries. Resume starts with future publications; updates missed while paused are not replayed.
  • Save changes stops pending deliveries, including retries. Future events use the new products, horizon and URL. The signing secret stays the same. You can explicitly resend a previous completed event if its product is still subscribed.
  • Delete removes the webhook from your dashboard and stops future deliveries. Create a new webhook to start again; it receives a new secret.

A request already in progress can still arrive after any of these actions. Access changes can also stop delivery. Once access is restored, save or resume your webhook and send a test before relying on new notifications.

If something is wrong

SymptomWhat to check
No events arriveIs the webhook active? Was a new version actually published for a selected product/day? Creating a subscription or crossing midnight alone does not send an event.
Test failsCheck the saved URL, public DNS, TLS certificate, firewall and your receiver's POST route. Avoid redirects and login pages.
Signature is invalidUse this endpoint's signing secret, the raw body and a synchronized clock. Do not use your API key as the signing secret.
Receiver returns 404 or 405Confirm the exact path and that it accepts POST.
Receiver returns 429 or times outAccept into a queue promptly and process asynchronously. Inspect capacity and retry settings.
Event is Delivered but data is missingCheck your background worker and API-key permissions. Inspect deliveryDays[].available; a longer horizon cannot create unpublished forecasts.
You receive duplicates or older eventsDedupe by event ID and guard each product/day against an older version.

For support, include the webhook name, event ID, attempt time and last HTTP status. Do not send your signing secret or API key.

Copyright © 2026