Guides · bonds
The desk pattern "price off the swap curve" is two POSTs: /v1/curves/bootstrap turns deposit and swap quotes into zero-rate pillars, and /v1/bonds/price discounts the bond's cashflows on those pillars — returning clean/dirty price, accrued, the curve-implied YTM, duration and BPV.
Deposits pin the short end, par swaps the long end (valuation date fixed for reproducibility):
curl -s https://quantbox.dev/v1/curves/bootstrap \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"valuation_date": "2026-07-01",
"deposits": [{"tenor": "3M", "rate": 0.031},
{"tenor": "6M", "rate": 0.032}],
"swaps": [{"tenor": "2Y", "rate": 0.034},
{"tenor": "5Y", "rate": 0.036},
{"tenor": "10Y", "rate": 0.0375}],
"output_tenors": ["1Y", "2Y", "5Y", "10Y"]}'
"points": [
{"tenor": "1Y", "years": 1.0, "zero_rate": 0.032968, ...},
{"tenor": "2Y", "years": 2.008219, "zero_rate": 0.033388, ...},
{"tenor": "5Y", "years": 5.002740, "zero_rate": 0.035415, ...},
{"tenor": "10Y", "years": 10.008219, "zero_rate": 0.036964, ...}
]
Runs on the free tier — 500 calls/month, no credit card. Get a key. Full walkthrough of this call: bootstrap a yield curve from deposits and swaps.
The bond endpoint takes zero_curve as a list of {tenor_years, zero_rate}. Turning call 1's pillars into call 2's input is three lines:
zero_curve = [
{"tenor_years": round(p["years"], 4), "zero_rate": round(p["zero_rate"], 6)}
for p in curve["points"]]
Which, on the pillars above, is exactly:
[{"tenor_years": 1.0, "zero_rate": 0.032968},
{"tenor_years": 2.0082, "zero_rate": 0.033388},
{"tenor_years": 5.0027, "zero_rate": 0.035415},
{"tenor_years": 10.0082, "zero_rate": 0.036964}]
Conventions match by construction: the bootstrap outputs continuously compounded act/365 zeros, and zero_curve expects continuously compounded act/365 zeros. No conversion step to get wrong. (If you ever need the DF↔zero math anyway: discount factor vs zero rate.)
A 5% annual bond, issued 2024-07-01, maturing 2029-07-01, settling 2026-10-15, act/act — with zero_curve instead of a flat yield_rate:
curl -s https://quantbox.dev/v1/bonds/price \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"coupon_rate": 0.05,
"issue_date": "2024-07-01",
"maturity_date": "2029-07-01",
"settlement_date": "2026-10-15",
"frequency": "annual",
"day_count": "act/act",
"zero_curve": [{"tenor_years": 1.0, "zero_rate": 0.032968},
{"tenor_years": 2.0082, "zero_rate": 0.033388},
{"tenor_years": 5.0027, "zero_rate": 0.035415},
{"tenor_years": 10.0082, "zero_rate": 0.036964}]}'
{
"clean_price": 103.9307,
"dirty_price": 105.3827,
"accrued": 1.4521,
"ytm": 0.034451,
"modified_duration": 2.4892,
"convexity": 8.7997,
"bpv": -0.0262,
"conventions": {
"prices": "per 100 of face value (market quote convention)",
"settlement": "as provided (settlementDays=0, no implicit T+2)",
"ytm": "compounded annual, act/act",
"zero_curve": "zero rates continuously compounded, act/365",
"calendar": "TARGET, unadjusted schedule"
}
}
| Field | Value | Meaning |
|---|---|---|
| clean_price | 103.9307 | Quoted price per 100 face, discounted pillar by pillar on the curve. |
| dirty_price | 105.3827 | What you actually pay: clean + accrued (103.9307 + 1.4521). |
| accrued | 1.4521 | 106 days since the 2026-07-01 coupon, act/act: 5 × 106/365 = 1.4521. |
| ytm | 0.034451 | Implied YTM — the single flat yield (annual comp., act/act) that reproduces the curve price. This is the answer to "yield to maturity from a zero curve": 3.4451%. |
| modified_duration | 2.4892 | ΔP ≈ −D·P·Δy at the implied yield. |
| bpv | -0.0262 | Exact price change for +1 bp. Cross-check: 2.4892 × 105.3827 / 10,000 ≈ 0.0262. |
The implied YTM is the bridge between the two pricing worlds: the curve view (four zeros between 3.30% and 3.70%) collapses, for this particular bond's cashflows, into one flat 3.4451%. Duration, convexity and BPV in the response are computed at that implied yield — same machinery as the flat-yield case, explained in bond duration, convexity and BPV over HTTP.
import requests
BASE = "https://quantbox.dev"
H = {"X-API-Key": KEY}
# 1) bootstrap the curve from market quotes
curve = requests.post(f"{BASE}/v1/curves/bootstrap", headers=H, json={
"valuation_date": "2026-07-01",
"deposits": [{"tenor": "3M", "rate": 0.031},
{"tenor": "6M", "rate": 0.032}],
"swaps": [{"tenor": "2Y", "rate": 0.034},
{"tenor": "5Y", "rate": 0.036},
{"tenor": "10Y", "rate": 0.0375}],
"output_tenors": ["1Y", "2Y", "5Y", "10Y"]}).json()
# 2) map pillars -> zero_curve (the whole "integration")
zero_curve = [
{"tenor_years": round(p["years"], 4), "zero_rate": round(p["zero_rate"], 6)}
for p in curve["points"]]
# 3) price the bond off the curve
bond = requests.post(f"{BASE}/v1/bonds/price", headers=H, json={
"coupon_rate": 0.05, "issue_date": "2024-07-01",
"maturity_date": "2029-07-01", "settlement_date": "2026-10-15",
"frequency": "annual", "day_count": "act/act",
"zero_curve": zero_curve}).json()
print(f'clean {bond["clean_price"]:.4f} implied YTM {bond["ytm"]:.4%}')
# clean 103.9307 implied YTM 3.4451%
The same bond priced with "yield_rate": 0.04 instead of the curve comes back at 102.4892 clean (that call is walked through in price a bond via REST API). The curve prices it 1.44 points higher — no mystery: the bootstrapped zeros (3.30–3.70%) all sit below 4%, so every cashflow discounts less. That spread between "the curve's price" and "the market's flat yield price" is exactly what relative-value screens are built on.
Pillars are measured from settlement. In the bond call, tenor_years means years from the settlement date (2026-10-15), while call 1 bootstrapped as of 2026-07-01. Passing the pillars unchanged is the standard quick pattern; if you need the curve exactly as of settlement, re-bootstrap with valuation_date = settlement_date.
Interpolation differs between the two endpoints. The bootstrapper interpolates log-linearly on discount factors; the bond pricer rebuilds a curve from your pillars linearly in zero rates (act/365, extrapolation enabled beyond the last pillar). With pillars this close together the difference is beyond the fourth decimal — pass more pillars if you need tighter agreement.
These exact pillars produced this exact response. The numbers above come from the 4-/6-decimal pillars shown, not from hidden full-precision values — rerun the call and you match every digit. Rounding zeros to 6 decimals perturbs each rate by at most 5×10⁻⁷; at a BPV of 0.0262 per bp, that is on the order of a ten-thousandth of a point on this bond.
Run both calls yourself. Key in seconds, 500 free calls a month, full request schemas in the docs.
Get a free API keyNo credit card · schemas and error codes in /docs
Related: Bootstrap a yield curve from deposits and swaps · Discount factor vs zero rate · Price a bond via REST API · Bond duration, convexity and BPV over HTTP