> Documentation index: https://roxyapi.com/llms.txt. Building an integration? Read https://roxyapi.com/AGENTS.md first.

# Astrology report software for white-label PDFs

> Generate a personalised natal chart, kundli, human design, BaZi or numerology report under your own brand with one API call. A print-ready PDF, a self-contained web page or Markdown, in the language of your client, on every plan.

A report is a finished document built from the same data the chart endpoints return: the wheel or the kundli drawn to its tradition, every placement in a table, and the reading for each one, laid out page by page with your name, logo, colours, font and footer. You send the birth details, you get the document back. This page is for developers generating reports in an app or a backend, and for sellers who want to know what the widget does underneath. Samples, the full catalogue and what each report contains are on [white label astrology reports](/reports).

**Tip: Sell reports without writing code**
The report widget turns any report into a form on your page and hands your client the branded PDF. Pick a report, brand it and copy the code in the [report builder](/widgets#report). Step by step: [how to sell astrology readings](/docs/tutorials/sell-astrology-reports).

## The report operations

Each report is one `POST` operation under the **Reports** tag, at `/api/v2/reports/{id}`, named `generate{Report}Report`.

| Report | Path | operationId |
|---|---|---|
| Natal chart | `/reports/natal` | `generateNatalReport` |
| Synastry | `/reports/synastry` | `generateSynastryReport` |
| Kundli | `/reports/kundli` | `generateKundliReport` |
| Kundli matching | `/reports/matchmaking` | `generateMatchmakingReport` |
| Tarot reading | `/reports/tarot` | `generateTarotReport` |
| Numerology | `/reports/numerology` | `generateNumerologyReport` |
| Human Design | `/reports/human-design` | `generateHumanDesignReport` |
| Yearly forecast | `/reports/yearly-forecast` | `generateYearlyForecastReport` |
| BaZi | `/reports/bazi` | `generateBaziReport` |

The spec is the live list, with the cost of each report beside it:

```bash
curl -s https://roxyapi.com/api/v2/openapi.json \
  | jq -r '.paths | to_entries[] | select(.key | startswith("/reports/")) | "\(.key)  \(.value.post.operationId)  \(.value.post["x-request-cost"])"'
```

Every operation, with its request schema and a live console, is in the [Reports API reference](/api-reference/reports).

## What the request carries

The body is the input of the chart the report is built on, plus four report fields. A natal report takes what [`POST /astrology/natal-chart`](/api-reference/western-astrology#tag/western-astrology/POST/astrology/natal-chart) takes, a kundli report what the Vedic birth chart takes, a synastry or matching report `person1` and `person2`, a numerology report `fullName`, `year`, `month` and `day`. A tarot report needs nothing: send a `question` if your client has one.

| Field | What it does |
|---|---|
| `format` | `pdf` (default), `html` or `markdown`. Picks which output field the response carries |
| `branding` | Your name, logo, colours, font and footer. Every field optional. See [Branding](#branding) |
| `page.size` | `A4` (default) or `Letter`, for the PDF |
| `readings` | `true` (default) or `false`. `false` keeps the charts, tables and key figures and leaves out every written reading, for a chart-only report at the same cost |
| `?lang=` | Query parameter. The language of the whole report: headings, labels and every reading |

Birth places follow the same rule as every chart: resolve the city with [`GET /location/search`](/api-reference/location-and-timezone#tag/location-and-timezone/GET/location/search) and send its `latitude`, `longitude` and IANA `timezone`. Never ask your client for coordinates.

## Generate a PDF

[Get your API key](/account?tab=keys), then request the report and decode the `pdf` field into a file.


### curl

```bash
curl -s -X POST "https://roxyapi.com/api/v2/reports/natal" \
  -H "X-API-Key: $ROXY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "date": "1990-07-15",
    "time": "14:30:00",
    "latitude": 40.7128,
    "longitude": -74.006,
    "timezone": "America/New_York",
    "format": "pdf"
  }' | jq -r .pdf | base64 --decode > natal-report.pdf
```

### JavaScript

```javascript
import { writeFile } from 'node:fs/promises';

const res = await fetch('https://roxyapi.com/api/v2/reports/natal?lang=es', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.ROXY_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    date: '1990-07-15',
    time: '14:30:00',
    latitude: 40.7128,
    longitude: -74.006,
    timezone: 'America/New_York',
    format: 'pdf',
    branding: {
      name: 'Luna Rivera Astrology',
      footer: 'For reflection. Book a reading at example.com.',
      colors: { primary: '#3e2a2c', accent: '#914955' },
    },
  }),
});
const report = await res.json();
if (!res.ok) throw new Error(`${report.code}: ${report.error}`);

await writeFile(`${report.title}.pdf`, Buffer.from(report.pdf, 'base64'));
console.log(report.title, report.pages);
```

### Python

```python
import base64, os, requests

res = requests.post(
    "https://roxyapi.com/api/v2/reports/tarot",
    params={"lang": "en"},
    headers={"X-API-Key": os.environ["ROXY_API_KEY"]},
    json={"question": "What should I focus on this month?", "format": "pdf"},
)
report = res.json()
if not res.ok:
    raise RuntimeError(f'{report["code"]}: {report["error"]}')

with open("tarot-reading.pdf", "wb") as f:
    f.write(base64.b64decode(report["pdf"]))
print(report["title"], report["pages"])
```


The JavaScript call above returns a Spanish natal report titled `Informe de carta natal`, saved under that name.

## The response

A JSON object with the document in one field, chosen by `format`.

| Field | What it holds |
|---|---|
| `template` | The report id, such as `natal` |
| `lang` | The language the report is written in |
| `format` | `pdf`, `html` or `markdown`: which of the three fields below is present |
| `title` | The report title in the report language, ready for a file name, an email subject or a heading |
| `pages` | Page count. Present for a PDF |
| `pdf` | The PDF, base64 encoded. Decode it and save it as a `.pdf` file |
| `html` | One self-contained HTML document: styles and charts inline, ready to serve |
| `markdown` | The report as CommonMark, for your own layout or a CMS |

## Branding

Send `branding` with every request. Every field is optional, and anything left out keeps the theme value. The document carries your brand alone, on every plan.

| Field | What it does |
|---|---|
| `name` | Business or practitioner name on the cover and in the running header of every page |
| `logo` | HTTPS URL of a PNG, JPEG, WebP or SVG under 1 MB, printed on the cover |
| `footer` | A line at the foot of every page, such as a disclaimer or a booking note |
| `website` | Website printed on the cover |
| `email` | Contact email printed on the cover |
| `colors.primary` | Headings and table headers, as six digit hex such as `#3e2a2c` |
| `colors.accent` | Rules and highlights, as six digit hex such as `#914955` |
| `font` | The font pair for headings and text, the same pairs the widgets take: `fraunces-jost`, `jost`, `geist` or `system` |
| `theme` | A design preset shared with the widgets: `practitioner` (Rosewater), `eucalyptus`, `kiln`, `moonlit` or `celeste` |

The logo URL must answer with the image file itself. A page that only shows the image, a host that answers an error, or a file over the size limit returns a `400`: see [invalid_branding](/docs/errors#invalid_branding) and [logo_fetch_failed](/docs/errors#logo_fetch_failed).

## Languages

Add `?lang=` and the whole report changes language: cover, headings, table labels and every reading. A Spanish natal report is `POST /api/v2/reports/natal?lang=es`. The supported codes are on the `lang` parameter of each operation in the [API reference](/api-reference/reports), and an unsupported code returns a `400` listing them.

## HTML and Markdown

`format: "html"` returns the report as one HTML document with everything inline, so you can store it or serve it as a page without fetching anything else. `format: "markdown"` returns the text of the report as CommonMark for your own template; the charts are drawn in the PDF and HTML formats.

## The report widget for browsers

Selling from a website? The report widget runs in the browser of your visitor with a [publishable key](/docs/widgets#get-your-publishable-key), asks for the birth details the report needs, and hands back the PDF or shows the report on the page. Copy it from the [report builder](/widgets#report), which writes every setting for you:

```html
<script src="https://cdn.jsdelivr.net/npm/@roxyapi/ui@latest/dist/cdn/widgets.js" defer></script>
<div data-roxy-widget="report" data-publishable-key="pk_live_YOUR_KEY" data-report="natal" data-format="pdf" data-brand-name="Luna Rivera Astrology" data-brand-footer="Book a reading at lunarivera.com" data-brand-color="#3e2a2c" data-brand-accent="#914955" data-font="jost"></div>
```

`data-report` takes the report id from the table above and `data-format` takes `pdf` or `html`. Every option, the component tag form and the platform walkthroughs: [widgets guide](/docs/widgets#sell-branded-reports). The widget generates a report for anyone who reaches the page, so put it on the page your checkout unlocks; your own platform takes the payment.

## What a report costs

Every data endpoint is one request. A report is a whole document built from many calculations and readings, so each report states its cost in requests up front: `x-request-cost` on its operation in the spec, the jq command above, and the cost table on [/reports](/reports#cost), which also shows how many of each report every plan covers a month. A report draws from the same monthly plan as every other call, with no separate packs and no tier gate. A request that fails on our side with a `5xx` does not count.

## What is never stored

A report is generated when you ask for it and handed straight back. The birth details, the branding, the logo and the finished file are never stored, so keep the response if you want to send it again. Branding travels with each request for the same reason.

## Errors

A failure is `{ error, code, doc_url }`, and a `400` adds `issues[]` naming every field at fault. The report-specific codes:

| Code | Status | Fix |
|---|---|---|
| [invalid_branding](/docs/errors#invalid_branding) | 400 | Point `branding.logo` at the image file itself |
| [logo_fetch_failed](/docs/errors#logo_fetch_failed) | 400 | Host the logo on a public HTTPS URL under 1 MB |
| [unsupported_character](/docs/errors#unsupported_character) | 400 | Remove the character the error names from your text |
| [reports_rate_limited](/docs/errors#reports_rate_limited) | 429 | Wait `Retry-After` seconds, then send it again |
| [reports_busy](/docs/errors#reports_busy) | 503 | Wait `Retry-After` seconds, then send it again unchanged |
| [report_render_timeout](/docs/errors#report_render_timeout) | 503 | Send the same request again |

## FAQ


### Can I sell RoxyAPI reports under my own brand?
Yes. Every plan includes white-label reports: your name, logo, colours, font and footer line on every page, and no mark of ours anywhere in the document.

### Which formats does a report come in?
Three: a print-ready PDF returned as base64 in the `pdf` field, one self-contained HTML document in the `html` field, or CommonMark in the `markdown` field. Pick one with `format` in the request body.

### How do I save the PDF from the response?
Decode the base64 `pdf` field and write the bytes to a `.pdf` file. On the command line that is `jq -r .pdf | base64 --decode > report.pdf`; in Node it is `Buffer.from(report.pdf, 'base64')`.

### Can a report be generated in another language?
Yes. Add `?lang=` to the request, for example `?lang=hi` or `?lang=es`, and the cover, headings, labels and every reading arrive in that language.

### How many requests does one report use?
Each report states its cost in requests: `x-request-cost` on its operation in the OpenAPI spec, and the cost table on the reports page. It draws from your normal monthly plan.

### Can I let visitors generate reports on my website?
Yes. The report widget takes a publishable key, asks the visitor for their birth details and hands back the branded PDF or shows the report in place. Build it in the report builder and paste it on the page your checkout unlocks.


## Next steps

- [White label astrology reports](/reports): samples, the full catalogue and the cost table.
- [How to sell astrology readings](/docs/tutorials/sell-astrology-reports): the no-code path from builder to checkout page.
- [Reports API reference](/api-reference/reports): every operation with a live console.
- [Caching and cost](/docs/guides/caching): keep the documents you have already generated.
