Structured Note Conditions: API
A structured note's terms are extracted once. What those terms do changes every trading day: a coupon barrier is passed or breached, an autocall trigger fires, a floating rate fixes, a note reaches maturity and settles. The conditions API serves that lifecycle — the note's current condition state, plus the full ordered log of every condition event it has ever produced.
This is computed data, not extracted data. Every night after the close we take each note's observation schedule and barriers out of the reference record, cross them against the day's underlier levels and rate fixings, and recompute the note from inception. Nothing here is scraped off a term sheet or copied out of an issuer notice.
The API lives at https://api.sqxnotes.com/v1/. Authentication is by API key in the x-api-key header. We'll get you a key.
Like the pricing API and unlike the reference-data API, conditions are synchronous. There's no analysis pipeline to wait on — every call returns its answer on the connection. No jobs, no polling.
The five-second version
curl -H "x-api-key: $API_KEY" \
"https://api.sqxnotes.com/v1/conditions/byIsin?isin=US17332URD09"
You get back 200 with the note's condition state and its whole event history, or 404 if we hold no condition state for that ISIN. That's the whole flow.
What you get back on success
Every response — success or error — comes in the same envelope as the rest of the v1 API: a data object, an errors array (empty on success), and a meta block with a request id and the server's as_of timestamp. On a 200, data carries four keys: the isin you asked for, a state object, an events array, and a reextraction block that is null on almost every call (see "When your read repairs the note").
Here is a real response, trimmed to the state block and the first event:
{
"data": {
"isin": "US17332URD09",
"state": {
"evaluable": "TRUE",
"ineligible_reason": null,
"status": "AUTOCALLED",
"autocall_date": "2026-05-27",
"last_observation_evaluated": "2026-05-27",
"next_observation_date": null,
"pending_data_observations": 0,
"missed_coupons_in_memory": 0,
"settlement_ineligible_reason": null,
"last_evaluated_at": "2026-07-24 23:46:32",
"changed_date": "2026-07-25 04:46:33"
},
"events": [
{
"observation_date": "2026-03-27",
"event_type": "COUPON_EARNED",
"payment_date": "2026-04-01",
"threshold_pct": "70.0000",
"worst_underlier": "Nasdaq-100 Index®",
"observed_level": "23132.769531",
"initial_level": "24960.040000",
"performance_pct": "92.6792",
"measurement_basis": "LEVEL",
"coupon_rate_pct": "13.800000",
"recovered_missed_coupons": null,
"rate_code_1": null,
"rate_value_1": null,
"rate_code_2": null,
"rate_value_2": null,
"payment_pct_of_notional": null,
"changed_date": "2026-07-24 01:36:10"
},
{ "...": "three more events" }
],
"reextraction": null
},
"errors": [],
"meta": {
"request_id": "3f1c8a2e-9b04-4e77-bd1a-0c2f5e6a7d10",
"as_of": "2026-07-25T20:00:00+00:00",
"page": null
}
}
Both objects always carry the same key set, whatever the note. Anything that doesn't apply to a given note or event comes back as JSON null — never an empty string or an "N/A" sentinel.
One serialization detail worth knowing before you parse: decimal fields come back as JSON strings, not numbers — "70.0000", not 70.0. These are exact decimal quantities compared against exact thresholds, and round-tripping them through a float would quietly change the answer several places out. Integer counters (pending_data_observations, missed_coupons_in_memory, recovered_missed_coupons) are real JSON numbers. Dates are YYYY-MM-DD; timestamps are YYYY-MM-DD HH:MM:SS in UTC.
The authoritative, field-by-field contract — every key, enum, and status code, including the response envelope — is the OpenAPI 3.1 spec (openapi.yaml). This page is the narrative guide; the spec is the source of truth.
Reading the state block
state is where the note stands right now. One row per note, rewritten whenever we re-evaluate it.
status—ACTIVE,AUTOCALLED, orMATURED. The note's terminal condition, or the absence of one.evaluable—"TRUE"or"FALSE", as a string. Whether we can evaluate this note's conditions exactly. See "Evaluate or refuse" below.ineligible_reason—nullwhenevaluableis"TRUE"; otherwise a stable reason token, optionally followed by a colon and free detail (unresolved_underlier: S&P 500 Futures Excess Return Index). Filter on the token, display the detail.autocall_date— the observation date on which the autocall trigger was met.nullunlessstatusisAUTOCALLED.last_observation_evaluated— the most recent scheduled observation date we were able to decide. Anything after it is either in the future or waiting on data.next_observation_date— the next scheduled observation date still ahead of us.nullon a terminated note, andnullon anACTIVEnote whose schedule is exhausted.pending_data_observations— how many scheduled observations we could not decide because a level or a rate fixing hadn't landed yet. Non-zero is not an error; it's a promise to retry. Notes with pending observations are re-selected on every subsequent run until the data arrives.missed_coupons_in_memory— on a memory note, how many missed coupons are currently banked and would be released on the next passing observation.0on notes without a memory feature.settlement_ineligible_reason— why aMATUREDnote has noMATURITY_SETTLEMENTevent.nullwhen settlement was computed, andnullon a note that hasn't matured. See "Maturity settlement" below.last_evaluated_at— when our engine last recomputed this note.changed_date— when this state row last actually changed. Use it, notlast_evaluated_at, to detect a real transition: we re-evaluate far more often than anything moves.
Reading the events log
events is a sparse transition log ordered by observation_date, then by event_type within a date. Only decisions land here. A fixed unconditional coupon decides nothing daily and emits nothing — so a plain fixed-rate note can legitimately return an empty events array alongside a perfectly healthy state.
The five event types:
| event_type | What happened |
|---|---|
COUPON_EARNED | The contingent coupon condition was met on this observation date. On a memory note, recovered_missed_coupons says how many banked coupons were released alongside it. |
COUPON_MISSED | The coupon barrier was breached. No payment. On a memory note the coupon is banked, not lost. |
AUTOCALL | The autocall trigger was met and the note terminated early. Carries payment_pct_of_notional. |
RATE_RESET | A floating or steepener coupon rate was determined for the period. Emitted for unconditional floaters; a contingent floater gets its determined rate stamped onto its COUPON_EARNED or COUPON_MISSED row instead. |
MATURITY_SETTLEMENT | Terminal principal redemption at maturity. Carries payment_pct_of_notional. |
And the fields on each event:
observation_date— the scheduled observation date being decided. Together withevent_typethis uniquely identifies an event.payment_date— the date the resulting cash flow pays, when the schedule records one.threshold_pct— the barrier or trigger the observation was measured against, as a percentage of initial. The coupon barrier on the coupon events, the autocall trigger onAUTOCALL. On rate-range barriers it carries the top of the range.worst_underlier— the underlier that actually decided the observation. On a worst-of basket this changes from date to date, and that's the point: it tells you which leg drove the verdict.nullon a weighted-average basket, where no single leg decides.observed_level,initial_level,performance_pct— the level we observed, the note's initial fixing level, and the ratio as a percentage. Weighted-average baskets are rebased to 100 at inception, so they report aninitial_levelof100.measurement_basis— how the level was established. See "Levels we trust" below.coupon_rate_pct— the annualized coupon rate for the period.recovered_missed_coupons— memory coupons released by thisCOUPON_EARNED.nullon notes without a memory feature.rate_code_1/rate_value_1,rate_code_2/rate_value_2— the rate fixings used to determine the period's rate, by benchmark code (SOFR,CMT_10Y, and so on). The second pair carries the other leg of a spread or steepener formula.payment_pct_of_notional— cash redeemed per 100 notional. Populated onAUTOCALLandMATURITY_SETTLEMENTonly, andnullon those when the amount could not be established from the extracted terms — refused, not guessed.changed_date— when this event row last changed. Events are recomputed against revised market data, so this is your revision signal.
We deliberately don't serve a per-period coupon cash amount. coupon_rate_pct is annualized, and day-count conventions aren't reliably extractable from a term sheet — publishing a cash figure would mean guessing one. Approximate it as coupon_rate_pct times the period fraction; consecutive payment_dates give you the period.
A worked example
US17332URD09 is a worst-of autocallable on the Nasdaq-100 and the Energy Select Sector SPDR Fund: 13.8% contingent coupon observed monthly, a 70% coupon barrier, a 100% autocall trigger, no memory feature. Its four events are the whole life of the note.
| observation_date | event_type | threshold_pct | worst_underlier | performance_pct | payment_pct_of_notional |
|---|---|---|---|---|---|
| 2026-03-27 | COUPON_EARNED | 70.0000 | Nasdaq-100 Index | 92.6792 | null |
| 2026-04-27 | COUPON_EARNED | 70.0000 | Energy Select Sector SPDR Fund | 101.5200 | null |
| 2026-05-27 | AUTOCALL | 100.0000 | Energy Select Sector SPDR Fund | 101.8866 | 100.0000 |
| 2026-05-27 | COUPON_EARNED | 70.0000 | Energy Select Sector SPDR Fund | 101.8866 | null |
Four things to notice.
A barrier is not a direction. On March 27 the worst leg was down 7.3% from its initial fixing, and the coupon was still earned — 92.68% clears a 70% barrier comfortably. Don't read a sub-100 performance_pct as a miss; read event_type.
The worst leg moves. The Nasdaq-100 decided the March observation, the Energy SPDR decided the next two. worst_underlier is per-observation, not per-note, which is why it's on the event rather than in the state block.
One date, two thresholds. On May 27 the worst leg came in at 101.89% of initial, which cleared both the 70% coupon barrier and the 100% autocall trigger. Two independent tests, two independent events.
Redemption and coupon are never folded together. That May 27 date carries two rows: the AUTOCALL with payment_pct_of_notional of 100.0000 — par, this note has no call premium — and the call-date coupon as its own COUPON_EARNED. If you're summing cash flows, sum both. payment_pct_of_notional is principal only, on autocall and at maturity alike.
Memory coupons
A memory note banks a missed coupon instead of losing it, and releases the whole backlog on the next observation that passes. Both sides of that are visible: COUPON_MISSED rows accumulate, state.missed_coupons_in_memory carries the current backlog, and the COUPON_EARNED that releases them reports the count in recovered_missed_coupons.
US09711NES53 shows a full cycle against an 80% barrier — a miss at 75.75%, then a recovery at 85.22% carrying recovered_missed_coupons: 1 (two coupons paid on one date), then three more misses, leaving missed_coupons_in_memory at 3 and still live. One of those misses came in at 79.9910% against a threshold of 80.0000%. Nine thousandths of a percent short is short — which is exactly why the decimals are strings.
Maturity settlement
When a note reaches maturity we evaluate its piecewise payoff profile at the final observed performance and emit a MATURITY_SETTLEMENT event carrying the redemption as payment_pct_of_notional. The profile is interpolated linearly between recorded breakpoints and never extrapolated past them.
US48134XYY56, a Russell 2000 note with a capped payoff, reached a final performance of 143.7478% and redeemed at 118.5000 — the cap, not the index return.
As with autocall, that figure is principal only. The final coupon, if there is one, stays its own COUPON_EARNED row.
When we can't establish the settlement exactly, we don't guess. No MATURITY_SETTLEMENT event is emitted, and state.settlement_ineligible_reason says why:
| settlement_ineligible_reason | Meaning |
|---|---|
no_piecewise_payoff | No payoff profile was extracted for this note. |
final_observation_pending | The final closing level hasn't been ingested yet. The one transient reason — retried on every subsequent run. |
final_level_unanchorable | The final level couldn't be anchored to the note's initial fixing. |
final_level_outside_piecewise_range | The final level sits beyond the recorded breakpoints; extrapolating would be a guess. |
piecewise_unbounded_segment | The profile ends in an unbounded "unlimited upside" segment whose slope isn't recoverable. |
piecewise_ambiguous_at_level | The final level lands exactly on a digital jump, or on a duplicated non-monotone branch. |
A refused settlement refuses only the settlement. The note's coupon and autocall history stays intact and served — the two are orthogonal.
Evaluate or refuse
The load-bearing design decision behind this endpoint: a note whose conditions cannot be evaluated exactly is refused, never approximated. evaluable comes back "FALSE" with a reason, and the note produces no events. We'd rather tell you that than serve a coupon verdict computed off an underlier we couldn't confidently identify.
The reason tokens are stable strings you can filter on:
| ineligible_reason | Meaning |
|---|---|
no_observation_schedule | No observation schedule was extracted. |
unparseable_schedule_date | A schedule date with a zero day or month; fabricating a real date would evaluate wrong. |
unsupported_observation_scope | An observation scope we don't yet evaluate. Supported today: a single underlier, worst-of, and weighted-average baskets. |
unresolved_underlier | An underlier we couldn't map to a price or level series. |
missing_initial_fixing | No initial fixing level for an underlier. |
initial_level_unverifiable | We have a level series, but it doesn't reconcile to the printed initial fixing, so we don't trust it. |
unknown_rate_index | A floating-rate benchmark we don't carry fixings for. |
rate_threshold_direction_unrecorded | A rate barrier whose direction (above or below) wasn't captured. |
contingent_coupon_without_barrier | The coupon is contingent but no barrier was extracted, so there's nothing to test. |
basket_leg_unresolved | A basket leg's initial level couldn't be anchored. |
basket_weights_incomplete | Fewer than two legs, a missing or non-positive weight, or weights that don't sum to about 100. |
unsupported_basket_observation_type | A window-averaged rather than point-in-time closing basket observation. |
unsupported_basket_calculation_method | A basket calculation method other than an explicit weighted average. |
Refusals are not permanent, and most clear on their own. When an underlier mapping is validated or a missing level series is backfilled, the note is re-selected on the next daily run and un-refused without waiting for its next observation date.
When your read repairs the note
Some refusals aren't a market-data gap at all — they're a terms gap. The note was extracted before we taught the extractor to capture the field the barrier needs, so there is nothing to test against. We keep a manifest of exactly which notes are behind which extraction capability.
Asking for a refused note consults that manifest. If your ISIN is on it, the call fires the re-extraction itself and tells you so in the reextraction block:
{
"isin": "US0000000000",
"state": { "evaluable": "FALSE", "ineligible_reason": "contingent_coupon_without_barrier", "...": "..." },
"events": [],
"reextraction": {
"tables": "r_coupon_barrier,r_observation_schedule",
"command_id": "8f3c0b1e-2a55-4c07-9d18-6b2e1f0a4c73"
}
}
tables names the term sections being re-pulled from the filing. command_id is our internal handle for the run — there's nothing for you to poll with it; it's there so you can correlate with us if you ever need to ask what happened.
Three things this deliberately is not.
It is not a 202 withhold. The reference-data API withholds its payload behind a poll handle when a newer filing supersedes the record on file, because serving the old terms would mean serving stale data. A condition state is different: it's an honest evaluate-or-refuse verdict, not stale data. So you get your 200 and the verdict immediately, and the repair happens alongside it.
It is not a promise the answer changed. Re-extracted terms can't move a verdict until the nightly engine recomputes the note. Expect a corrected verdict on the next business-day run, not on a retry a second later. Polling this endpoint in a tight loop after seeing a reextraction block will just show you the same refusal.
It is not fired for a note that has nothing to gain. Only a note carrying an unresolved refusal is a candidate — either an ineligible_reason, or a settlement refusal other than the transient final_observation_pending (re-extracting a filing can't supply a missing market close). An evaluable note reads reextraction as null even if it is technically behind a capability, and so does a note whose repair would need a full re-analysis rather than a targeted one.
The block is best-effort by design: if the repair can't be dispatched, you still get your 200 and reextraction comes back null. A conditions read never fails because of maintenance work happening behind it.
Levels we trust
measurement_basis on each level-bearing event tells you how the level behind it was established:
LEVEL— the underlier's own series, validated against the note's printed initial fixing level at the fixing date. Exact filing units.PROXY_RETURN— the same-series return taken via the underlier's price proxy, in the proxy's own currency. An approximation, and typical for index underliers with no directly quotable price series.MIXED— a weighted-average basket whose legs anchor on different bases.
This distinction exists because a stored close is not automatically the right close. Our price table keeps one row per date and identifier, and a foreign print of a US-listed name can overwrite the primary listing. So a series is never trusted blind: it has to reproduce the note's own printed initial fixing before we'll measure a barrier against it. initial_level_unverifiable is that gate saying no.
Freshness, and what "recomputed" means
The engine runs after the close on business days, once the day's prices are loaded. state.changed_date and each event's changed_date are the fields to watch — they move only when something actually changed.
Two consequences worth planning for.
Events are not immutable. We recompute whole notes from inception on every selection, so a revised or corrected market print can change a past event's performance_pct, flip a verdict, or make an event appear where a pending observation used to be. changed_date is how you spot it. If you cache, cache against changed_date — not against the fact that you fetched the ISIN once.
Conditions are not a substitute for the reference record. This endpoint is deliberately separate from /reference/byIsin for that reason: reference terms are extracted once and stay cacheable for a long time, condition state changes daily, and folding the two together would poison the cacheability of the terms. Fetch terms rarely, conditions often.
When you get input wrong
Bad input returns 400 with a structured error in the errors array, in the same ErrorDetailV2 shape the rest of the v1 API uses — a numeric messageCode, a category, a machine-readable type, and a human message:
{
"data": null,
"errors": [
{
"category": "VALIDATION_ERROR",
"type": "INVALID_INPUT",
"messageCode": 1010,
"message": "Invalid ISIN: ISIN must be 12 characters, got 9",
"messageValues": { "isin": "NOTANISIN" }
}
],
"meta": { "...": "..." }
}
The codes you'll see on this endpoint:
| messageCode | When |
|---|---|
1010 | ISIN is malformed or fails its check digit (400). |
1040 | Well-formed ISIN, but no condition state on file (404). |
1033 | The API key has lapsed (403). |
1050 | The key is over its distinct-ISIN quota (429). |
A 404 here means something narrower than on the other endpoints: we hold no condition state for this ISIN. Either it isn't a structured note we track conditions on, or the daily engine hasn't reached it yet. It does not mean we've never heard of the security — an ISIN can be fully covered by the reference and pricing APIs and still 404 here.
Note the distinction on the other side, too: a note that is refused is not a 404. You get 200, a full state with evaluable of "FALSE" and a reason, and an empty events array. The refusal is the answer.
Lowercase identifiers are silently uppercased — copy-paste from a spreadsheet just works.
Endpoint reference
| Method | Path | Purpose |
|---|---|---|
| GET | /conditions/byIsin?isin={ISIN} | Condition state and full event history for one structured note. 200 with state plus events, 404 if we hold no condition state for the ISIN. |
| GET | /version | Report API version. No auth required. |
The payload for any covered ISIN is also browsable on this site — https://sqxnotes.com/api/conditions/US17332URD09.json — where the isin, state, and events keys are exactly the live API's data payload. The static file additionally carries the timeline-overlay keys the Explorer's charts use (schedule, underliers, composite, scope, maturity_date, as_of); those are site rendering data and not part of the API contract.
Authentication & versioning
Every request except /version needs a valid API key in x-api-key. Conditions access is granted through the structured-note reference scope: a key entitled to reference.structured_note, or to the broader reference, reaches this endpoint. Metering is separate, though — conditions bill on their own ledger, independent of reference and pricing, so the same ISIN unlocks once per product family rather than once across all three. A key can also carry an expiry (403, messageCode 1033) or a lifetime distinct-ISIN quota (429, messageCode 1050).
The v1 base path is stable. Field additions are non-breaking and ship in-place. One forward-compatibility note specific to this endpoint: the event-type and reason enums above are open. New tokens can appear without a major version, so treat an unrecognized reason token as "something we can't evaluate" rather than a parse error. Breaking changes — renames, removals, semantic shifts — land at a new major version on a new stage. Minor and patch updates are reported at /version.