Getting started

Metric pull endpoints

Let Tuumo pull datapoints from your systems

There are three ways to populate metric datapoints:

  1. Input (manually type values in our UI)

  2. Push (via API)

  3. Pull (via pull endpoints)

This article covers how to use the Pull method.

Overview

Pull endpoints are basically configuration that tell Tuumo how to fetch metric datapoints from your system. The configuration is:

  • URL to your https endpoint

  • Our IP address where our datapoints request will be coming from. Used eg. for whitelisting

  • Automatic pull -toggle that says whether or not we should periodically request missing datapoints

  • Secret for request authentication

  • Finally you need to associate a metric with the metric pull endpoint, so that we will start requesting missing datapoints for that metric.

In addition to configuring a metric pull endpoint, you need to have a functioning https endpoint that can:

  1. Receive an https request from our IP address

  2. Authenticate and validate our request

  3. Respond with the requested metric datapoints if they are available

Note: we are not specifying timezones for metrics, datapoints nor pull endpoints. It is up to the client to decide when a specific day start or ends. When we request a datapoint for a specific day/week/month/quarter, make sure that is has ended before responding with a datapoint.

Create a metric pull endpoint

  1. Go to product settings by clicking the product menu in the left navigation bar, and selecting "Product settings"

  2. Go to pull endpoints

  3. Click create a pull endpoint

  4. Add a descriptive and the URL of the https API endpoint and click Save

Generate a secret

  1. Go to metric pull endpoint secret generation section

  2. Generate a secret. It is only visible one.

  3. Copy the secret to a secure place. If lost, a new one can be generated

Associate with a metric

  1. Go to metrics

  2. Go to metric settings via the menu with a vertical ellipsis next to the metric you want associated with a pull endpoint

  3. Use the pull endpoint select to choose the metric endpoint you have created

Create the https endpoint

This process differs based on the platform/language/frameworks/libraries used. The general process is:

  1. Receive an https request from our IP address

  2. Authenticate and validate our request

    1. The authorization header has a value in format "HMAC-SHA512 Signature=<hex encoded signature>" where the signature is a sha512 HMAC of the request body

    2. You generate the hex encoded hmac sha512 signature using the currently active secret and verify that the signature is the same as in the authorization header

    3. The request body should be in the format:

{
  "workspace": "<the workspace name>",
  "product": "<the product name>",
  "metric": "<the metric name>",
  "datapointDates": ["2025-12-24", "<possibly more dates in the same format>"]
}

Finally Respond with the requested metric datapoints if they are available. The response should be "Content-Type": "application/json"and in the format:

{
  "datapoints": [{
    "date": "2025-12-24",
    "value": 42
  },{
    "date": "<other dates where datapoints are ready",
    "value": 67
  }]
}

Here is an example in typescript using react-router v7 in framework mode:

import crypto from "node:crypto";
import { z } from "zod/v4";
import type { Route } from "./+types/tuumo-pull-endpoint._index";
import { env } from "~/env.server";
import { db } from "~/server/database.server";

const verifyHmac = ({
  data,
  key,
  signature,
}: {
  data: string;
  key: string;
  signature: string;
}) => {
  return crypto.timingSafeEqual(
    Buffer.from(signature, encoding),
    crypto.createHmac(algorithm, key).update(data).digest(),
  );
};

// format "YYYY-MM-DD"
const dateSchema = z
  .string()
  .refine((date) => !Number.isNaN(Date.parse(date)), {
    message: "Invalid date format",
  });
const bodySchema = z.object({
  workspace: z.string().min(1),
  product: z.string().min(1),
  metric: z.string().min(1),
  datapointDates: z.array(dateSchema),
});

type Datapoint = {
  date: string;
  value: number;
};
type Payload = {
  datapoints: Datapoint[];
};

export const action = async ({
  request,
}: Route.ActionArgs): Promise<Payload> => {
  // "Authorization": "HMAC-SHA512 Signature=<hex encoded signature>"
  const authorization = request.headers.get("authorization");
  if (!authorization) {
    throw new Error("Missing tuumo authorization header");
  }
  const signature = authorization.split("=")[1];
  if (!signature) {
    throw new Error("Missing tuumo signature header");
  }
  const bodyString = await request.text();
  const isValidSignature = verifyHmac({
    data: bodyString,
    key: env.TUUMO_PULL_ENDPOINT_SECRET,
    signature,
  });
  if (!isValidSignature) {
    throw new Error("Invalid tuumo signature");
  }

  const result = bodySchema.safeParse(JSON.parse(bodyString));
  if (result.error) {
    throw new Error(`Invalid tuumo request body: ${result.error.message}`);
  }
  const body = result.data;

  if (body.workspace !== "<your workspace name>") {
    throw new Error(`Unsupported tuumo workspace name ${body.workspace}`);
  }
  if (body.product !== "<your product name>") {
    throw new Error(`Unsupported tuumo product name ${body.product}`);
  }
  switch (body.metric) {
    case "New daily users": {
      const datapoints: Datapoint[] = [];
      for (const date of body.datapointDates.filter((date) => {
        // filter out future dates and current dates (you choose which data is included for which day)
        const [year, month, day] = date.split("-").map(Number);
        if (!year || !month || !day) {
          throw new Error(`Invalid tuumo date format: ${date}`);
        }
        const dateObj = new Date(year, month - 1, day, 23, 59, 59);
        const today = new Date();
        return dateObj <= today;
      })) {
        const [year, month, day] = date.split("-").map(Number);
        if (!year || !month || !day) {
          throw new Error(`Invalid tuumo date format: ${date}`);
        }
        const { newDailyUsers } = await db
          .selectFrom("user")
          .select((eb) => eb.fn.count("user.id").as("newDailyUsers"))
          .where("user.createdAt", ">=", new Date(year, month - 1, day))
          .where(
            "user.createdAt",
            "<=",
            new Date(year, month - 1, day, 23, 59, 59),
          )
          .executeTakeFirstOrThrow();
        datapoints.push({
          // return the same date in the exact same format
          date,
          value: Number(newDailyUsers),
        });
      }
      return { datapoints: datapoints };
    }
    default: {
      throw new Error(`Unsupported tuumo metric name ${body.metric}`);
    }
  }
};

Test the endpoint

  1. Return to pull endpoint settings

  2. Go to test section

  3. Choose a metric that the endpoint should be able to return data for

  4. Choose a date to test what results you would get for that date

    1. Verify that past dates return correct values

    2. Verify that current and future dates do not return values

If there are errors, double check connectivity, your application code and logs. If there are issues we can provide support.

Manually pull missing datapoints

  1. Go to pull manually section

  2. Select only missing datapoints

  3. Click pull to do the initial datapoint population

  4. If you have performance issues (too many datapoints requested causing timeouts), edit your application code to only return datapoints for a limited number of dates

  5. You can invoke data pull multiple times and get datapoints imported in batches

This section can be also used to overwrite existing datapoints, in case there was an error or a change in how to datapoints are generated.

Toggle automatic pull

  1. Toggle automatic pull

  2. Every hour we check if there are missing datapoints for a date (timezone utc) and pull them using the pull endpoint settings

  3. After initial pull, there should be a single datapoint requested every time

Support

If you have issues reach out to us via email support@tuumo.fi.

On this page

© 2026

Tuumo · All rights reserved