quote-to-cash — Claude Code skill: product catalog and invoices
A skill for driving HubSpot from the CLI: it creates products, assembles quotes with line items and associations, and reads invoices and subscriptions.
npx skills add HubSpot/agent-cli-skills --skill quote-to-cash -a claude-codeWhat quote-to-cash does
The skill covers the path from product to revenue inside HubSpot. It creates catalog records, builds quotes out of line items, links them to deals, then reads invoices and subscriptions. Everything runs through the hubspot CLI, so no clicking around the UI.
Quote assembly happens in three pipes: line items are created from JSONL, then the quote itself, then the associations from items to quote and quote to deal. A cheat sheet in resources/q2c-essentials.md covers six fields and association directions. Read bulk-operations/SKILL.md first — it documents the dry-run, digest and confirm flow for destructive operations.
One hard limit: quotes created through the CLI stay in DRAFT, and share links, PDFs and invoice creation happen only in the HubSpot UI. Invoices and subscriptions need a matching scope on the token, otherwise you get a 403. Also hs_total_discount on line items is read-only, so set the discount percentage instead.
What it helps with
- create a product with price and SKU in the HubSpot catalog
- build a quote from line items and associate it with a deal
- list all outstanding invoices
- find overdue invoices by due date
- sum MRR across active subscriptions
Example prompts for the agent
- create a product Enterprise License priced at 12000
- build a quote for deal 12345 from items.jsonl
- show me all overdue invoices
How to install quote-to-cash
1Skills CLI (Vercel)
npx skills add HubSpot/agent-cli-skills --skill quote-to-cash -a claude-code2Manually: put the files in place
~/.claude/skills/quote-to-cash/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/quote-to-cash/
1Skills CLI (Vercel)
npx skills add HubSpot/agent-cli-skills --skill quote-to-cash -a codex2Manually: put the files in place
.agents/skills/quote-to-cash/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add HubSpot/agent-cli-skills --skill quote-to-cash -a cursor2Manually: put the files in place
.cursor/skills/quote-to-cash/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install HubSpot/agent-cli-skills quote-to-cash2Manually: put the files in place
.github/skills/quote-to-cash/SKILL.md1Skills CLI (Vercel)
npx skills add HubSpot/agent-cli-skills --skill quote-to-cash -a gemini-cli2Platform CLI
gemini skills install https://github.com/HubSpot/agent-cli-skills --path quote-to-cashManually: put the files in place
.devin/skills/quote-to-cash/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add HubSpot/agent-cli-skills --skill quote-to-cash -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about quote-to-cash
How do I install quote-to-cash in Claude?
Run npx skills add HubSpot/agent-cli-skills --skill quote-to-cash -a claude-code. The skill lands in ~/.claude/skills/quote-to-cash/SKILL.md.
What does quote-to-cash do?
It creates products and quotes in HubSpot through the CLI, associates quote line items with a deal, and reads invoices and subscriptions. It works on top of the hubspot objects and hubspot associations commands.
Why do invoices and subscriptions return a 403?
Those object types need the matching read scope on the active token, such as invoices-read or subscriptions-read. Without it the request is rejected.
Source text
SKILL.md · Apache-2.0
---
name: quote-to-cash
description: Build the product catalog, assemble quotes (line items + associations to deals), and track invoices and subscriptions through to revenue.
triggers:
- "create product"
- "product catalog"
- "build a quote"
- "create quote"
- "line items"
- "associate line items"
- "outstanding invoices"
- "overdue invoices"
- "active subscriptions"
- "quote to cash"
---
## Resources
| File | When to use |
|---|---|
| `resources/q2c-essentials.md` | Six-field cheat sheet, association directions, portal caveats for invoices/subscriptions/orders/carts. |
## Foundations
Read `bulk-operations/SKILL.md` first — JSONL piping, batch read, pagination, and the dry-run/digest/confirm flow for destructive ops live there. Reshape recipes (read → write payload) are in `bulk-operations/resources/json-patterns.md`.
`hubspot <command> --help` is the source of truth. Object types are plural (`products`, `line_items`, `quotes`, `invoices`, `subscriptions`). Never hardcode property tables — `hubspot properties list --type <type>` is one call away. Verify any enum value the agent is about to write with `hubspot properties options-list --type <type> <property> | jq -r '.value'`.
Portal note: `invoices`, `subscriptions`, `orders`, `carts` show an empty `objectTypeId` in `hubspot objects types`. They work through `objects search`/`list` when the token has the matching scope (`invoices-read`, `subscriptions-read`, etc.) and 403 otherwise. CLI-created quotes are always `DRAFT`; approval routing, share links, PDF generation, and invoice creation usually require the HubSpot UI.
## 1. Create a product
```bash
hubspot objects create --type products \
--property name="Enterprise License" \
--property price=12000 \
--property hs_sku=ENT-001
```
For a recurring product set `recurringbillingfrequency`; check the API enum values first with `hubspot properties options-list --type products recurringbillingfrequency | jq -r '.value'`. Bulk-import a catalog by piping JSONL of `{"properties":{...}}` to `hubspot objects create --type products --dry-run`.
## 2. Build a quote: line items → quote → associations
`objects create` emits one result line per stdin line, in input order. That lets you build line items, capture their IDs, and associate them to the new quote in three pipes — no per-record shell loop.
```bash
DEAL_ID=12345
# 1. Create the line items. items.jsonl holds {"name":..,"qty":..,"price":..,"product_id":..} per line.
jq -c '{properties:{
name:.name, quantity:(.qty|tostring), price:(.price|tostring),
hs_product_id:.product_id, hs_line_item_currency_code:"USD"
}}' items.jsonl \
| hubspot objects create --type line_items > /tmp/lineitems.jsonl
# 2. Create the quote.
QUOTE_ID=$(hubspot objects create --type quotes \
--property hs_title="Acme Corp - 2026" \
--property hs_expiration_date=2026-06-30 \
--property hs_currency=USD \
--format json | jq -r '.data.id // .id')
# 3. Associate every new line item to the quote in one pipe.
jq -r '.id' /tmp/lineitems.jsonl \
| jq -cR --arg q "$QUOTE_ID" '{from:("quotes:" + $q), to:("line_items:" + .)}' \
| hubspot associations create
# 4. Link the quote to the deal.
hubspot associations create --from "deals:$DEAL_ID" --to "quotes:$QUOTE_ID"
```
Discount handling — `discount` is the writable percentage (`10` = 10% off). `hs_total_discount` is HubSpot-computed; do not set it. Verify with `hubspot properties get --type line_items hs_total_discount` (property name is positional; look for `modificationMetadata.readOnlyValue:true`) before relying on this in a portal you don't own.
Promote a quote out of `DRAFT` when ready to share. `objects update` is irreversible — dry-run first, then re-run with the digest and `--confirm <quote_id>`:
```bash
hubspot objects update --type quotes <quote_id> --property hs_status=APPROVAL_NOT_NEEDED --dry-run
hubspot objects update --type quotes <quote_id> --property hs_status=APPROVAL_NOT_NEEDED --digest <hash> --confirm <quote_id>
```
Verify `hs_status` enum values for your portal: `hubspot properties options-list --type quotes hs_status | jq -r '.value'`.
## 3. Track invoices
The CLI reads invoice data and updates status; creation usually needs HubSpot Commerce + UI. Filter by `hs_invoice_status` and date.
```bash
# All outstanding invoices
hubspot objects search --type invoices \
--filter "hs_invoice_status=OUTSTANDING" \
--properties hs_number,hs_amount_billed,hs_balance,hs_due_date
# Past-due (overdue) invoices, dynamic date
hubspot objects search --type invoices \
--filter "hs_due_date<$(date +%Y-%m-%d) AND hs_invoice_status!=PAID" \
--properties hs_number,hs_due_date,hs_balance
# Invoices billed in the last 30 days
hubspot objects search --type invoices \
--filter "hs_invoice_date>=$(date -v-30d +%Y-%m-%d 2>/dev/null || date -d '30 days ago' +%Y-%m-%d)" \
--properties hs_number,hs_amount_billed,hs_invoice_date
```
Verify the status enum the same way: `hubspot properties options-list --type invoices hs_invoice_status | jq -r '.value'`.
## 4. Track subscriptions
Same shape, filter on `hs_subscription_status`. Verify the enum values before writing the filter — do not hardcode `ACTIVE`/`CANCELLED`/`PAST_DUE`:
```bash
hubspot properties options-list --type subscriptions hs_subscription_status | jq -r '.value'
# Then filter (case matters)
hubspot objects search --type subscriptions \
--filter "hs_subscription_status=<value-from-above>" \
--properties hs_mrr,hs_arr,hs_subscription_status
# Sum MRR across active subs
hubspot objects search --type subscriptions \
--filter "hs_subscription_status=<active-value>" --format json \
| jq '[.data[].properties.hs_mrr | select(. != null and . != "") | tonumber] | add // 0'
```
## Known constraints
- `invoices`, `subscriptions`, `orders`, `carts`, `payments` need the matching read scope on the active token; 403 means the user OAuth login or private-app token is missing the scope.
- `objects delete` on products/quotes/line_items works under both user OAuth (`hubspot auth login`, with the object's write scope) and a service key (`export HUBSPOT_ACCESS_TOKEN=<token>`); a 403 means the active token is missing that write scope. The exception is the `--gdpr` permanent purge, which requires a service key — the GDPR endpoint does not accept user OAuth tokens. See `bulk-operations/SKILL.md` for the dry-run → digest → confirm flow before bulk-deleting catalog records.
- Quote share links, PDF generation, approval routing, and from-scratch invoice creation are UI-only — the CLI updates records but cannot send a quote to a customer.
- `hs_total_discount` on line items is read-only — set `discount` (percentage) instead.
© HubSpot · Apache-2.0 · HubSpot/agent-cli-skills