Portfolio Report
curl --request POST \
--url https://api.chicago.global/v1/portfolioreport \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"portfolio_data": {},
"format": "pdf"
}
'import requests
url = "https://api.chicago.global/v1/portfolioreport"
payload = {
"portfolio_data": {},
"format": "pdf"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({portfolio_data: {}, format: 'pdf'})
};
fetch('https://api.chicago.global/v1/portfolioreport', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.chicago.global/v1/portfolioreport",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'portfolio_data' => [
],
'format' => 'pdf'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.chicago.global/v1/portfolioreport"
payload := strings.NewReader("{\n \"portfolio_data\": {},\n \"format\": \"pdf\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.chicago.global/v1/portfolioreport")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"portfolio_data\": {},\n \"format\": \"pdf\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.chicago.global/v1/portfolioreport")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"portfolio_data\": {},\n \"format\": \"pdf\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"error": "<string>",
"timestamp": "<string>",
"details": "<string>"
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"input": "<unknown>",
"ctx": {}
}
]
}{
"success": true,
"error": "<string>",
"timestamp": "<string>",
"details": "<string>"
}Portfolio Analysis
Portfolio Report
Generate a PDF/HTML portfolio report from analyzer output (ASYNC - returns job_id immediately).
This endpoint creates a background job for report generation. The actual generation
happens asynchronously without blocking. Use GET /jobs/{job_id} to check status.
**Request Body:**
- `portfolio_data`: The complete result from the /analyzer endpoint
- `format`: Output format - "pdf" (default), "html", or "both"
**Response Format:**
```json
{
"job_id": "port-uuid-string",
"status": "pending",
"format": "pdf",
"check_url": "/v1/jobs/{job_id}",
"estimated_duration_seconds": 60
}
```
**Workflow:**
1. POST /portfolioreport -> Get job_id (returns in <1 second)
2. Poll GET /jobs/{job_id} every 2-5 seconds
3. When status="completed", result contains pdf_url, html_url, expires_at
**Report includes:**
- Executive Summary with key metrics
- Portfolio Value vs Benchmark chart
- Drawdown Analysis
- Sector & Market Allocation pie charts
- Top Holdings bar chart
- Monthly Returns heatmap
- Rolling Sharpe Ratio chart
- Period returns (MTD, YTD, 1Y, etc.)
**Note:** Report URLs expire after 24 hours.
**Example:**
```bash
# First get analyzer result
RESULT=$(curl -X POST "https://api/v1/analyzer" ...)
# Start report generation
curl -X POST "https://api/v1/portfolioreport" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"portfolio_data": '"$RESULT"', "format": "pdf"}'
# Check status
curl "https://api/v1/jobs/{job_id}" -H "Authorization: Bearer YOUR_KEY"
```
POST
/
v1
/
portfolioreport
Portfolio Report
curl --request POST \
--url https://api.chicago.global/v1/portfolioreport \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"portfolio_data": {},
"format": "pdf"
}
'import requests
url = "https://api.chicago.global/v1/portfolioreport"
payload = {
"portfolio_data": {},
"format": "pdf"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({portfolio_data: {}, format: 'pdf'})
};
fetch('https://api.chicago.global/v1/portfolioreport', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.chicago.global/v1/portfolioreport",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'portfolio_data' => [
],
'format' => 'pdf'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.chicago.global/v1/portfolioreport"
payload := strings.NewReader("{\n \"portfolio_data\": {},\n \"format\": \"pdf\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.chicago.global/v1/portfolioreport")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"portfolio_data\": {},\n \"format\": \"pdf\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.chicago.global/v1/portfolioreport")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"portfolio_data\": {},\n \"format\": \"pdf\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"error": "<string>",
"timestamp": "<string>",
"details": "<string>"
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"input": "<unknown>",
"ctx": {}
}
]
}{
"success": true,
"error": "<string>",
"timestamp": "<string>",
"details": "<string>"
}Generate a PDF or HTML portfolio report from the output of the
/v1/portfolio/analyze endpoint.
How It Works
This endpoint uses async job processing:- Run Analysis First: POST your portfolio to
/v1/portfolio/analyzeand wait for completion - Submit Report Request: POST the analysis result to
/v1/portfolioreport - Receive Job ID: Get a job ID and polling URL immediately
- Poll for Status: Check
/v1/jobs/{job_id}until status iscompleted - Get Report URLs: The completed job contains download URLs for the generated report
Report generation typically takes 30-60 seconds. Report URLs expire after 24 hours.
Request Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
portfolio_data | object | Yes | — | The complete result from the /v1/portfolio/analyze endpoint |
format | string | No | pdf | Output format: pdf, html, or both |
{
"portfolio_data": { ... },
"format": "pdf"
}
The
portfolio_data field must contain the full analysis result object — the result field from a completed /v1/portfolio/analyze job.Response (202 Accepted)
{
"job_id": "port-a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "pending",
"format": "pdf",
"check_url": "/v1/jobs/port-a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"estimated_duration_seconds": 60,
"message": "Portfolio report generation started. Poll the check_url to get results."
}
Completed Response
When the job completes,GET /v1/jobs/{job_id} returns:
{
"job_id": "port-a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "completed",
"result": {
"success": true,
"report_id": "f3a1b2c3",
"pdf_url": "https://your-supabase-url.supabase.co/storage/v1/object/public/portfolio-reports/pdf/2026/04/01/f3a1b2c3-report.pdf",
"html_url": null,
"expires_at": "2026-04-02T12:00:00"
}
}
Result Fields
| Field | Description |
|---|---|
success | Whether the report was generated successfully |
report_id | Unique identifier for this report |
pdf_url | Download URL for the PDF report (if format is pdf or both) |
html_url | Download URL for the HTML report (if format is html or both) |
expires_at | ISO 8601 timestamp when the report URLs expire (24 hours) |
Report Contents
The generated report includes:| Page | Contents |
|---|---|
| Executive Summary | Key metrics, portfolio value vs benchmark chart, factor scores, investment thesis |
| Performance Analysis | Portfolio vs benchmark comparison table, period returns (YTD, 1W, 1M, 3M, 6M, 1Y, 3Y, 5Y) |
| Charts & Visuals | Growth of $100 chart, drawdown analysis, rolling Sharpe ratio |
| Allocations | Sector allocation pie chart, market allocation pie chart, top holdings bar chart |
| Returns | Monthly returns heatmap, annual returns comparison |
Example: Full Workflow
// Step 1: Run portfolio analysis
const analyzeResponse = await fetch('https://api.chicago.global/v1/portfolio/analyze', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
portfolio: [
{ date: '2024-01-01', symbol: 'AAPL.O', weight: 0.25 },
{ date: '2024-01-01', symbol: 'MSFT.O', weight: 0.25 },
{ date: '2024-01-01', symbol: '2330.TW', weight: 0.25 },
{ date: '2024-01-01', symbol: '7203.T', weight: 0.25 }
],
start_date: '2024-01-01',
end_date: '2024-12-31',
benchmark: 'ACWI'
})
});
const { job_id: analyzeJobId } = await analyzeResponse.json();
// Step 2: Wait for analysis to complete
let analysisResult;
while (true) {
const status = await fetch(
`https://api.chicago.global/v1/jobs/${analyzeJobId}`,
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
).then(r => r.json());
if (status.status === 'completed') {
analysisResult = status.result;
break;
} else if (status.status === 'failed') {
throw new Error(status.error);
}
await new Promise(r => setTimeout(r, 3000));
}
// Step 3: Generate report from analysis result
const reportResponse = await fetch('https://api.chicago.global/v1/portfolioreport', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
portfolio_data: analysisResult,
format: 'pdf'
})
});
const { job_id: reportJobId, check_url } = await reportResponse.json();
// Step 4: Wait for report and get download URL
const interval = setInterval(async () => {
const status = await fetch(
`https://api.chicago.global${check_url}`,
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
).then(r => r.json());
if (status.status === 'completed') {
clearInterval(interval);
console.log('PDF URL:', status.result.pdf_url);
console.log('Expires:', status.result.expires_at);
} else if (status.status === 'failed') {
clearInterval(interval);
console.error('Failed:', status.error);
}
}, 3000);
# Step 1: Start analysis
ANALYZE_JOB=$(curl -s -X POST "https://api.chicago.global/v1/portfolio/analyze" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"portfolio": [
{"date": "2024-01-01", "symbol": "AAPL.O", "weight": 0.5},
{"date": "2024-01-01", "symbol": "MSFT.O", "weight": 0.5}
],
"start_date": "2024-01-01",
"end_date": "2024-12-31"
}')
JOB_ID=$(echo $ANALYZE_JOB | jq -r '.job_id')
# Step 2: Poll until complete, then get result
RESULT=$(curl -s "https://api.chicago.global/v1/jobs/$JOB_ID" \
-H "Authorization: Bearer YOUR_API_KEY" | jq '.result')
# Step 3: Generate report
curl -X POST "https://api.chicago.global/v1/portfolioreport" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"portfolio_data\": $RESULT, \"format\": \"pdf\"}"
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
application/json
Response
Job created successfully
Last modified on April 1, 2026
⌘I