Structured Note Reference Data

Explorer Contact Us
More ▾

Structured Note Reference Data: API

The reference data feed is also available as an HTTP API. You give us an ISIN or a PDF, we give you the same nested record that ships in the daily file feed — every field on the term sheet, every underlier, every payoff breakpoint.

The API base URL is https://api.sqx.com/v1/. Requests authenticate with an API key in the x-api-key header. Contact us for a key.


The five-second version

If you have an ISIN, ask for it:

curl -H "x-api-key: $API_KEY" \
    "https://api.sqx.com/v1/reference/byIsin?isin=US78017UPK06"

If you have a PDF, send it:

curl -H "x-api-key: $API_KEY" \
    -H "Content-Type: application/pdf" \
    --data-binary @./pricing_supplement.pdf \
    https://api.sqx.com/v1/reference/byPdf

Either way, when we already have the analyzed record, you get back 200 with the full payload right away. Skip the next section.


When we don't have it yet

Sometimes you'll ask about an ISIN we know exists — say, one that just hit our SEC ingest queue — but our extraction pipeline hasn't run on it yet. Or you'll send us a PDF whose ISIN we've never seen. In both cases we can't answer immediately. The note has to be analyzed end-to-end: text extracted, ISIN resolved, underliers identified, payoff structure decoded. That takes a few minutes per filing.

So the API uses the standard asynchronous request-reply pattern. Instead of making you wait on the connection, we accept the request, kick off the analysis on our side, and hand you back a job handle to poll.

You get a 202 Accepted response with three things in it:

{
    "job_id": "df8414cbddf37d265ecc6e5961b8d9847d9cb9ddeeb06656848346ce6fe2e61d",
    "status": "queued",
    "status_url": "https://api.sqx.com/v1/reference/jobs/df8414cbddf37d265ecc6e5961b8d9847d9cb9ddeeb06656848346ce6fe2e61d"
}

A Location header on the same response repeats the status_url, in case your HTTP client prefers to read it from there.

You then GET the status_url on a polling loop. While we're working, it returns 202 with {"status": "queued"} (the upload is in our queue but hasn't started analysis yet) or {"status": "processing"} (extraction is running). When the analysis completes, the same URL flips to 200 with the same nested record you would have gotten from byIsin directly — or to 404 if the filing was analyzed but turned out to hold no structured note record. Keep polling while you get a 202; stop on anything else.

A reasonable poll loop:

deadline=$(( $(date +%s) + 600 ))
while [ "$(date +%s)" -lt "$deadline" ]; do
    code=$(curl -s -w '%{http_code}' -o /tmp/r.json \
        -H "x-api-key: $API_KEY" "$status_url")
    case "$code" in
        200) cat /tmp/r.json; break ;;     # done — payload is in /tmp/r.json
        202) sleep 15 ;;                   # still working
        4*|5*) echo "error: $code"; cat /tmp/r.json; break ;;
    esac
done

Analysis runs on a pipeline separate from the API, so frequent polling leaves its speed unchanged: poll at 15-second intervals. Set a time limit on the loop. If a failure occurs on our side, a loop that stops and reports the failure is more useful than an endless loop.

Total wait is usually under five minutes; complex multi-tranche supplements can run longer.

The same status_url works for both flows — the only difference is the job_id shape (a 64-character SHA for PDF uploads, a numeric filing ID for ISIN lookups). You don't have to handle them differently.


What you get back on success

The 200 payload is the full nested reference-data record — same structure as the file feed. Top-level keys, in the order they appear:

call_leg                  issuer-call / autocall provisions
classification            payoff archetype, link type, direction, tax + legal
coupon_leg                coupon mechanics, conditionality, memory, payout
fee_economics             public-offering price, fees, estimated value
isin                      the 12-char identifier
mechanics                 issue date, denomination, listing, settlement
observation_leg           observation schedule and reference levels
payoff_legs               piecewise-linear payoff breakpoints at maturity
r_observation_schedule    per-date observation calendar (incl. per-date coupon rate)
reference                 cross-references (filing_id, parent prospectus, etc.)
security_master           identifying fields (CUSIP, issuer, currency, maturity)
underliers                one entry per underlying asset (single-stock or basket)

Here is an excerpt of the response for a real autocallable note (US05617VCX10):

[
  {
    "isin": "US05617VCX10",
    "classification": {
      "instrument_id": 3937870,
      "legal_wrapper": "NOTE",
      "payoff_link_type": "EQUITY",
      "payoff_direction": "BULLISH",
      "payoff_profile": "AUTOCALLABLE",
      "tax_classification": "CPDI",
      "income_accrual_method": "CONSTANT_YIELD",
      "has_phantom_income": "TRUE"
    },
    "call_leg": {
      "instrument_id": 3937870,
      "call_exercise_style": "AUTOCALL",
      "call_payment_type": "PRINCIPAL_PLUS_COUPON",
      "first_call_date": "2026-08-21",
      "last_call_date": "2028-11-21"
    },
    "coupon_leg": {
      "instrument_id": 3937870,
      "coupon_rate_type": "FIXED",
      "coupon_conditionality": "CONTINGENT",
      "frequency": "QUARTERLY",
      "has_memory": "TRUE",
      "observation_scope": "WORST_OF",
      "barrier": { "barrier_type": "PCT_OF_INITIAL" },
      "payout": { "coupon_rate_pct": 7.35 },
      "coupon_terms": "7.350% per annum (1.8375% per quarter); paid if Closing Level of each Underlying Asset >= 70% of Initial Level..."
    },
    "fee_economics": {
      "estimated_value_pct": 96.424,
      "public_offering_price_pct": 100.0,
      "principal_amount_per_unit": 1000.0,
      "total_offering_amount": 5140000,
      "units_issued": 5140
    },
    "underliers": [
      { "underlier_id": "...", "weight": "...", "ticker": "..." },
      { "underlier_id": "...", "weight": "...", "ticker": "..." }
    ],
    "...": "remaining sections: mechanics, observation_leg, payoff_legs, r_observation_schedule, reference, security_master"
  }
]

coupon_leg.coupon_rate_pct is the first-period headline rate — derived from the earliest coupon row of the observation schedule. The per-date rates (step-up/step-down ladders carry one row per coupon period) are in r_observation_schedule[].coupon_rate_pct.

The full record for this ISIN is browsable at https://sqxnotes.com/api/reference/US05617VCX10.json — that's the same JSON shape the live API returns to a 200. Field-by-field documentation lives in the Data Dictionary.

The response is a JSON array. For most ISINs there's exactly one element; the array form is preserved because a single filing can cover multiple note tranches that share a parent prospectus.

Alongside the record we return two provenance fields:

Every record also carries withheld, the sections we hold back because a consistency check found their stored values wrong. It is [] for most notes. A withheld object or list section is null; a withheld field of coupon_leg is null; a withheld payoff curve is an empty payoff_legs with payoff_status: "failed_validation". Before we hold a section back we re-extract the note once, so a lookup can answer 202 while that runs. The values return once an extraction passes the nightly checks. The change feed lists the note when we hold a section back and again when we serve it, under that section.

The OpenAPI 3.1 spec (openapi.yaml) is the authoritative contract for all keys, enums, status codes, and the response envelope. This page is a narrative guide; the spec is the source of truth.

Reading the payoff curve

payoff_legs is the note's cash settlement at maturity, drawn as a piecewise-linear function of where the underlier ends up. It is the machine-readable form of the "Payment at Maturity" paragraph on the term sheet, and for most notes it is the only place the downside protection is expressed — so it is worth getting the reading rules exactly right.

Each leg is one breakpoint on that curve, not one segment:

"payoff_legs": [
    { "piece_id": 84177, "underlier_pct_of_initial": 0.000,   "payoff_pct_of_notional": 100.000 },
    { "piece_id": 84178, "underlier_pct_of_initial": 100.000, "payoff_pct_of_notional": 100.000 },
    { "piece_id": 84179, "underlier_pct_of_initial": 110.500, "payoff_pct_of_notional": 110.500 },
    { "piece_id": 84180, "underlier_pct_of_initial": 999.999, "payoff_pct_of_notional": 110.500 }
]

Units

Both axes are percentages, never levels or dollars. Neither axis carries a currency — settlement currency is on security_master.

The curve covers maturity only. Contingent coupons, memory coupons and autocall premia are not points on it; they live in coupon_leg, call_leg and r_observation_schedule. Where a note's terms fold a final premium into the maturity payment, that premium is inside the curve's payoff value because the term sheet states it that way.

piece_id is an internal row handle, not a curve index. It is reassigned whenever a note is re-extracted, and it does not order the curve — see below. If you need to key a leg, key it on the (underlier_pct_of_initial, payoff_pct_of_notional) pair, which is unique per note and is what the file feed uses.

Reading between two breakpoints

Sort the legs ascending on underlier_pct_of_initial. The response array is not sorted for you; the API emits legs in whatever order the store returns them.

Between two consecutive breakpoints the payoff is linear. For an underlier level x falling between (x1, y1) and (x2, y2):

payoff(x) = y1 + (y2 - y1) * (x - x1) / (x2 - x1)

Nothing else is implied. There is no curvature, no smoothing, and no optionality baked into a segment — if a note's real payoff between two levels is not a straight line, the extraction emits enough breakpoints to approximate it as one.

Worked example, using the curve above (US09711QTW32, a principal-protected note capped at 110.5%): at an underlier of 105% of initial, x sits between (100, 100) and (110.5, 110.5), so the payoff is 100 + (110.5 - 100) × (105 - 100) / (110.5 - 100) = 105.0 — a 1:1 participation. At an underlier of 50% the payoff is 100, because the segment from (0, 100) to (100, 100) is flat: this note returns principal in full no matter how far the underlier falls.

Cliffs

A payoff curve for a barrier or trigger note jumps. We express a jump as two breakpoints at effectively the same underlier level carrying different payoffs, in one of two shapes:

Treat both shapes as the same thing: a discontinuity at that level. Do not interpolate across the pair — an interpolation over a 0.001-wide segment will hand you an implied slope in the thousands.

Two examples, both real:

ISINlegsreading
US05615HZW14(0,0) (70,70) (70,100) (100,100) (200,324.25)70% barrier. At or above 70%, par. Below 70%, 1:1 with the underlier down to zero.
US09711KBA34(0,10) (90,100) (99.999,100) (100,135.75) (999.999,135.75)10% buffer to 90%, then flat par to 100%, then a jump to a fixed 135.75% cap for any gain at all — a digital upside.

Row order does not tell you which side of the jump is which. Both orderings occur in the data and neither is authoritative. Resolve it from the values instead: the payoff that continues the segment below the level is the limit approaching from below, and the payoff that continues the segment above it is the value at and above the level. Where the note's own threshold language reads "at or above L" — which is how essentially every barrier and trigger is drafted — the payoff exactly at L is the higher branch.

The far end of the curve

underlier_pct_of_initial = 999.999 is a sentinel meaning "and beyond". It is not a forecast that an underlier can go up 900%; it is the largest value the field can hold, used to anchor the right-hand end of the curve. It only ever appears on the underlier axis.

The payoff_pct_of_notional on that final leg is an ordinary payoff value, and the slope of the segment leading into it is what continues indefinitely:

If no leg carries 999.999, the curve is undefined above its last breakpoint. This is the case that most often catches people out. A curve that ends at (200, 250) is telling you the payoff at 200% of initial — it is not telling you the note is capped at 250%. It might be capped there, it might carry on at 150% participation, and the legs alone cannot distinguish the two. Read the last breakpoint as the end of what we assert, and fall back to classification.payoff_profile and the mechanics prose before assuming a cap.

Where the data does not yet follow these rules

The rules above describe the convention. The stored data does not uniformly follow it yet, and you should code defensively for three departures. Figures are as of August 2026, across the 23,307 notes that carry any legs at all.

what you may seenoteshow to handle it
final leg (999.999, <real payoff>) — the convention above~10,960as documented
no 999.999 leg at all; curve stops at 200% (or occasionally 100%)~9,340the tail is undefined — do not infer a cap
payoff_pct_of_notional = 999.999, with the underlier value back-solved, e.g. (699.999, 999.999)~1,530the payoff was clipped at the field's maximum, not capped at 999.999%. The final segment's slope is the real information: extend it. Being migrated to the convention above. Guard on 999.999 appearing in the payoff column once: on a few dozen curves it shows up at several breakpoints, and those are defective rather than clipped — treat their tail as unknown.
final leg (999.999, 999.999)~1,420treat as no information about the upside. It sits exactly on the 1:1 line, so it is indistinguishable between a genuine uncapped 1:1 note and an upside we failed to encode. Reading it as 1:1 participation will understate the upside on any note that actually pays more.

That last row is worth being blunt about, because the safe reading and the natural reading differ. US61780ECR62 ends (100, 100) (999.999, 999.999); its summary text says the note pays 111% of any gain. The curve says 1:1. The curve is the one that is wrong.

We are normalising all four shapes onto the first. Until that lands, a consumer who (a) sorts on the underlier, (b) refuses to interpolate across a sub-0.01-wide segment, (c) never reads a missing 999.999 leg as a cap, and (d) treats (999.999, 999.999) as unknown rather than as 1:1, gets a correct answer on every note in the set or a clean "unknown" — never a wrong number.


Keeping up with the final terms

When you first ask about a note that's only just been filed, the record you get may carry filing_status: "preliminary". Issuers later file a final pricing supplement with the locked terms.

You don't have to poll for that yourself. On any byIsin call, if we've ingested a newer filing for the ISIN than the one behind the record on file — the final supplement, or an amendment — we re-extract from it automatically and answer 202 with a poll handle (see "When we don't have it yet"), rather than handing you stale preliminary terms. Once the newer extraction lands, the same lookup returns 200 with filing_status: "final". In practice: an ISIN we'd previously answered 200 for can briefly return 202 again while the fresher terms are extracted — that's the system keeping you current, not an error.

Keeping up with changes to notes you have pulled

Terms move after you first pull them: an issuer restates a coupon, a final supplement replaces a preliminary supplement, an observation date shifts, a review corrects a field. Rather than re-pull your whole universe to find out, ask what changed:

GET /v1/reference/bulk/changes?since=2026-05-01

You get back, ISIN by ISIN, the parts of the record that changed in the window (sections, using the keys of the byIsin payload — coupon_leg, observation_schedule, underliers, payoff_legs, ...), the columns where the change was recorded at column level (fields, each with the reason for its latest change: new_source_document, method_fix, validation_failure, or review_correction), and whether the note was added, updated, or removed. Re-pull the ISINs listed with byIsin or bulk/byIsin; the rest of your mirror is current.

This call is free. It returns ISINs and field names, without values, so it is unmetered and leaves your distinct-ISIN limit untouched. Times are UTC; pages are ISIN-ordered, up to 1000 per page, and next_cursor continues.

Poll it with since set to the time of your last successful call. A row shows its latest change time only, so a window closed with until lists a note changed inside it only while nothing changed it again afterwards; until pins a page set to a moment rather than replaying history. Treat added as an upsert: a note removed and re-extracted inside the window comes back as added.


When the ISIN isn't a structured note

If you call /reference/byIsin with an ISIN we have on file as something other than a structured note — an ETF, an ADR, a corporate bond — we'll still answer. You get 200 with whatever we have in our security master:

{
    "isin": "US0010122028",
    "cusip": "001012202",
    "description": "AECI LTD",
    "type": "ADR",
    "issuer": "AECI Ltd",
    "message": "ISIN is type=ADR; only security_master fields are available for this type."
}

If we've never heard of the ISIN at all, you get 404.


Finding out what we have, before you spend anything

An ISIN we hold in the security master but have no offering document for comes back as a stub — reference_scope: "security_master_only", no terms, no underliers, no payoff curve. That answer is free. A sparse return costs nothing against your distinct-ISIN limit; you are never charged to discover a gap.

To ask ahead of time, POST a list to /reference/coverage:

curl -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
    -d '{"isin_list": ["US05613FTH72", "US83371HCZ08"]}' \
    https://api.sqx.com/v1/reference/coverage

It sorts them into covered (terms are extracted; a lookup returns them now), extractable (we have identified the offering document but haven't extracted it yet — a lookup returns 202 and the terms follow a few minutes later), and not_covered (we hold the security and no document). This call is free and unmetered too.

What we cover, by issuer

Structured notes in our security master, and the response to a request for a note.

Terms ready: we have extracted the payoff curve, underliers and coupon mechanics, and /reference/byIsin returns the terms immediately.

On request: we have identified the offering document and extract the document on request. A request for the ISIN starts the extraction and returns a poll URL; the terms arrive a few minutes later. Advance arrangement is unnecessary.

The remainder are securities without an offering document. They return reference_scope: security_master_only, and that response is free: it leaves your distinct-ISIN limit unchanged. To check a list before requesting the ISINs, POST the list to /v1/reference/coverage, also free.

IssuerNotes on fileTerms readyOn requestReachable
JPM80,3083,72549,34666.1%
GS38,5742,99827,15478.2%
MS40,3832,66729,74680.3%
Barclays29,4722,49820,44777.9%
Citi50,9882,22529,29461.8%
UBS39,6622,21134,84693.4%
BOFA23,9221,89116,40276.5%
BMO16,5141,30212,67184.6%
HSBC17,3337649,06056.7%
BNS13,1706413,60832.3%
RBC12,2176309,51283.0%
TD12,29062110,19388.0%
BNP51,31414315,27130.0%
SG10,008101071.2%
BBVA14,787220.0%
CS11,16906,57058.8%

Issuers with fewer than 5,000 notes are omitted. Coverage depends on offering documents: we extract terms from the offering document, so an issuer whose US notes are unregistered with the SEC shows low coverage here. A distributor feed is the only route to those terms, and work on our SEC pipeline leaves the figure unchanged.

Counts as of 2026-08-14.


When the PDF isn't what you thought

PDFs without a parseable ISIN are still accepted — our pipeline mints a synthetic identifier and runs the same extraction. If the document turns out to be an SN filing whose ISIN was on a page our OCR couldn't read, you'll still get a result. If it isn't an SN at all, the analysis pipeline detects that and skips the record cleanly; the polling response will eventually surface a 404 or a status indicating no record was produced.

PDFs containing active content — embedded JavaScript, launch actions, embedded files, XFA forms — are refused with 400 at the upload boundary. We don't try to strip the dangerous bits and continue; we reject the upload outright. Extracted text is also normalized and prompt-injection markers neutralized before any downstream LLM step.

The hard size cap is 10 MB per request. Above that you'll get a 413, returned as plain text by the AWS platform layer (this is the only response that isn't JSON).


When you get input wrong

Invalid input returns 400 with a structured error in the errors array, in the ErrorDetailV2 format used across the v1 API: a numeric messageCode, a category, a machine-readable type, and a human-readable message:

{
    "data": null,
    "errors": [
        {
            "category": "VALIDATION_ERROR",
            "type": "INVALID_INPUT",
            "messageCode": 1010,
            "message": "Invalid ISIN: checksum invalid",
            "messageValues": { "isin": "US06370EDU90" }
        }
    ],
    "meta": { "...": "..." }
}

Filter on messageCode. The codes are stable, and the message wording can change. Platform-wide codes:

messageCodeWhen
1001A required parameter is missing. messageValues.parameter identifies the parameter.
1010The ISIN is malformed or fails its check digit; also used for a filter that the route rejects.
1011CUSIP is malformed or fails its check digit.
1012A date is an invalid YYYY-MM-DD value, or a window's to is before its from.
1015The request refers to an item outside the route's offering (a field, a product, or a job kind), or requests intraday prices for a security other than a treasury. messageValues identifies the item.
1033The API key's grant has lapsed (403).
1040The identifier is well-formed, but the security or job is absent from our data (404).
1050The key is over a distinct-ISIN quota (lifetime or monthly), over its per-second rate, or a bulk request exceeded its cap (429).
1060A database failure on our side, or an asynchronous job that failed or was unable to start (500 / 503).

The API converts lowercase identifiers to uppercase, so identifiers pasted from a spreadsheet work as entered.

The table above is platform-wide; no endpoint in this family takes a CUSIP or a date, so 1011 and 1012 never appear here.

This endpoint family adds two of its own input surfaces, in the same envelope. PDF uploads reject an empty body, anything without a %PDF- header, and files carrying active-content markers such as /JavaScript. The status URL rejects a missing job_id, one that is neither a numeric filing id nor a 64-character SHA, and one we hold no job for — that last is a 1040, alongside a next field telling you how to submit the document.

One caveat on those two surfaces: their 400s currently come back with messageCode 1090 — the platform's generic catch-all, with category: "OTHER" — rather than a dedicated validation code. Until they migrate, treat a 1090 on an upload or a status poll as bad input and read the message text for the specifics; it's the only discriminator these responses carry.

All API responses are JSON, with a single platform-level exception: the 413 for oversize bodies, which is plain text from the AWS gateway.


Endpoint reference

MethodPathPurpose
GET/reference/byIsin?isin={ISIN}Look up by ISIN. 200 if analyzed (with filing_status), 202 if known but pending or a newer filing supersedes the record on file, 404 if unknown.
POST/reference/byPdfUpload a PDF. 200 if its ISIN is already analyzed, 202 with a job_id otherwise, 400 on bad input.
GET/reference/jobs/{job_id}Poll a 202 from either of the above, with the same API key. 200 with payload when ready, 202 with status while pending; 404 to any other key.
POST/reference/coverageAsk which of a list of ISINs we hold terms for. Free — never metered, never counted against your ISIN limit.
GET/reference/bulk/changes?since={date}Which notes' terms changed since a date, and which sections and fields. Free — never metered. Paged by cursor.
GET/versionReport API version. No auth required.

Authentication & versioning

All requests except /version require a valid API key in the x-api-key header. Keys are scoped per client. A key can also have an expiry, a lifetime distinct-ISIN quota (a cap on the total distinct ISINs the key can retrieve), a monthly distinct-ISIN cap per product or across a chosen set of products, and a per-second request rate. A key whose grant has lapsed receives 403 (messageCode 1033). A key over a quota, a monthly cap, or its rate receives 429 (messageCode 1050, with the limit and window in messageValues).

The v1 base path is stable. Field additions are non-breaking and ship in place. Breaking changes (renames, removals, and changes of meaning) ship as a new major version on a new stage. /version reports minor and patch updates.

On this endpoint family, the scope also selects which reference fields a key is served — two keys can get different column sets back for the same ISIN.

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

Contact us to learn more