Guide
Getting started
Everything the app shows is open to your own systems: parcels, tax bills and their checks, payment and appeal deadlines, documents and reports. This guide explains how to connect; the reference lists every query, mutation and type.
Overview#
The API is GraphQL over HTTPS at one endpoint. Every request is a POST with a JSON body and an API key in the Authorization header, and every answer is JSON.
POST https://api.retrieveiq.net/graphqlEach request sees your firm's own data and nothing else. retrieve-IQ tracks and checks bills; it never pays them, and nothing in the API moves money.
Bills are fetched from the counties retrieve-IQ covers, which the app's Coverage screen and the coverage query list by state, on a schedule retrieve-IQ sets for each firm: every parcel is read again at least weekly. California is the first state open. A parcel in a county not yet covered is tracked with its deadlines, and its bills arrive once the county is.
Call the API from a server or a script. A browser may call it only from the retrieve-IQ app's own origin, so a web page of your own cannot reach it directly.
- Common tasks: loading parcels, reading bills, reviewing them, deadlines and appeals, reports.
- Keeping in sync: a nightly read of what changed, and the file feed.
- AI assistants: the same data over the Model Context Protocol.
- Changes: what the API promises about change, and what has changed.
API keys#
A person at your firm creates a key in the app under Settings, API keys, New key: a name that says what it is for, the scopes it may use, and optionally the last day it works.
- The key starts with
riq_and is shown once. Store it where your system will read it; only a hash is kept, and it cannot be shown again. - A key acts as your firm, limited to the scopes you gave it. It can never hold
ADMINand cannot create other keys. - Revoke a key on the same screen. A revoked or expired key is refused at once, with a message that says which; a calendar feed it created stops answering, and a report it requested that has not run yet fails.
- Send it as a bearer token:
Authorization: Bearer riq_...
Your first request#
Ask who the key acts as and how many parcels it can see (the key needs PARCEL_READ):
curl https://api.retrieveiq.net/graphql \
-H "Authorization: Bearer $RETRIEVEIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query":"{ viewer { name tenant { name } } parcels(first: 5) { totalCount edges { node { id parcelNumber } } } }"}'const response = await fetch('https://api.retrieveiq.net/graphql', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.RETRIEVEIQ_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: "{ viewer { name tenant { name } } parcels(first: 5) { totalCount edges { node { id parcelNumber } } } }" }),
});
const { data, errors } = await response.json();import json
import os
import urllib.request
request = urllib.request.Request(
"https://api.retrieveiq.net/graphql",
data=json.dumps({"query": "{ viewer { name tenant { name } } parcels(first: 5) { totalCount edges { node { id parcelNumber } } } }"}).encode(),
headers={
"Authorization": f"Bearer {os.environ['RETRIEVEIQ_API_KEY']}",
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(request) as response:
payload = json.load(response) # payload["data"], and payload.get("errors")The answer is a JSON object with data, and an errors list when something was refused (see Errors). Any GraphQL client works; there is nothing to install. Every response carries an x-request-id header; quote it when you write to support about a request.
{
"data": {
"viewer": { "name": "Nightly loader", "tenant": { "name": "Example Tax Advisors" } },
"parcels": { "totalCount": 0, "edges": [] }
}
}A firm's own portfolio is usually empty, as here: its parcels belong to its clients. The next section names one.
Acting for a client#
A tax firm holds no property of its own; it works for clients under an engagement. List the clients your key may act for, then name one on each request with the X-Engagement-ID header (this request needs REPORT_READ):
{ myEngagements { engagementId live client { name } scopes } }curl https://api.retrieveiq.net/graphql \
-H "Authorization: Bearer $RETRIEVEIQ_API_KEY" \
-H "X-Engagement-ID: $ENGAGEMENT_ID" \
-H "Content-Type: application/json" \
-d '{"query":"{ portfolioSummary { parcelCount } }"}'- Without the header, a firm's key sees the firm's own portfolio, which for most firms is empty.
- Under an engagement the key may do what both the key's scopes and the engagement's scopes allow.
- An engagement that has ended is refused with
FORBIDDEN, never answered with an empty list, so a lapsed client cannot be mistaken for one with no data. - Never send
X-Tenant-ID,X-Actor-Tenant-ID,X-User-IDorX-Scopes. Who you are is derived from the key, and a request carrying any of them is refused with a 400.
Scopes#
A key holds one or more of 6 data scopes, and a person signed in to the app holds all of them.
| Scope | Allows |
|---|---|
APPEAL_WRITE | Record appeals and update them. |
BILL_READ | Read tax bills, the checks run against them, the documents behind them and the nightly fetch status. |
BILL_REVIEW | Accept, flag and reject bills, resolve their checks, and take a decision back. |
PARCEL_READ | Read parcels, their assessments, the units that assess and bill them, deadlines and calendar feeds. |
PARCEL_WRITE | Add and import parcels. |
REPORT_READ | Request and download reports, and read the portfolio summary. |
Every entry in the reference says which scope it needs, and the types say which scope reading them needs wherever they are reached from: portfolioSummary needs REPORT_READ, and a parcel's bills field needs BILL_READ even though the parcel itself needs PARCEL_READ.
A write scope does not include the matching read scope, so each mutation's entry names both: a review needs BILL_REVIEW and BILL_READ, an appeal APPEAL_WRITE and PARCEL_READ. A key holding only the write scope is refused when it reads the result, and because any error undoes the whole request, nothing is written. Reading the inbox and the fetch status (inboxSummary, billInbox, ingestionStatus) needs PARCEL_READ beside BILL_READ. Give a key the least it needs; a nightly export needs only the read scopes.
Conventions#
- Ids are opaque strings. Keep them as they are given; never build or parse one.
Dateis a calendar date,YYYY-MM-DD, with no time and no timezone.DateTimeis an instant in ISO 8601, UTC.Moneyis a decimal string in dollars with two places,"71486.32", so no precision is lost on the way. A figure the county does not publish isnull, never0.- A record you may not see and a record that does not exist look the same:
null, or absence from a list. The API does not say which. - Property tax has two years.
assessmentYearis the year a value was set, which an appeal contests;payableYearis the year the bill falls due. Which comes first depends on the county. - The schema is open to introspection with your key, and the SDL is at /docs/schema.graphql for a client that generates code from it.
Paging#
Long lists (parcels, billInbox) are connections. Ask for a page with first (25 when omitted, at most 100), then pass the last endCursor as after while hasNextPage is true. Lists page forward only: last and before are accepted and ignored. A cursor belongs to the filter it was cut under; a cursor presented with another filter is refused with INVALID_CURSOR.
query ($after: String) {
parcels(first: 100, after: $after) {
totalCount
pageInfo { hasNextPage endCursor }
edges { node { id parcelNumber situsAddress { line1 city state } } }
}
}Documents and reports#
Files are not inside GraphQL responses. A Document carries a contentPath and a finished ReportJob a downloadUrl. Fetch either with a plain GET, the same Authorization header and, for a client's files, the same X-Engagement-ID:
curl -OJ https://api.retrieveiq.net/documents/<id>/content \
-H "Authorization: Bearer $RETRIEVEIQ_API_KEY"Where a header cannot be sent (a new browser tab, an embedded viewer), createDocumentLink gives a path that opens one document without it, for five minutes, checked again when it is opened. A document goes the other way through an upload link; Common tasks shows both steps.
Reports are made in the background: requestReport queues one, and reportJobs shows when it is DONE and where to fetch it. Both routes answer with the file and a Content-Disposition header naming it, or with a small JSON body and one of these statuses:
| Status | Body | Meaning |
|---|---|---|
| 401 | {"reason": "unauthenticated"} | No valid key. The document route writes the reason in capitals. |
| 403 | {"reason": "NOT_ENTITLED"} | The key lacks BILL_READ (documents) or REPORT_READ (reports), or the engagement does not allow it. |
| 404 | {"reason": "not_found"} | No such file, or one you may not read. The two are not told apart. |
| 404 | {"reason": "bytes_unavailable"} | The record exists but the bytes are not held. For a document, sourceUrl names the county page the bill was read from. |
| 410 | {"reason": "file_removed"} | A document removed by the retention rule, seven years after it was fetched or uploaded; the message says when. What was read from it stays. |
| 503 | {"reason": "documents_unavailable"} | The document is held but cannot be opened on the server at the moment. Try again later; it is not a fault in your request. |
| 429 | {"reason": "rate_limited"} | Too many requests; wait the seconds in Retry-After. |
Errors#
A refusal comes back in errors, each with extensions.code to switch on and a message written to be read by a person. A request refused on its credentials (no bearer token, a key that is unknown, revoked or expired), on its rate or on a forbidden header also carries a matching HTTP status; everything else is a 200 whose errors carry the code. Switch on extensions.code, not on the status.
| Code | Status | Meaning |
|---|---|---|
UNAUTHENTICATED | 401 | No bearer token, one that is not a key, or a key that is unknown, revoked or expired; the message says which. |
RATE_LIMITED | 429 | Too many requests. Wait the seconds in the Retry-After header. |
BAD_USER_INPUT | 400 or 200 | A forbidden header (400); otherwise an input that is wrong, or a query that asks for too much at once (extensions.reason is QUERY_DEPTH, QUERY_BREADTH or QUERY_COMPLEXITY). |
FORBIDDEN | 200 | Refused on who you are. extensions.reason says which: NOT_ENTITLED (the key or the engagement lacks the scope, or the engagement is not yours), ENGAGEMENT_LAPSED (it has ended or not started), INVALID_ENGAGEMENT (the header is not an engagement id), NOT_A_PERSON (keys are managed by people, not by another key), NOT_OWN_FIRM (keys are managed from your own firm, not under an engagement). |
CONFLICT | 200 | The change collides with what is already recorded, such as a second appeal for the same year, or the bill or appeal changed after the version you read (extensions.reason is STALE_VERSION): read it again and decide on what it says now. |
UNRESOLVED_VALIDATIONS | 200 | A bill cannot be accepted while an error-level check stands against it; extensions.codes lists them. |
UNDO_REFUSED | 200 | A review decision cannot be taken back. extensions.reason is NO_DECISION, ALREADY_UNDONE, NOT_YOURS (only the person who decided can), EXPIRED (older than 10 minutes) or CHANGED (the bill moved since). |
INVALID_CURSOR | 200 | A paging cursor from another filter or another list. |
AUTH_UNAVAILABLE | 503 | The service cannot verify credentials at the moment. Retry with a backoff; it is not a fault in your request. |
A query GraphQL itself cannot accept (an unknown field, a missing variable) is a 200 whose errors carry GRAPHQL_VALIDATION_FAILED and the position of the fault; a request that sends Accept: application/graphql-response+json gets it as a 400.
Limits#
- 600 requests a minute per key, refilling continuously, so a steady loader never meets the limit and a short burst is absorbed. Past it, a 429 with
Retry-After. Downloads count against the same limit. - Query size: at most 8 levels deep, 250 fields, and a cost of 5,000 where a list counts ten times what it holds. Every screen of the app fits well inside; a query that asks for everything does not. Page through lists instead. Every example in this documentation is held under these limits with each release.
- Imports:
importParcelstakes up to 500 rows a call, andimportPaymentsup to 2,000.
AI assistants#
The same data is offered to assistants that speak the Model Context Protocol, at https://api.retrieveiq.net/mcp, with the same key as the bearer token, under the same scopes and engagements as the GraphQL API. AI assistants lists its tools and how to connect one.
This documentation is also written for assistants to read: /llms.txt indexes every page, and each page is available as Markdown at its address with .md added. The Copy page button at the top of each page copies that Markdown.
Glossary#
- Parcel: one property as the county assesses it, with a parcel number. What the portfolio counts.
- Taxing unit: the office that assesses a parcel or sends its bill; a county in California, a county, township or school district elsewhere. The reference calls it a
Jurisdiction. - Tax account: one parcel's billing relationship with one unit, under the number the unit uses. A parcel billed by three units has three.
- Bill and installment: a bill is one year's tax from one unit on one account, paid in installments, each with its own due date. A payment posted or interest grown updates the bill in place; a re-issue with other charges, or a person's correction, is its next revision, waiting for review.
- Check: a comparison run against a bill as it arrives (the parcel number, the year, a jump from last year). The reference calls it a
Validation; itsseveritysays whether it blocks acceptance. - Withheld: a bill with an unresolved
ERRORcheck. Its figures are shown for review and left out of every total until the check is resolved or the bill rejected. - Owner of record: the entity the parcel is held under, as the client registered it. The reference calls it a
LegalEntity. - Engagement: a client's standing arrangement for your firm to act on its portfolio, with its own scopes and dates.
- Scope: one of the permissions a key holds; see Scopes.
- Deadline: a payment or appeal date derived from a bill and the unit's rules. One the rules cannot date is still listed, with the reason.
- Assessment year and payable year: see Conventions.