We are running into critical limitations with the Payment Links API (and consequently the Subscriptions created through them) regarding Product Catalog associations, discounts, and custom payment terms.
Here is a breakdown of the core issues we are experiencing:
1. Line items lose connection to hs_product_id / SKU
When line items are added to a Payment Link (especially programmatically), they appear to lose their link/association to the actual Product in the HubSpot Product Catalog (hs_product_id / SKU).
As a result, the resulting Subscription created by the Payment Link also completely lacks the Product ID / SKU association.
Workaround question: Can/should we rely on name and description matching as a fallback, or is there a plan to persist hs_product_id / SKU on Payment Link line items?
2. 500 Internal Server Error on Discounts with Recurring Products
When attaching line items based on existing products:
Adding a discount to a line item that has a recurring billing frequency throws an HTTP 500 Internal Server Error.
Discounts currently only seem stable on non-recurring/one-time items.
3. Inability to add fixed/one-off payments to recurring setups
It is currently not possible to combine or set up fixed payments (e.g., a 1-month setup fee or a single fixed payment) alongside a recurring subscription on the same Payment Link line item structure.
Questions for the HubSpot Team:
Is persisting hs_product_id / SKU through Payment Links → Subscriptions on the product roadmap? As well as other custom properties?
Is the HTTP 500 error on recurring product discounts a known bug currently being investigated?
What is the recommended best practice for tracking which catalog product a Subscription belongs to when created via Payment Links?
I’d keep the product-linkage thing separate from those two 500s.
For reporting, don’t try to join line items back to the catalog by name or description. Treat each line item as a snapshot of that transaction. When the Payment Link is created, persist an immutable catalog key or SKU on the line item itself. If the beta payload doesn’t keep it, store a simple {paymentLinkId, lineItemId, productId} mapping outside HubSpot. First check what the GET Payment Link response actually returns before checkout so we can see exactly where the identity is getting lost.
For the errors, take the same minimal payload and make three fixtures: recurring product only, then add the discount, then add the one-time fee. HubSpot already supports these combinations on the product, so a 500 on the last two is a beta API defect — not something to retry around. Capture the response body and correlation ID for each request. That gives Developer Support two clean reproducible cases instead of one combined payload.
For the missing product link, I wouldnt try to match the line item back to the product later using name or description. Those are basically transaction records. I’d write a stable SKU/catalog key onto the line item when creating the payment link. If the beta API still drops the productId, keep a small “paymentLinkId + lineItemId → productId” map outside HubSpot.
First thing I’d check is the GET payment link response before checkout. If the product identity is already missing there, at least you know exactly where it gets lost.
The 500s feel like a different issue. I’d reduce it to 3 tiny payloads: recurring only, recurring + discount, then recurring + one-time fee.
If the first works and the other two consistently 500, save the response body + correlation IDs and send those exact cases to HubSpot. I wouldnt add retries around a deterministic 500 like that.
I’ve reproduced the discount failure with two payment-link requests. The only change between them is the discount field on a recurring FROM_PRODUCT line item.
Baseline — recurring product only:POST /commerce/payment-links/2026-09/payment-links returned 201 and created payment link 1380171590879. The line item in the response has productId: "431750959351" and recurringBillingFrequency: "WEEKLY".
Same request with a discount: Adding "discount": "2" to that line item returned 500. The response body was: