Skip to content

The Data feed (API): developer guide

The analyst research on your company as JSON, for you to show in your own design. This is for the developer building it: about a day’s work, most of it the layout.

In beta. The API is live and in use, and we may add fields as it grows. If anything here does not match what you see, tell us.

How it fits together

  1. You make an RSA key pair and send us the public half. The private half never leaves your server.
  2. We send you a Client Token and your feed’s URL.
  3. For each request, your server signs a short token with the private key and calls the URL with it.
  4. We check the signature against your public key and return the research as JSON, which you lay out in your own design.

Everything happens server to server. Never call the feed from a browser: the page would have to carry your private key, and anyone could read it.

1. Make a key pair

Two OpenSSL commands. The first writes the private key in PKCS#8 form, which every example below reads; older genrsa commands can write a form some libraries refuse.

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out company-private.pem
openssl pkey -in company-private.pem -pubout -out company-public.pem

Send us company-public.pem, or paste it into the sign-up form. Keep company-private.pem in your server’s secret store and nowhere else. If a private key is ever sent to anyone, make a new pair.

2. Sign a token

A JSON Web Token, signed with your private key, carrying these:

Claim What it means
alg (header) RS256. Any other algorithm is refused.
iss Your Client Token, exactly as we sent it.
aud ResearchTree
iat, exp Issued-at and expiry. Keep tokens short-lived: 30 minutes is plenty.

3. Call the feed

Send the token as Authorization: Bearer <token> in a GET to your feed’s URL. It has this shape; use the one we send you exactly as given.

GET https://research-tree-api.com/api/partners/erf/companyfeed/{partnerTag}

Every parameter is optional:

Parameter What it means
includeResearch Analyst research. Default true.
includeMedia Video and audio. Default true.
includeBlog Commentary and blogs. Default true.
includeEvent Company events. Default false, and only returned if events are switched on for you.
includeSingleCompanyNote Single-company research notes.
includeSectorNote Sector notes.
includeMorningNote Morning notes.
startRow, pageLength Paging. pageLength is at most 100.
email Do not send it. Visitors to an IR page are anonymous; the feed works without it.
filterByUser Leave it out, for the same reason.

Examples

Each one reads the Client Token and the feed URL from environment variables, signs a 30-minute token and returns the items.

Node.js, with jsonwebtoken
// npm install jsonwebtoken
import { readFileSync } from 'node:fs';
import jwt from 'jsonwebtoken';

const privateKey = readFileSync('company-private.pem', 'utf8');

export async function fetchResearchFeed() {
  const token = jwt.sign({}, privateKey, {
    algorithm: 'RS256',
    issuer: process.env.RT_CLIENT_TOKEN,
    audience: 'ResearchTree',
    expiresIn: '30m',
  });
  const url = new URL(process.env.RT_FEED_URL); // exactly as we sent it
  url.searchParams.set('pageLength', '20');
  const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
  const feed = await response.json();
  if (feed.Message) throw new Error(feed.Message); // errors arrive as HTTP 200
  return feed.Items;
}
Python, with PyJWT
# pip install "pyjwt[crypto]" requests
import os
import time

import jwt
import requests

with open("company-private.pem") as handle:
    PRIVATE_KEY = handle.read()


def fetch_research_feed() -> list[dict]:
    now = int(time.time())
    claims = {"iss": os.environ["RT_CLIENT_TOKEN"], "aud": "ResearchTree", "iat": now, "exp": now + 1800}
    token = jwt.encode(claims, PRIVATE_KEY, algorithm="RS256")
    response = requests.get(
        os.environ["RT_FEED_URL"],  # exactly as we sent it
        params={"pageLength": 20},
        headers={"Authorization": f"Bearer {token}"},
        timeout=10,
    )
    feed = response.json()
    if feed.get("Message"):  # errors arrive as HTTP 200
        raise RuntimeError(feed["Message"])
    return feed["Items"]
Supabase Edge Function, with jose
// supabase/functions/research-feed/index.ts
import { importPKCS8, SignJWT } from 'npm:jose@5';

// Secrets set with `supabase secrets set`. A PEM pasted into one secret keeps
// its line breaks as \n, so put them back before reading the key.
const PRIVATE_KEY = Deno.env.get('RT_PRIVATE_KEY')!.replace(/\\n/g, '\n');
const CLIENT_TOKEN = Deno.env.get('RT_CLIENT_TOKEN')!;
const FEED_URL = Deno.env.get('RT_FEED_URL')!; // exactly as we sent it
const CORS = { 'Access-Control-Allow-Origin': 'https://www.yourcompany.com' };

Deno.serve(async (request) => {
  if (request.method === 'OPTIONS') {
    return new Response(null, { headers: { ...CORS, 'Access-Control-Allow-Headers': 'authorization, content-type' } });
  }
  const token = await new SignJWT({})
    .setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
    .setIssuer(CLIENT_TOKEN)
    .setAudience('ResearchTree')
    .setIssuedAt()
    .setExpirationTime('30m')
    .sign(await importPKCS8(PRIVATE_KEY, 'RS256'));

  const url = new URL(FEED_URL);
  url.searchParams.set('pageLength', '20');
  const feed = await (await fetch(url, { headers: { Authorization: `Bearer ${token}` } })).json();
  if (feed.Message) {
    return Response.json({ error: feed.Message }, { status: 502, headers: CORS });
  }
  return Response.json(feed.Items, { headers: { ...CORS, 'Cache-Control': 'max-age=300' } });
});

What comes back

A JSON object. Check Message first; if it is empty, the notes are in Items, each with these fields:

Field What it means
ContentId Research Tree’s id for the item.
Headline The title. Events are prefixed "EVENT: ".
Synopsis A short summary. Empty for media.
ProviderName The broker or research house.
ProviderLogoUrl Their logo. Can be null: fall back to ProviderName.
ThumbnailUrl An image for the item.
ContentType Research, Media, Blog or Event.
Companies Id, CompanyName, Ticker, Exchange for each company covered. Any field can be null and the list can be empty.
PublishedDate, LastModifiedDateTime When it was published and last changed.
NumOfPages Page count, for research.
IsPremiumOnly, OnlyAvailableToPayingClients Whether reading it needs a paid Research Tree account.
LoggedInUserHasAccess Whether the reader can open it. Your visitors are not logged in, so this is the answer for an anonymous reader.
ResearchTreeUrl The link to the item. Use it exactly as returned: its ending credits your company and routes the reader correctly, and a rebuilt URL returns a 404.
DirectDownloadViewerUrl, PartnerViewerUrl For a viewer page only. Null unless one is set up for you.

When something goes wrong

Errors come back as HTTP 200 with Message set, so a status check alone will not catch them.

Message What to do
Unable to extract issuer from token The iss claim is missing. Set it to your Client Token.
Company not found The Client Token or the URL is not one we issued. Use both exactly as given.
Invalid token signature The token was not signed with the private key matching the public key we hold. Check the key, or send us the public key again.
Token has expired Sign a fresh token. Check the server clock if this happens with new tokens.
Invalid token audience aud must be exactly ResearchTree.
Invalid token algorithm Sign with RS256.
API not enabled for this company The feed is not switched on for you yet. Tell us and we will check.

Caching

We cache each feed for three minutes, so calling more often than that returns the same notes. Cache on your side too, for five minutes or so, rather than calling the feed on every page view.

Stuck on the Data feed?

Send us the Message you are getting and we will look at it with you.