How do you get a yield curve out of an API response?
A yield curve is a picture: maturity on the x-axis, yield on the y-axis, one line per date. The endpoint that feeds it does not return anything shaped like that. Getting from one to the other is a five-line transformation, and knowing it is the difference between reading about the curve and having one.
The call
GET /ust/yield-rates documents from and to as YYYY-MM-DD, filter[year] as an integer, page[limit] and page[offset], and fmt as json or csv. With no year it defaults to the current one.
Now the thing to know before you build anything on this family, because it is not in the documentation. Only filter[year] does anything. from, to, filter[from], filter[to] and page[limit] are all accepted and all ignored: a request for a single day returns the entire year, on every one of the four Treasury endpoints. Checked on all four.
That is not a general property of the API. The filter[...] endpoints later in this course, and every credit-risk path, honour their filters exactly. It is this family, documenting a convention it does not implement, which makes it a good early lesson in reading a response rather than a parameter table.
So narrowing is your job here: fetch the year, filter client-side. The response is an object with three keys: meta (carrying total, and no page, whatever you asked for), data, and links (a next URL or null). Every rates and credit endpoint in this course uses that envelope; the macro endpoints from Unit 1 do not.
The shape nobody expects
Each element of data is not a day. It is a single point on a single day's curve:
Three keys: date, tenor, rate. This is long format — one row per observation — where most people picture wide format, one row per date with a column per maturity. Building a curve means grouping by date and pivoting on tenor. Code written against the wide assumption does not error; it looks for a per-maturity key, finds nothing, and draws an empty chart.
It also explains a count that otherwise looks alarming. filter[year]=2026 returned meta.total of 1988 records at the end of July, for a year that had had 142 Treasury business days. The total grows by fourteen every session, so treat the product rather than the figure as the thing to remember. There are 14 tenors — 1M, 1.5M, 2M, 3M, 4M, 6M, 1Y, 2Y, 3Y, 5Y, 7Y, 10Y, 20Y, 30Y — and 142 × 14 = 1,988. You are counting cells, not days.
The tenor set is not a constant
That 1.5M tenor is the six-week constant-maturity point, a relatively recent addition. Which raises the question of what happens further back.
Call filter[year]=1990 and meta.total is 2250 over 250 business days — nine tenors, not fourteen. On 2 January 1990 the available points were 3M 7.83 through 10Y 7.94 to 30Y 8.00, with no 1M, 1.5M, 2M or 4M, and no 20Y — and not for want of issuance. filter[year]=2000 returns a 20Y on all 251 days at 6.94, in an era when the Treasury issued none. Publication gap, not issuance gap.
A pivot that hard-codes fourteen columns therefore returns nulls for five of them in the early 1990s, and filling those forward draws a short end that never existed.
Enumerate the tenors you actually receive, per date range, and take the response as the authority on its own shape. Checking a live payload before writing the parser is trust-but-verify applied to structure rather than values; it costs one request.
One real curve
Requesting the last rows of 2026 gives the complete curve for 27 July 2026:
| Tenor | Rate | Tenor | Rate |
|---|---|---|---|
| 1M | 3.80 | 3Y | 4.35 |
| 1.5M | 3.89 | 5Y | 4.40 |
| 2M | 3.95 | 7Y | 4.52 |
| 3M | 3.96 | 10Y | 4.65 |
| 4M | 4.05 | 20Y | 5.15 |
| 6M | 4.10 | 30Y | 5.12 |
| 1Y | 4.14 | ||
| 2Y | 4.31 |
Two features need no modelling. The curve rises from 3.80% at one month to 5.15% at twenty years — 135 basis points of slope. And it then falls: the 30-year at 5.12% sits 3 basis points below the 20-year. A curve can be upward-sloping over most of its length and inverted at the very long end on the same day.
The standard slope measure, 10Y minus 2Y, is 4.65 − 4.31 = 34 basis points. On 1 June 2026 (10Y 4.47, 2Y 4.05) it was 42. Over those eight weeks the 2-year rose 26 basis points and the 10-year rose 18, so the curve moved up and flattened at once. Only one of those movements is visible if you watch a single tenor.
Every value here is a constant maturity Treasury (CMT) par yield: the coupon a hypothetical security issued today at exactly that maturity would need to pay to trade at par, derived from a curve fitted to bid yields on real securities. There is no bond with the CUSIP "10Y". The conceptual companion is the Fixed Income course's lesson on curve shapes; this lesson is only about getting the numbers out cleanly.
Try it now
- Here is
/ust/yield-rates?filter[year]=1990&page[limit]=14&page[offset]=0, then the same call forfilter[year]=2026. Two things to find: the distincttenorvalues differ between the two years, andpage[limit]=14did nothing. Name the tenors 1990 lacks, and show frommeta.totaland the last row that you received both years whole.
- Here is one full date, the first fourteen rows of
filter[year]=2026. Pivot them into a single record keyed by tenor, deriving the column list from the rows rather than declaring it. That pivot is the function every chart in this unit depends on.
- Compute 10Y minus 2Y in basis points for that first date, and again for the newest date in the table below (the 2Y is
data[-7], the 10Ydata[-3]). You have just measured the change in the curve's slope over the year with two subtractions.