Reading Stats data API
The charts are computed in your browser from the file you drop, so there is no job to submit and nothing to bill. What the API exposes is the one thing the app stores: the year summaries a person chooses to save.
No run API, by design
Reading Stats has no model. Parsing, statistics and the PNG card all happen in the page, and your library file never leaves the device. To compute the same numbers in a script, read the CSV yourself; to sync saved summaries, use the endpoints below. They cost nothing to call.
Base URL and envelope
https://api.skillsafe.ai/v1/app-api
Every response is a JSON envelope. Success carries data; failure carries error with a stable code.
{"ok":true,"data":{ ... }}
{"ok":false,"error":{"code":"NOT_FOUND","message":"no such record"}}
| Code | HTTP | What it means here |
|---|---|---|
UNAUTHORIZED | 401 | Missing, malformed or expired bearer token. |
FORBIDDEN | 403 | A valid token for a different app. |
NOT_FOUND | 404 | No such record visible to you. Rows belong to their owner, so someone else's record is a 404. |
VALIDATION_ERROR | 400 | A declared field has the wrong type, usually a saved_at that is not ISO-8601 with a Z. |
RATE_LIMITED | 429 | Too many requests; back off and retry. |
QUOTA_EXCEEDED | 409 | Too many records for one owner, or a document over 64 KB. |
The years collection
One record per saved year. Owner-read, user-write.
| Field | Type | Meaning |
|---|---|---|
label | string | The card title, e.g. "My 2025 in books". |
year | string | "2025", or "all" for all years. |
books | number | Books finished in the selection. |
pages | number | Pages, over books that have a page count. |
avg_rating | number | Your average star rating, 0 if none rated. |
authors | number | Distinct authors. |
top_author | string | Most-read author. |
summary_md | string | The Markdown summary, clipped on whole lines to stay under the record cap. |
card_json | string | The card model (title, tiles, bars, highlights) as JSON. |
saved_at | timestamp | ISO-8601 with a Z. |
Steps
1 Get a token
A guest token needs no credentials and owns its own saved years. To use the identity your browser already has, copy the token from the session page instead; every later step is the same.
POST https://api.skillsafe.ai/v1/app-api/guest
curl -s -X POST https://api.skillsafe.ai/v1/app-api/guest
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 1
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/guest",
method="POST",
)
with urllib.request.urlopen(req) as r:
print(json.load(r)["data"])
const TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 1
const res = await fetch("https://api.skillsafe.ai/v1/app-api/guest", {
method: "POST",
});
const { data, error } = await res.json();
if (error) throw new Error(error.code);
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
token := "YOUR_TOKEN" // from /tokens.html, or step 1
body := strings.NewReader("")
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/guest", body)
_ = token
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
import java.net.URI;
import java.net.http.*;
HttpClient client = HttpClient.newHttpClient();
String token = "YOUR_TOKEN"; // from /tokens.html, or step 1
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/guest"))
.method("POST", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
require "json"
require "net/http"
token = "YOUR_TOKEN" # from /tokens.html, or step 1
uri = URI("https://api.skillsafe.ai/v1/app-api/guest")
req = Net::HTTP::Post.new(uri)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN"; // from /tokens.html, or step 1
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.skillsafe.ai/v1/app-api/guest");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Accept: application/json"]);
$res = curl_exec($ch);
curl_close($ch);
print_r(json_decode($res, true)["data"]);
using System.Net.Http;
using System.Text;
var token = "YOUR_TOKEN"; // from /tokens.html, or step 1
using var client = new HttpClient();
var req = new HttpRequestMessage(new HttpMethod("POST"), "https://api.skillsafe.ai/v1/app-api/guest");
var res = await client.SendAsync(req);
Console.WriteLine(await res.Content.ReadAsStringAsync());
Returns:
{"data":{"token":"aut_...","subject_type":"guest","subject_id":"gst_..."}}
2 See who the token is
Three fields: subject_type, subject_id and credits. The signed-in test is subject_type == "user". Nothing on this app spends credits.
GET https://api.skillsafe.ai/v1/app-api/me
curl -s -X GET https://api.skillsafe.ai/v1/app-api/me \
-H "Authorization: Bearer $READING_STATS_TOKEN"
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 1
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/me",
method="GET",
headers={"Authorization": "Bearer " + TOKEN},
)
with urllib.request.urlopen(req) as r:
print(json.load(r)["data"])
const TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 1
const res = await fetch("https://api.skillsafe.ai/v1/app-api/me", {
method: "GET",
headers: { Authorization: "Bearer " + TOKEN },
});
const { data, error } = await res.json();
if (error) throw new Error(error.code);
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
token := "YOUR_TOKEN" // from /tokens.html, or step 1
body := strings.NewReader("")
req, _ := http.NewRequest("GET", "https://api.skillsafe.ai/v1/app-api/me", body)
req.Header.Set("Authorization", "Bearer "+token)
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
import java.net.URI;
import java.net.http.*;
HttpClient client = HttpClient.newHttpClient();
String token = "YOUR_TOKEN"; // from /tokens.html, or step 1
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/me"))
.header("Authorization", "Bearer " + token)
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
require "json"
require "net/http"
token = "YOUR_TOKEN" # from /tokens.html, or step 1
uri = URI("https://api.skillsafe.ai/v1/app-api/me")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{token}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN"; // from /tokens.html, or step 1
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.skillsafe.ai/v1/app-api/me");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Accept: application/json", "Authorization: Bearer $token"]);
$res = curl_exec($ch);
curl_close($ch);
print_r(json_decode($res, true)["data"]);
using System.Net.Http;
using System.Text;
var token = "YOUR_TOKEN"; // from /tokens.html, or step 1
using var client = new HttpClient();
var req = new HttpRequestMessage(new HttpMethod("GET"), "https://api.skillsafe.ai/v1/app-api/me");
req.Headers.Add("Authorization", "Bearer " + token);
var res = await client.SendAsync(req);
Console.WriteLine(await res.Content.ReadAsStringAsync());
Returns:
{"data":{"subject_type":"user","subject_id":"usr_...","credits":12000}}
3 Save a year summary
The same record the app writes when you press Save this year to my account. saved_at is a declared timestamp and must be ISO-8601 with a Z. card_json is the card model as a JSON string; the app redraws the PNG from it. Keep a document under 64 KB.
POST https://api.skillsafe.ai/v1/app-api/collections/years/records
curl -s -X POST https://api.skillsafe.ai/v1/app-api/collections/years/records \
-H "Authorization: Bearer $READING_STATS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"doc": {"label": "My 2025 in books", "year": "2025", "books": 30, "pages": 12635, "avg_rating": 4.21, "authors": 28, "top_author": "Margaret Atwood", "summary_md": "# My reading year, 2025\n...", "card_json": "{}", "saved_at": "2026-01-02T09:00:00Z"}}'
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 1
body = json.dumps({"doc": {"label": "My 2025 in books", "year": "2025", "books": 30, "pages": 12635, "avg_rating": 4.21, "authors": 28, "top_author": "Margaret Atwood", "summary_md": "# My reading year, 2025\n...", "card_json": "{}", "saved_at": "2026-01-02T09:00:00Z"}}).encode()
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/collections/years/records",
method="POST",
data=body,
headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as r:
print(json.load(r)["data"])
const TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 1
const res = await fetch("https://api.skillsafe.ai/v1/app-api/collections/years/records", {
method: "POST",
headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json" },
body: JSON.stringify({"doc": {"label": "My 2025 in books", "year": "2025", "books": 30, "pages": 12635, "avg_rating": 4.21, "authors": 28, "top_author": "Margaret Atwood", "summary_md": "# My reading year, 2025\n...", "card_json": "{}", "saved_at": "2026-01-02T09:00:00Z"}}),
});
const { data, error } = await res.json();
if (error) throw new Error(error.code);
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
token := "YOUR_TOKEN" // from /tokens.html, or step 1
body := strings.NewReader(`{"doc": {"label": "My 2025 in books", "year": "2025", "books": 30, "pages": 12635, "avg_rating": 4.21, "authors": 28, "top_author": "Margaret Atwood", "summary_md": "# My reading year, 2025\n...", "card_json": "{}", "saved_at": "2026-01-02T09:00:00Z"}}`)
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/collections/years/records", body)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
import java.net.URI;
import java.net.http.*;
HttpClient client = HttpClient.newHttpClient();
String token = "YOUR_TOKEN"; // from /tokens.html, or step 1
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/collections/years/records"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("{\"doc\": {\"label\": \"My 2025 in books\", \"year\": \"2025\", \"books\": 30, \"pages\": 12635, \"avg_rating\": 4.21, \"authors\": 28, \"top_author\": \"Margaret Atwood\", \"summary_md\": \"# My reading year, 2025\\n...\", \"card_json\": \"{}\", \"saved_at\": \"2026-01-02T09:00:00Z\"}}"))
.build();
HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
require "json"
require "net/http"
token = "YOUR_TOKEN" # from /tokens.html, or step 1
uri = URI("https://api.skillsafe.ai/v1/app-api/collections/years/records")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{token}"
req["Content-Type"] = "application/json"
req.body = {"doc" => {"label" => "My 2025 in books", "year" => "2025", "books" => 30, "pages" => 12635, "avg_rating" => 4.21, "authors" => 28, "top_author" => "Margaret Atwood", "summary_md" => "# My reading year, 2025\n...", "card_json" => "{}", "saved_at" => "2026-01-02T09:00:00Z"}}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN"; // from /tokens.html, or step 1
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.skillsafe.ai/v1/app-api/collections/years/records");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, '{"doc": {"label": "My 2025 in books", "year": "2025", "books": 30, "pages": 12635, "avg_rating": 4.21, "authors": 28, "top_author": "Margaret Atwood", "summary_md": "# My reading year, 2025\n...", "card_json": "{}", "saved_at": "2026-01-02T09:00:00Z"}}');
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Accept: application/json", "Authorization: Bearer $token", "Content-Type: application/json"]);
$res = curl_exec($ch);
curl_close($ch);
print_r(json_decode($res, true)["data"]);
using System.Net.Http;
using System.Text;
var token = "YOUR_TOKEN"; // from /tokens.html, or step 1
using var client = new HttpClient();
var req = new HttpRequestMessage(new HttpMethod("POST"), "https://api.skillsafe.ai/v1/app-api/collections/years/records");
req.Headers.Add("Authorization", "Bearer " + token);
req.Content = new StringContent("{\"doc\": {\"label\": \"My 2025 in books\", \"year\": \"2025\", \"books\": 30, \"pages\": 12635, \"avg_rating\": 4.21, \"authors\": 28, \"top_author\": \"Margaret Atwood\", \"summary_md\": \"# My reading year, 2025\\n...\", \"card_json\": \"{}\", \"saved_at\": \"2026-01-02T09:00:00Z\"}}", Encoding.UTF8, "application/json");
var res = await client.SendAsync(req);
Console.WriteLine(await res.Content.ReadAsStringAsync());
Returns:
{"data":{"record":{"record_id":"rec_...","doc":{"label":"My 2025 in books", ...}}}}
4 List saved years
Every where entry is an operator object (eq ne lt lte gt gte in contains); the bare-value form is rejected. Sorting uses sort. Records come back as {record_id, doc}: read fields from doc.
POST https://api.skillsafe.ai/v1/app-api/collections/years/query
curl -s -X POST https://api.skillsafe.ai/v1/app-api/collections/years/query \
-H "Authorization: Bearer $READING_STATS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"where": {"year": {"eq": "2025"}}, "sort": {"field": "saved_at", "dir": "desc"}, "limit": 20}'
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 1
body = json.dumps({"where": {"year": {"eq": "2025"}}, "sort": {"field": "saved_at", "dir": "desc"}, "limit": 20}).encode()
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/collections/years/query",
method="POST",
data=body,
headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as r:
print(json.load(r)["data"])
const TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 1
const res = await fetch("https://api.skillsafe.ai/v1/app-api/collections/years/query", {
method: "POST",
headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json" },
body: JSON.stringify({"where": {"year": {"eq": "2025"}}, "sort": {"field": "saved_at", "dir": "desc"}, "limit": 20}),
});
const { data, error } = await res.json();
if (error) throw new Error(error.code);
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
token := "YOUR_TOKEN" // from /tokens.html, or step 1
body := strings.NewReader(`{"where": {"year": {"eq": "2025"}}, "sort": {"field": "saved_at", "dir": "desc"}, "limit": 20}`)
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/collections/years/query", body)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
import java.net.URI;
import java.net.http.*;
HttpClient client = HttpClient.newHttpClient();
String token = "YOUR_TOKEN"; // from /tokens.html, or step 1
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/collections/years/query"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("{\"where\": {\"year\": {\"eq\": \"2025\"}}, \"sort\": {\"field\": \"saved_at\", \"dir\": \"desc\"}, \"limit\": 20}"))
.build();
HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
require "json"
require "net/http"
token = "YOUR_TOKEN" # from /tokens.html, or step 1
uri = URI("https://api.skillsafe.ai/v1/app-api/collections/years/query")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{token}"
req["Content-Type"] = "application/json"
req.body = {"where" => {"year" => {"eq" => "2025"}}, "sort" => {"field" => "saved_at", "dir" => "desc"}, "limit" => 20}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN"; // from /tokens.html, or step 1
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.skillsafe.ai/v1/app-api/collections/years/query");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, '{"where": {"year": {"eq": "2025"}}, "sort": {"field": "saved_at", "dir": "desc"}, "limit": 20}');
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Accept: application/json", "Authorization: Bearer $token", "Content-Type: application/json"]);
$res = curl_exec($ch);
curl_close($ch);
print_r(json_decode($res, true)["data"]);
using System.Net.Http;
using System.Text;
var token = "YOUR_TOKEN"; // from /tokens.html, or step 1
using var client = new HttpClient();
var req = new HttpRequestMessage(new HttpMethod("POST"), "https://api.skillsafe.ai/v1/app-api/collections/years/query");
req.Headers.Add("Authorization", "Bearer " + token);
req.Content = new StringContent("{\"where\": {\"year\": {\"eq\": \"2025\"}}, \"sort\": {\"field\": \"saved_at\", \"dir\": \"desc\"}, \"limit\": 20}", Encoding.UTF8, "application/json");
var res = await client.SendAsync(req);
Console.WriteLine(await res.Content.ReadAsStringAsync());
Returns:
{"data":{"records":[{"record_id":"rec_...","doc":{"year":"2025","books":30, ...}}]},"meta":{"pagination":{"has_more":false}}}
5 Delete one
Use the record_id from the create or query step. A record that is not yours returns 404, because rows are visible only to their owner.
DELETE https://api.skillsafe.ai/v1/app-api/collections/years/records/rec_REPLACE_ME
curl -s -X DELETE https://api.skillsafe.ai/v1/app-api/collections/years/records/rec_REPLACE_ME \
-H "Authorization: Bearer $READING_STATS_TOKEN"
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 1
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/collections/years/records/rec_REPLACE_ME",
method="DELETE",
headers={"Authorization": "Bearer " + TOKEN},
)
with urllib.request.urlopen(req) as r:
print(json.load(r)["data"])
const TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 1
const res = await fetch("https://api.skillsafe.ai/v1/app-api/collections/years/records/rec_REPLACE_ME", {
method: "DELETE",
headers: { Authorization: "Bearer " + TOKEN },
});
const { data, error } = await res.json();
if (error) throw new Error(error.code);
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
token := "YOUR_TOKEN" // from /tokens.html, or step 1
body := strings.NewReader("")
req, _ := http.NewRequest("DELETE", "https://api.skillsafe.ai/v1/app-api/collections/years/records/rec_REPLACE_ME", body)
req.Header.Set("Authorization", "Bearer "+token)
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
import java.net.URI;
import java.net.http.*;
HttpClient client = HttpClient.newHttpClient();
String token = "YOUR_TOKEN"; // from /tokens.html, or step 1
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/collections/years/records/rec_REPLACE_ME"))
.header("Authorization", "Bearer " + token)
.method("DELETE", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
require "json"
require "net/http"
token = "YOUR_TOKEN" # from /tokens.html, or step 1
uri = URI("https://api.skillsafe.ai/v1/app-api/collections/years/records/rec_REPLACE_ME")
req = Net::HTTP::Delete.new(uri)
req["Authorization"] = "Bearer #{token}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN"; // from /tokens.html, or step 1
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.skillsafe.ai/v1/app-api/collections/years/records/rec_REPLACE_ME");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Accept: application/json", "Authorization: Bearer $token"]);
$res = curl_exec($ch);
curl_close($ch);
print_r(json_decode($res, true)["data"]);
using System.Net.Http;
using System.Text;
var token = "YOUR_TOKEN"; // from /tokens.html, or step 1
using var client = new HttpClient();
var req = new HttpRequestMessage(new HttpMethod("DELETE"), "https://api.skillsafe.ai/v1/app-api/collections/years/records/rec_REPLACE_ME");
req.Headers.Add("Authorization", "Bearer " + token);
var res = await client.SendAsync(req);
Console.WriteLine(await res.Content.ReadAsStringAsync());
Returns:
{"data":{"deleted":true}}