Call the API
Numbers checked against the live data on
You read factors with a plain GET under /v2/, sending an API key as Authorization: Bearer ocai_live_.... This page shows the calls you use most: list with filters, page with a cursor, read one factor, read its decomposition, list the libraries, and poll for a change without downloading everything again.
Two APIs, and where each one is written down#
There are two HTTP surfaces on the same host, and they answer different questions. Reading factor search is not the same thing as matching your own lines against it.
| prefix | what it does | credential | its reference |
|---|---|---|---|
/v2/factors, /v2/libraries, /v2/currencies |
Browse and read factor search | A key, Authorization: Bearer ocai_live_... |
the Factor API group: factors and libraries |
/v2/uploads, /v2/mapping-jobs |
Upload your own lines and run a mapping job | A key, Authorization: Bearer ocai_live_... |
the Mapping API group: uploads, mapping jobs and results |
HTTP is not the only way in. The same data is also readable as SQL against the open_ef schema, which is a connection you arrange rather than a hosted endpoint to point a client at (Connect to factor search over SQL).
Both are in one generated API reference, field by field, with a console beside each operation that sends a real request from your browser. Both halves take a key, and the console offers a field for it on every operation: paste the key in, pick a server, press send, read the response.
This page is the narrative version of the same contract: what each call is for, and which mistakes it lets you make.
Note.
/v2/is the contract: inside it we add, we do not remove, so a client that ignores a field it does not recognise keeps working. The website explorer reads the same factors under/factors/api/, which is its own and not part of that contract: point your code at/v2/.
Both surfaces list two servers: https://api.open-climate.ai (production) and https://dev.open-climate.ai (sandbox, same contract, separate data). The examples below use the sandbox.
What a credential changes#
Every read needs a credential: an API key or an OAuth token, in Authorization: Bearer .... A read with neither is a 401 with code unauthenticated and a WWW-Authenticate: Bearer realm="ocai" header, and so is one whose credential does not resolve. The factor explorer on the website reads the same factors through its own routes, on a short-lived pass it gets for the browser that opened it, held to the pace of one person browsing; that pass is not a way in for a program, and the terms forbid using it from one.
It changes two things, and they move independently. The detail's depth is not one of them: it is full for every reader.
| what you send | metered | detail depth |
licence visibility |
|---|---|---|---|
| nothing | refused, 401 |
||
an OAuth token, or a user key ocai_live_... issued to one account |
never | full |
that account's entitled tiers |
a partner key plus an X-OCAI-Subject header |
never | full |
anonymous, on purpose |
The search meter counts only the website explorer's searches, and any credential lifts it. Licence visibility follows that reader's own entitlement instead, so a partner key acting for a subject is never metered while still being served the anonymous licence view. Both key kinds begin ocai_live_, so the token string does not say which one you hold. A user key acts for the one account it was issued to; a partner key acts for the customer its X-OCAI-Subject header names, and without that header it is a 401. Holding the publisher's licence is what widens a restricted library, not the act of signing in.
Read what you were served rather than inferring it from what you sent: value_withheld on a row and on each cell of a detail's matrix, and value_withheld_reason beside it, null exactly when the value is served. licence means your entitlement does not reach the library's tier, so a credential that holds the publisher's licence would be served the number. pending means the value is withheld from every caller, signed in, entitled or not, so a credential changes nothing and there is nothing to retry (a value withheld from every caller). A key is issued on request and serves both surfaces: the API reference has the contact and the details of partner and single-account keys.
A list or detail response is private, max-age=0, must-revalidate and carries an ETag, so your client revalidates it on every read and an unchanged answer is a 304. The list, the detail, the decomposition and the library list lose their ETag only when your entitlement widens them (what a credential changes on the library list), and a saved=true page never carries one. See Cache and detect a change.
List factors with filters#
curl -s -H "Authorization: Bearer $OCAI_API_KEY" "https://dev.open-climate.ai/v2/factors?library=ademe®ion_code=EUROPE&unit_code=kwh&system_boundary=well_to_use&limit=2"
Each filter is a query parameter. Repeat a parameter to OR its values (library=ademe&library=desnz); different parameters AND together. The filters are:
library, category, sub_category, region_code, unit_code, unit_family, boundary_family, system_boundary, boundary_preset, gwp_method_normalized, calculation_approach, grid_mix, license_tier, data_quality_normalized, supplier_name, has_supplier, is_epd, is_derived, biogenic_in_headline, year.
One more parameter filters the list without naming anything in factor search: saved keeps the signed-in reader's own saved factors, and it needs a credential (filter by your saved factors).
currency is on the list too and filters nothing at all: it restates the monetary rows into the money you asked for (read the money in your currency).
The response is a page:
| field | meaning |
|---|---|
kind |
factors for a page of results. Branch on it (see the search meter). |
rows |
Slim rows: identity fields plus the columns you asked for. |
columns |
The columns the rows carry. Pick your own with repeated columns= parameters. |
total |
How many factors match. |
values_total |
How many values sit behind those factors, a withheld one included: one factor can carry a year series, or a location and a market figure. |
facets |
Per filter, each option's count. Name the ones you want with facets=. |
category_breakdown |
The whole result split by category, largest first, with the leading sub-categories under each. Its counts add up to total. |
next_cursor |
The cursor for the next page, or null on the last one. |
total, values_total, facets and category_breakdown are the counting half of the page, and you can ask for them apart from the rows (split the rows from the counts).
Add q for a semantic search instead of a pure filter. A searched response groups matches under their top-level factor in entries, carries the rows of the matched parts in nodes, and serves facets as null.
Which form a filter value takes#
Most filters take factor search's own code: region_code=EUROPE, unit_code=kwh, system_boundary=well_to_use, license_tier=open, gwp_method_normalized=ar6 (lowercase, always).
A filter that matches one column exactly takes that column's name, so the value you send is the value a row carries under the same name. Three filters are not a plain match on one column, and keep a name of their own: year keeps the factors whose window, applies_from_year to applies_to_year, covers the year; boundary_preset expands into the system_boundary codes of a named coverage, such as energy_full_chain; biogenic_in_headline removes the combustion factors whose headline counts biogenic CO2.
Three do not:
| filter | the value to send |
|---|---|
category |
the English category label, such as Fuels |
sub_category |
the English sub-category label, such as Solid fossil fuels |
supplier_name |
a supplier's name, spelled as a row's supplier_name carries it, not a code |
So category=fuels is not an error. It matches nothing, and you get an empty page with total 0. The code lists enumerate codes, which is the wrong column for these two: take the value from a row's category field or from a facets=category block. In SQL the code is a column of its own, category_code beside category, and that is the one a query keys on (Fit judgement). Those labels stay English whatever lang you send, like every other filter value and cursor.
A supplier's name has no count block to pick it from (the facets that answer nothing): ask for the supplier_name column and read it off a row.
has_supplier, is_epd, is_derived and biogenic_in_headline take a single value, the string true or false; omit one to get both sides. A flag travels as text everywhere on the list route: the facet counts list the same two strings, true and false, as their options. is_derived=true keeps only the totals open-climate.ai computed by adding up parts the publisher printed separately, and false only the publisher's own factors. Like every filter it is matched on the parts, so under false a computed total still heads its entry when one of its parts passes. biogenic_in_headline states your accounting basis for the biogenic CO2 a burnt biofuel releases: false, the GHG Protocol convention of reporting it outside the scopes, removes the combustion factors whose headline includes it, and true removes nothing. year takes an integer. Those five and license_tier are checked against their sets, so a value outside them is a 400 with code invalid_request. Every other filter is free text, and a value nothing carries simply returns no rows.
Choose the columns you get back#
Omit columns and every row carries the same seven: library, activity_full, region_code, unit_code, system_boundary, value_co2e_normalized, reference_year. That is the API's default, not factor search's: the table you see at /factors/ asks for a wider set of its own.
Thirteen fields ride on every row whatever you asked for: slug, library, license_tier, value_withheld and value_withheld_reason; io_table, null outside an input-output library that prints both a product and an industry table; child_count, value_count and has_description; the provenance marks is_estimated, co2e_origin and grid_mix_origin, the last null where a factor has no grid mix; and biogenic_in_headline, true when the headline value counts the biogenic CO2 a burnt fuel releases.
columns= takes 33 ids, repeated one per parameter, and the response echoes the effective set back in columns, so a single call tells you what you are getting. A column you did not select is absent from the row, not null: absent means you did not ask, null means the factor has no value there. Ask for columns=activity_full&columns=applies_to_year and a factor open at the end carries "applies_to_year": null, and no unit_code key at all. What the columns mean reads each machine name; they are the same names an export file uses.
An id the registry does not know is a 400, not an ignored parameter:
{"error": {"code": "invalid_request", "message": "Unknown list columns: license_tier.", "status": 400, "request_id": "..."}}
That is the usual trap, because the response model shows more fields than you may request. The ones to leave out: the twelve of those thirteen that are not columns, since you are given them anyway (slug, license_tier, value_withheld, value_withheld_reason, io_table, child_count, value_count, has_description, is_estimated, co2e_origin, grid_mix_origin, biogenic_in_headline), anything ending in _label (a label rides along with the field it names), activity_full_stored (it rides with activity_full), lifecycle_coverage, boundary_preset, boundary_role, boundary_scope and boundary_scope_source (they ride with system_boundary), gwp_horizon_years and gwp_normalization_basis (they ride with gwp_method_normalized), original_unit_code and fx_ratio (they come with currency), and the detail-only fields such as source_key, classifications and the gas breakdown, which a list row never carries.
The decomposition route takes the same parameter with a default of its own: activity_full, unit_code, system_boundary, parent_relation, value_co2e_normalized.
value_co2e_normalized is the number on the normalized basis: restated to AR6 GWP100 where the restatement is exact, the publisher's own number otherwise, with gwp_method_normalized naming the method it is on. The publisher's own number, on the publisher's method, is value_co2e_native on the detail's value passport.
Count only the facets you draw#
facets= names the count blocks you want back; leave it out and you get every block the response can serve. It never changes the rows, the total or the meter, so ask for the ones you actually render.
Fourteen filters have a count block: library, category, sub_category, region_code, unit_code, unit_family, system_boundary, calculation_approach, grid_mix, license_tier, data_quality_normalized, has_supplier, is_epd, year. A boolean block's options are the strings true and false.
The other six, boundary_family, boundary_preset, gwp_method_normalized, supplier_name, is_derived and biogenic_in_headline, are accepted and answer nothing. There is no supplier_name block on purpose: it would list every declarant, thousands of options, so a supplier is found with q or read off a row. The server expands a boundary_preset into its system_boundary codes before it counts anything, so ask for system_boundary to count them. Only an id that names no filter at all is a 400, code invalid_request, message Unknown facets: ....
Watch out.
facetsisnullfor the whole response, not one block short, whenever the counts cannot be served exactly: withq, with any filter carrying more than one value, and with aboundary_family,gwp_method_normalizedorboundary_presetfilter. Rows andtotalare still exact. It is alsonullundercounts=none, along withtotal, because you asked for no counts. Readfacetsbefore you iterate it.
Split the rows from the counts#
A page is two reads on the server: the rows, and the numbers beside them (total, values_total, facets and category_breakdown). One request runs both at once and answers when the slower lands, which on a filter nobody has asked for yet is the counting. counts takes them apart:
counts |
what comes back |
|---|---|
full, the default |
the rows and the four numbers |
none |
the rows and next_cursor, with the four numbers null |
only |
the four numbers, with no rows and no next_cursor |
Send a none and an only request side by side and a table can paint its rows without waiting for the counts. On the explorer's signed-out pass the pair spends one search on the meter, not two, because counts is not part of what the meter counts (the explorer's meter). With q it is ignored: a searched response counts its own ranking, so there is nothing to split.
Filter by your saved factors#
saved=true keeps only the factors the signed-in reader saved. It narrows like any other filter, so the rows, the total, the breakdowns and the facet counts are all of the saved slice: a total under saved=true is not a factor search number. It combines with q and with every filter, and it has no count block of its own.
It is the one parameter that names the caller rather than factor search, so it needs a signed-in person behind the call. On the explorer's signed-out pass it is a 401 with code unauthenticated and the message Sign in to filter by your saved factors., raised before the meter and before the cache: it never spends a search and never answers the sign-in wall. A partner key acting for a subject is a 403 with code forbidden, because a saved set belongs to a person's own account.
Saving and unsaving is a portal action today. There is no published route for it, so saved is for a client acting on behalf of a reader who saved factors in factor search.
Pick the year#
Without year, each factor answers with its latest value. With year, you get the value whose year window covers that year:
curl -s -H "Authorization: Bearer $OCAI_API_KEY" "https://dev.open-climate.ai/v2/factors?q=butane&library=ademe®ion_code=EUROPE&unit_code=kwh&year=2018&limit=1&columns=value_co2e_normalized&columns=applies_to_year"
The first row is ef-ademe-butane_maritime_included-europe-kwh-well_to_use-53be0cf1 with value_co2e_normalized 0.26748 and applies_to_year 2018. Drop year and the same factor answers 0.269, its value from 2019 on. The SQL quickstart walks the same factor.
Read the money in your currency#
A spend factor is quoted in the money its publisher priced it in. Add currency and every monetary row comes back restated into yours, at the annual average rate for that row's own activity year.
curl -s -H "Authorization: Bearer $OCAI_API_KEY" "https://dev.open-climate.ai/v2/factors?library=exiobase&unit_family=monetary¤cy=gbp&limit=2"
The code is lowercase ISO 4217, spelled the way unit_code spells a monetary denominator on a row. It works the same way on one factor, where the year-by-methodology matrix converts cell by cell, each at its own year.
It is not a filter. The rows, the total, the breakdowns, the facet counts and the order are identical with it and without it, so a client can offer the choice without re-running anything. Leave it off and every number is what the route served before currencies existed.
A restated row says so in two fields, whichever columns you asked for:
| field | meaning |
|---|---|
original_unit_code |
The currency the publisher quoted, once the value has been restated. null on a row that was not. |
fx_ratio |
What the value was multiplied by. Divide the served value by it to recover the published one. |
The ratio is in the publisher's currency per yours, the inverse of the exchange rate you would convert an amount of money at. A factor is per unit of money, so a euro factor read in dollars at the 2025 rate is multiplied by about 0.885 (euro per dollar), not by 1.13.
Three rows come back untouched, with null in both fields: a physical factor, one already quoted in the currency you asked for, and a monetary one whose activity year the rates do not reach.
curl -s -H "Authorization: Bearer $OCAI_API_KEY" "https://dev.open-climate.ai/v2/currencies"
currencies lists every code the routes accept, each with first_year and last_year, the window its rates cover. Build a picker from this route rather than from a list compiled into your client: the rates are seeded, so a hard-coded list eventually offers a code nobody can serve. The route names no caller, is not metered, and caches like the library list (cache and detect a change).
A code outside that list is a 400 with code unsupported_currency, never a page of rows quietly left in the publisher's money.
Page through results#
Pages use an opaque cursor, not offsets.
- Send your request with
limit(1 to 200, default 50). - Read
next_cursorfrom the response. - Send the same request again with
cursor=<next_cursor>. - Stop when
next_cursorisnull.
curl -s -H "Authorization: Bearer $OCAI_API_KEY" "https://dev.open-climate.ai/v2/factors?library=ademe&unit_code=kwh&limit=200&cursor=<next_cursor>"
Keep every other parameter identical while you page. A cursor from a searched request is only valid under the same q, filters, year and sort. sort takes relevance, category, value or year, and the last three take a leading - for descending. There is no -relevance.
Read one factor#
curl -s -H "Authorization: Bearer $OCAI_API_KEY" "https://dev.open-climate.ai/v2/factors/ef-ademe-butane_maritime_included-europe-kwh-well_to_use-53be0cf1"
The detail carries everything the list leaves out: source_key, value_key, classifications, parent_slug, child_slugs, the GHG breakdown, license_code, attribution_text, license_url and a year-by-methodology matrix. The matrix gives its shape and every cell to every reader, so depth is always full. A value the licence withholds from you keeps its cell, with value_withheld set, value_withheld_reason at licence and no value (what a credential changes). A value withheld from every caller keeps its cell the same way, with value_withheld_reason at pending.
A factor served thinner than its publisher prints it also carries withheld_for_quality: the matrix positions set aside on quality grounds and why, in counts, so a gap in the matrix reads as deliberate rather than missing. On nearly every factor it is null (what a factor withholds).
Add lang=fr for French labels where they exist. Codes, filter values and cursors stay English. Add currency to restate the default row and every cell of the matrix (read the money in your currency).
Follow a moved slug#
source_key is the published value's identity, an efv1- key such as efv1-6a7b8c9d0e1f20314253647586970a1b, hashed from the publisher, the release and the publisher record's own identity. It is not the publisher's identifier, and a new release gets a new one. value_key names the value the headline serves: the source_key on a physical factor, one key per activity year on a monetary one; every matrix cell carries its own. The v parameter (?v=<value_key>) is reserved for reading one value by that key: the route does not serve it yet and ignores it. Wherever a payload names the release a value comes from, the field is release_version; version appears only in a library's releases[].
A slug is stable. When a factor is merged or re-keyed, its old slug answers 301 with the canonical URL in Location. Follow the redirect and update the slug you stored. Location keeps your query string, so lang and currency carry over.
curl -sL -H "Authorization: Bearer $OCAI_API_KEY" "https://dev.open-climate.ai/v2/factors/<slug>"
A slug that never existed answers 404:
{"error": {"code": "factor_not_found", "message": "No factor is published as ef-ademe-nope-12345678.", "status": 404, "request_id": "..."}}
Read a decomposition in one call#
Try GET /v2/factors/{slug}/decomposition
curl -s -H "Authorization: Bearer $OCAI_API_KEY" "https://dev.open-climate.ai/v2/factors/ef-ademe-butane_maritime_included-europe-kwh-well_to_use-53be0cf1/decomposition"
root is the slug you asked for, rows holds the root and every component under it, and children maps each parent slug to its child slugs. An old slug answers 301 to the decomposition under the canonical slug, query string kept, like the detail. See Parents, children, double counting before you add rows up.
List the libraries#
One more route answers about the libraries rather than the factors. It is not metered, and like every route here it needs a credential, which it also reads to decide one count it serves (what a credential changes on it).
curl -s -H "Authorization: Bearer $OCAI_API_KEY" "https://dev.open-climate.ai/v2/libraries"
One row per library: publisher, its releases (releases[], the one served now with status served), licence tier, the attribution line to reproduce, how many factors are served and how many published values sit behind them, and the reference-year range. Read year_semantics before you compare years across libraries, because publishers do not all mean the same thing by a factor's year (Reference year). Add lang=fr for French labels.
It is also the one list a mapping job reads its library codes from. A library a job can search carries job_search: name its code in a job's libraries to restrict the search to it, is_default_selected says whether a library picker should start with it ticked, and quantification_bases says which lines it can answer, so a library listing monetary alone cannot price a line in kilograms. job_search is null on a library you can read here but no job searches yet. Build a job's library picker from these rows rather than from a list compiled into your client: libraries are added, and one in the sandbox before production is normal.
The factor explorer shows more about each library than this route: its coverage by category and by boundary, its open data section with the licence answers and audit checks, and what it withholds. The explorer reads those from routes that are internal: served, but left out of the API reference because their format may change without notice. Do not build on them. What a client needs from them already travels on the published routes:
- Coverage. Filter the list on
libraryand read itsfacets: each option's count, per filter (split the rows from the counts). - Licence. Each row here carries
license_tier,license_code,license_url,license_noticeandattribution_text. Licences answers, per licence, what you may do with the data. - What is withheld. A factor served thinner than its publisher prints it says so on its own detail (what a factor withholds).
What a factor withholds#
What a publisher prints that factor search sets aside on quality grounds is stated on each factor it touches, in the detail's withheld_for_quality. It answers in counts, never with a withheld number, whatever you send: read that figure from the publisher, where its caveats travel with it. Without it, a gap in a factor's matrix looks the same whether it is missing data or a deliberate omission.
values_withheld counts the published numbers set aside. positions holds one entry per reason and matrix position, latest year first, on the matrix's own axes: reference_year, calculation_approach, grid_mix, radiative_forcing and gwp_method_normalized. Line one up against the served matrix to see which column is missing. On a factor with nothing withheld, which is nearly all of them, the field is null.
Key your own handling on each position's reason code, such as biogenic_excluded_zero, which is stable. label and explanation are written for a reader, in the language lang asks for where we have written one, English otherwise.
What a credential changes on the list#
values_served is 0 on a restricted library's tile anonymously, because the values are withheld. A reader whose entitlement widens that tier is served the real count on those tiles. Nothing else on the tile moves: values_total, factors_served, the regions, the years, the licence fields and job_search are the same for every caller. The two value fields answer different questions: values_total is what the library covers, values_served is how much of it you may read. Quote values_total when you are describing the library and values_served only when you are describing your own access. Both count numbers only: values_pending counts the values a library lists that are withheld from every caller, 0 on every library that has none (a value withheld from every caller).
What also moves is the caching. A widened response is private, max-age=0, must-revalidate with Vary: Authorization, Cookie and no ETag, so there is no 304 to be had and a shared cache must not keep it. Only an OAuth token or a user key ocai_live_... that holds the grant widens anything: a partner key acting for a subject, and a signed-in reader with no grant, are served the anonymous bytes under the public validator (what a credential changes).
Cache and detect a change#
All five routes answer a key with a strong ETag. Three also say how long your client may keep the response: a decomposition, the library list and the currency list. The list and one factor's detail say max-age=0, so your client revalidates those two on every read. Send the ETag back as If-None-Match and an unchanged answer is a 304 with no body:
curl -si -H "Authorization: Bearer $OCAI_API_KEY" "https://dev.open-climate.ai/v2/libraries" | grep -i '^etag'
curl -si -H "Authorization: Bearer $OCAI_API_KEY" -H 'If-None-Match: "<etag>"' "https://dev.open-climate.ai/v2/libraries" | head -1
| route | Cache-Control |
ETag |
|---|---|---|
| the list and one factor's detail | private, max-age=0, must-revalidate |
yes |
| a decomposition, the library list and the currency list | private, max-age=300, stale-while-revalidate=600 |
yes |
the list, a detail, a decomposition or the library list that your entitlement widens (the rule is below), and a saved=true page of the list |
private, max-age=0, must-revalidate |
none |
They are private because every read needs a credential: a shared cache that kept one would hand it to a caller holding none. Your own client may keep a response for its max-age and revalidate it with the ETag. The list, the detail and every response without a validator carry Vary: Authorization, Cookie; the others carry Vary: Authorization.
What the validator tells you#
The ETag is two things joined: factor search's ingest stamp, which moves when factors are ingested, and a hash of the exact request you made together with the licence view it was served under and the copy of the data set that answered it, which also moves when we republish the data set. The credential you send is not part of it: a user key, an OAuth token and a partner key served the same licence view get the same ETag for the same request. That gives you a change detector for free. A schema change on its own need not move either part: a renamed or removed column can land without touching the ETag, and the notice you get is the whole of the warning.
- The same request URL, a different
ETag. Something in the data moved. Re-read that page. - The same request URL, the same
ETag. Nothing you asked for has changed, and the304costs you the round trip only. - Two different requests. Their
ETags always differ. Comparing them says nothing at all.
So poll by storing one ETag per request URL you care about, re-sending it on a schedule, and acting only on a 200. Every validator starts with the same ingest stamp, so the library list's ETag moving is also the signal to re-read a page you hold no ETag for. A change tells you the data moved, not which rows: compare source_key per slug to find that out (Trace a factor to its source).
Watch out. The generated reference declares the
If-None-Matchrequest header, theETagresponse header and the304response on all five operations, so a client generated from it models them. It cannot tell which reads your entitlement widens, and those come back with noETag: handle its absence.
A grant costs you the validator on the four routes it widens, the list, the detail, the decomposition and the library list (what a credential changes on the library list), so a mirror is better off polling with a key that holds none. The currency list keeps its validator whatever you send. A saved=true page of the list never carries one: your saves can change while the data set does not.
Limits#
- Rate limit. 120 requests per minute per IP across the factor routes,
/v2/and the explorer's/factors/api/counted together. Past it you get429with coderate_limitedand aRetry-Afterheader. - The explorer's pass. Held to one person's browsing: 60 reads a minute per pass, 5,000 a day per network address, and 60 new passes an hour per address. A key is not held to these.
- Search meter. A key is never metered. The meter counts the list calls the website explorer makes for a signed-out reader: ten distinct searches, where a search is a new combination of
q, filters and libraries, and past ten the list answers200withkind: "sign_in_required"and no rows. The detail, decomposition, library and currency routes are never metered.
The explorer's meter#
The meter is the website explorer's own protocol, carried in an X-OCAI-Search-Meter header on its signed-out pass. A key lifts it, so a call with a key never receives a meter token and never meets the wall: kind is always factors for you. The reference still declares both shapes, so branch on kind before you touch rows.
Related#
- API reference: the generated contract for both surfaces, with a console that sends a real request to the sandbox or production from your browser.
- Quickstart: SQL
- Integrate our MCP server into yours
- Identifiers and stability