Skip to content

Build a Faceted News Dashboard

Use Faceting to get aggregated counts instead of articles. One request returns the numbers behind a whole dashboard — how coverage splits by category, country and source bias — and range faceting turns publication dates and sentiment scores into ready-to-plot histograms.

Step 1: Break the result set down by field

Set facet=true and list up to 5 fields in facet.field. Counts are computed over everything that matches your filters, not just the current page.

bash
curl "https://api.apitube.io/v1/news/everything?facet=true&facet.field=category.id,source.country.id,source.bias&facet.limit=10&per_page=1&api_key=YOUR_API_KEY"
python
import requests

response = requests.get(
    "https://api.apitube.io/v1/news/everything",
    params={
        "facet": "true",
        "facet.field": "category.id,source.country.id,source.bias",
        "facet.limit": "10",
        "per_page": "1",
        "api_key": "YOUR_API_KEY",
    },
)
print(response.json())
javascript
const params = new URLSearchParams({ "facet": "true", "facet.field": "category.id,source.country.id,source.bias", "facet.limit": "10", "per_page": "1", "api_key": "YOUR_API_KEY" });
const response = await fetch(`https://api.apitube.io/v1/news/everything?${params}`);
const data = await response.json();
console.log(data);
php
$query = http_build_query(["facet" => "true", "facet.field" => "category.id,source.country.id,source.bias", "facet.limit" => "10", "per_page" => "1", "api_key" => "YOUR_API_KEY"]);
$response = file_get_contents("https://api.apitube.io/v1/news/everything?$query");
$data = json_decode($response, true);
print_r($data);
go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
)

func main() {
	u, _ := url.Parse("https://api.apitube.io/v1/news/everything")
	q := u.Query()
	q.Set("facet", "true")
	q.Set("facet.field", "category.id,source.country.id,source.bias")
	q.Set("facet.limit", "10")
	q.Set("per_page", "1")
	q.Set("api_key", "YOUR_API_KEY")
	u.RawQuery = q.Encode()

	resp, _ := http.Get(u.String())
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var data map[string]any
	json.Unmarshal(body, &data)
	fmt.Println(data)
}
java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Example {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.apitube.io/v1/news/everything?facet=true&facet.field=category.id,source.country.id,source.bias&facet.limit=10&per_page=1&api_key=YOUR_API_KEY"))
            .GET()
            .build();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.body());
    }
}
text
Write a script in your preferred language that calls the APITube News API:

GET https://api.apitube.io/v1/news/everything?facet=true&facet.field=category.id,source.country.id,source.bias&facet.limit=10&per_page=1

Read the API key from an environment variable (do not hardcode it), handle request
errors, and print the key fields of each result.
Docs: https://docs.apitube.io/platform/news-api/parameters

The response carries a facets object keyed by field name, each holding { value, count } pairs:

json
{
    "facets": {
        "source.bias": [
            { "value": 0, "count": 3809437 },
            { "value": 2, "count": 3746702 },
            { "value": 1, "count": 1868553 }
        ]
    }
}
  • Pie or bar widgetfacets["category.id"], feed the value into the category list to get a readable label.
  • Map widgetfacets["source.country.id"].
  • Bias splitfacets["source.bias"], where 0 is left, 1 is center and 2 is right. Sources with no bias rating are returned under their own value — drop it before charting.
  • Noise floor — raise facet.mincount to hide long-tail values with only a handful of articles.

Step 2: Draw the publication timeline

Range faceting buckets a numeric or date field. For a timeline set facet.range.field=published_at and pick a gap — 1HOUR for a live view, 1DAY for a week, 1MONTH for a year.

bash
curl "https://api.apitube.io/v1/news/everything?facet.range=true&facet.range.field=published_at&facet.range.start=2026-07-20&facet.range.end=2026-07-27&facet.range.gap=1DAY&per_page=1&api_key=YOUR_API_KEY"
python
import requests

response = requests.get(
    "https://api.apitube.io/v1/news/everything",
    params={
        "facet.range": "true",
        "facet.range.field": "published_at",
        "facet.range.start": "2026-07-20",
        "facet.range.end": "2026-07-27",
        "facet.range.gap": "1DAY",
        "per_page": "1",
        "api_key": "YOUR_API_KEY",
    },
)
print(response.json())
javascript
const params = new URLSearchParams({ "facet.range": "true", "facet.range.field": "published_at", "facet.range.start": "2026-07-20", "facet.range.end": "2026-07-27", "facet.range.gap": "1DAY", "per_page": "1", "api_key": "YOUR_API_KEY" });
const response = await fetch(`https://api.apitube.io/v1/news/everything?${params}`);
const data = await response.json();
console.log(data);
php
$query = http_build_query(["facet.range" => "true", "facet.range.field" => "published_at", "facet.range.start" => "2026-07-20", "facet.range.end" => "2026-07-27", "facet.range.gap" => "1DAY", "per_page" => "1", "api_key" => "YOUR_API_KEY"]);
$response = file_get_contents("https://api.apitube.io/v1/news/everything?$query");
$data = json_decode($response, true);
print_r($data);
go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
)

func main() {
	u, _ := url.Parse("https://api.apitube.io/v1/news/everything")
	q := u.Query()
	q.Set("facet.range", "true")
	q.Set("facet.range.field", "published_at")
	q.Set("facet.range.start", "2026-07-20")
	q.Set("facet.range.end", "2026-07-27")
	q.Set("facet.range.gap", "1DAY")
	q.Set("per_page", "1")
	q.Set("api_key", "YOUR_API_KEY")
	u.RawQuery = q.Encode()

	resp, _ := http.Get(u.String())
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var data map[string]any
	json.Unmarshal(body, &data)
	fmt.Println(data)
}
java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Example {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.apitube.io/v1/news/everything?facet.range=true&facet.range.field=published_at&facet.range.start=2026-07-20&facet.range.end=2026-07-27&facet.range.gap=1DAY&per_page=1&api_key=YOUR_API_KEY"))
            .GET()
            .build();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.body());
    }
}
text
Write a script in your preferred language that calls the APITube News API:

GET https://api.apitube.io/v1/news/everything?facet.range=true&facet.range.field=published_at&facet.range.start=2026-07-20&facet.range.end=2026-07-27&facet.range.gap=1DAY&per_page=1

Read the API key from an environment variable (do not hardcode it), handle request
errors, and print the key fields of each result.
Docs: https://docs.apitube.io/platform/news-api/parameters

Range buckets come back under <field>_ranges, each with its own start, end and count — plot them directly:

json
{
    "facets": {
        "published_at_ranges": [
            { "range_start": "2026-07-24", "range_end": "2026-07-25", "count": 283467 },
            { "range_start": "2026-07-25", "range_end": "2026-07-26", "count": 174354 },
            { "range_start": "2026-07-26", "range_end": "2026-07-27", "count": 165699 }
        ]
    }
}

Step 3: Add a sentiment histogram

The same mechanism works on numeric fields. sentiment.overall.score runs from -1 to 1, so a gap of 0.25 gives eight readable buckets from very negative to very positive.

bash
curl "https://api.apitube.io/v1/news/everything?facet.range=true&facet.range.field=sentiment.overall.score&facet.range.start=-1&facet.range.end=1&facet.range.gap=0.25&per_page=1&api_key=YOUR_API_KEY"
python
import requests

response = requests.get(
    "https://api.apitube.io/v1/news/everything",
    params={
        "facet.range": "true",
        "facet.range.field": "sentiment.overall.score",
        "facet.range.start": "-1",
        "facet.range.end": "1",
        "facet.range.gap": "0.25",
        "per_page": "1",
        "api_key": "YOUR_API_KEY",
    },
)
print(response.json())
javascript
const params = new URLSearchParams({ "facet.range": "true", "facet.range.field": "sentiment.overall.score", "facet.range.start": "-1", "facet.range.end": "1", "facet.range.gap": "0.25", "per_page": "1", "api_key": "YOUR_API_KEY" });
const response = await fetch(`https://api.apitube.io/v1/news/everything?${params}`);
const data = await response.json();
console.log(data);
php
$query = http_build_query(["facet.range" => "true", "facet.range.field" => "sentiment.overall.score", "facet.range.start" => "-1", "facet.range.end" => "1", "facet.range.gap" => "0.25", "per_page" => "1", "api_key" => "YOUR_API_KEY"]);
$response = file_get_contents("https://api.apitube.io/v1/news/everything?$query");
$data = json_decode($response, true);
print_r($data);
go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
)

func main() {
	u, _ := url.Parse("https://api.apitube.io/v1/news/everything")
	q := u.Query()
	q.Set("facet.range", "true")
	q.Set("facet.range.field", "sentiment.overall.score")
	q.Set("facet.range.start", "-1")
	q.Set("facet.range.end", "1")
	q.Set("facet.range.gap", "0.25")
	q.Set("per_page", "1")
	q.Set("api_key", "YOUR_API_KEY")
	u.RawQuery = q.Encode()

	resp, _ := http.Get(u.String())
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var data map[string]any
	json.Unmarshal(body, &data)
	fmt.Println(data)
}
java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Example {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.apitube.io/v1/news/everything?facet.range=true&facet.range.field=sentiment.overall.score&facet.range.start=-1&facet.range.end=1&facet.range.gap=0.25&per_page=1&api_key=YOUR_API_KEY"))
            .GET()
            .build();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.body());
    }
}
text
Write a script in your preferred language that calls the APITube News API:

GET https://api.apitube.io/v1/news/everything?facet.range=true&facet.range.field=sentiment.overall.score&facet.range.start=-1&facet.range.end=1&facet.range.gap=0.25&per_page=1

Read the API key from an environment variable (do not hardcode it), handle request
errors, and print the key fields of each result.
Docs: https://docs.apitube.io/platform/news-api/parameters

Other numeric fields accept the same treatment: read_time with a gap of 5 splits your corpus into short reads and long reads, source.rank.opr with a gap of 2 shows how much of the coverage comes from high-authority domains, and media.images.count tells you how illustrated the feed is.

Every filter you already use still applies before aggregation, so a dashboard scoped to one industry is the same request plus industry.id:

python
import requests

def dashboard(api_key, **filters):
    params = {
        "facet": "true",
        "facet.field": "category.id,source.country.id",
        "facet.range": "true",
        "facet.range.field": "published_at",
        "facet.range.start": "2026-07-20",
        "facet.range.end": "2026-07-27",
        "facet.range.gap": "1DAY",
        "per_page": 1,
        "api_key": api_key,
        **filters,
    }
    return requests.get("https://api.apitube.io/v1/news/everything", params=params).json()["facets"]

Tip: keep per_page=1 on aggregation requests. The facet counts cover the full match set regardless of page size, so there is no reason to pay for article payloads you will throw away.