Structured Note Reference Data

Explorer Contact Us
More ▾

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.


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_typeWhat happened
COUPON_EARNEDThe 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_MISSEDThe coupon barrier was breached. No payment. On a memory note the coupon is banked, not lost.
AUTOCALLThe autocall trigger was met and the note terminated early. Carries payment_pct_of_notional.
RATE_RESETA 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_SETTLEMENTTerminal principal redemption at maturity. Carries payment_pct_of_notional.

And the fields on each event:

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_dateevent_typethreshold_pctworst_underlierperformance_pctpayment_pct_of_notional
2026-03-27COUPON_EARNED70.0000Nasdaq-100 Index92.6792null
2026-04-27COUPON_EARNED70.0000Energy Select Sector SPDR Fund101.5200null
2026-05-27AUTOCALL100.0000Energy Select Sector SPDR Fund101.8866100.0000
2026-05-27COUPON_EARNED70.0000Energy Select Sector SPDR Fund101.8866null

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_reasonMeaning
no_piecewise_payoffNo payoff profile was extracted for this note.
final_observation_pendingThe final closing level hasn't been ingested yet. The one transient reason — retried on every subsequent run.
final_level_unanchorableThe final level couldn't be anchored to the note's initial fixing.
final_level_outside_piecewise_rangeThe final level sits beyond the recorded breakpoints; extrapolating would be a guess.
piecewise_unbounded_segmentThe profile ends in an unbounded "unlimited upside" segment whose slope isn't recoverable.
piecewise_ambiguous_at_levelThe 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_reasonMeaning
no_observation_scheduleNo observation schedule was extracted.
unparseable_schedule_dateA schedule date with a zero day or month; fabricating a real date would evaluate wrong.
unsupported_observation_scopeAn observation scope we don't yet evaluate. Supported today: a single underlier, worst-of, and weighted-average baskets.
unresolved_underlierAn underlier we couldn't map to a price or level series.
missing_initial_fixingNo initial fixing level for an underlier.
initial_level_unverifiableWe have a level series, but it doesn't reconcile to the printed initial fixing, so we don't trust it.
unknown_rate_indexA floating-rate benchmark we don't carry fixings for.
rate_threshold_direction_unrecordedA rate barrier whose direction (above or below) wasn't captured.
contingent_coupon_without_barrierThe coupon is contingent but no barrier was extracted, so there's nothing to test.
basket_leg_unresolvedA basket leg's initial level couldn't be anchored.
basket_weights_incompleteFewer than two legs, a missing or non-positive weight, or weights that don't sum to about 100.
unsupported_basket_observation_typeA window-averaged rather than point-in-time closing basket observation.
unsupported_basket_calculation_methodA 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:

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:

messageCodeWhen
1010ISIN is malformed or fails its check digit (400).
1040Well-formed ISIN, but no condition state on file (404).
1033The API key has lapsed (403).
1050The 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

MethodPathPurpose
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/versionReport 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.

Want this data flowing into your systems? Talk to us about coverage, delivery, and the API.

Contact us to learn more