Guide
Common tasks
The things a firm's own system does against the API, each as a short walk with the exact operations to send. Every example here validates against the schema with each release.
Before you start#
Every example below is sent the way Your first request shows, with an API key and, when the portfolio is a client's, the X-Engagement-ID header from Acting for a client. Ids are passed as variables; literal values are made up.
Load a portfolio#
A parcel belongs to an owner of record and to the unit that assesses it. List the owners the portfolio already holds, and find the unit by state and name (needs PARCEL_READ):
{
legalEntities { id name }
jurisdictions(state: "CA", search: "Los Angeles") { id name unitCode installmentsPerYear }
}Add one parcel with upsertParcel (needs PARCEL_WRITE, and PARCEL_READ to read the parcel back). Naming the county is enough in California; jurisdictionId names the unit directly elsewhere. Repeating the call is safe: a parcel already held comes back with created: false and nothing changed.
mutation ($owner: ID!) {
upsertParcel(input: {
parcelNumber: "5144-012-023"
county: "Los Angeles"
legalEntityId: $owner
situs: { line1: "350 S Grand Ave", city: "Los Angeles", state: "CA", postalCode: "90071" }
}) {
created
parcel { id parcelNumber status jurisdiction { name } }
}
}Add a spreadsheet of parcels with importParcels, up to 500 rows a call. Run it with dryRun: true first: every row is checked and nothing is written, and rejected names each bad row by its index. Then send the same call without dryRun. An owner is named once for the whole call, by id or by a name that is created when it is new.
mutation {
importParcels(input: {
dryRun: true
legalEntityName: "Grand Avenue Holdings LLC"
rows: [
{ parcelNumber: "5144-012-023", county: "Los Angeles", situsLine1: "350 S Grand Ave", city: "Los Angeles", zip: "90071" }
{ parcelNumber: "5149-034-010", county: "Los Angeles" }
]
}) {
dryRun
created
existing
rejected { index reason }
}
}Read bills and their checks#
A parcel's bills arrive from the fetch, newest year first. Each bill carries its installments, the checks run against it, the charges the county levied and the document it was read from (needs PARCEL_READ and BILL_READ):
query ($id: ID!) {
parcel(id: $id) {
parcelNumber
status
amountOutstanding { known installmentCount installmentsWithUnknownPayment withheldCount }
bills(last: 6) {
key
assessmentYear
payableYear
billType
billNumber
revision
version
status
totalBilled
withheld
withheldReason
installments { id installmentNo installmentLabel dueDate delinquentAfter amountDue amountPaid paymentKnown amountOutstanding status }
validations { id code severity message expected observed resolvedAt }
charges { description amount chargeType levyingAgency agencyPhone }
sourceDocument { id filename contentPath receivedAt }
}
}
}withheld: truemeans anERRORcheck stands: the figures are shown here and left out of every total until a person resolves the check or rejects the bill.amountPaidis what the county publishes;paymentKnown: falsemeans it publishes nothing, which is not the same as nothing paid.chargesdescribe the whole bill and sum tototalBilled, never to one installment.levyingAgencyandagencyPhoneare the office to ask about a line.revisioncounts the bill's issues. When the unit re-issues it with other charges, or a person corrects a reading, the new figures are the next revision, under akeyof their own and waiting for review, and the revision they replace readsSUPERSEDEDorREJECTEDand counts nowhere. A payment posted or interest grown updates the same revision.versionmoves with every change to the bill, its figures and its decisions alike; a review decision names it (below).- The document's bytes are a
GETofhttps://api.retrieveiq.netfollowed by itscontentPath, with the same headers.
What is outstanding across the portfolio, and by county (needs PARCEL_READ and BILL_READ):
{
amountOutstanding { known installmentCount installmentsWithUnknownPayment withheldCount }
countiesInPortfolio { jurisdiction { name state } parcelCount amountOutstanding { known withheldCount } }
}Upload a document#
A bill that reached you on paper or by email, or any other document about a parcel, can be filed on the parcel (needs BILL_REVIEW). Start the upload with startDocumentUpload, naming the parcel, what the document is, its file name and its exact size. The answer is a link for the bytes, good once, for fifteen minutes:
mutation ($parcelId: ID!) {
startDocumentUpload(input: { parcelId: $parcelId, kind: TAX_BILL, filename: "2026-secured-bill.pdf", byteSize: 182344 }) {
uploadPath
method
expiresAt
maxBytes
}
}Then send the file to that path with a PUT and the same Authorization header. The link already names the portfolio the upload was started under, so it needs no X-Engagement-ID:
curl -X PUT "https://api.retrieveiq.net$UPLOAD_PATH" \
-H "Authorization: Bearer $RETRIEVEIQ_API_KEY" \
--data-binary @2026-secured-bill.pdf- A PDF, CSV or Excel (
.xlsx) file, no larger thanmaxBytes. Scans and photos are not read. - A bill PDF from a county whose bills the product reads exactly is read as a bill, which waits for review like a fetched one; anything else is kept on the parcel as evidence. The answer, a 201, names the document and says what became of the reading.
- The same file already on the parcel is that document, and the answer, a 200, says so; the same file on another parcel is refused with a 409 that names it, and a bill printed for another parcel with a 422.
- A link already used or past its fifteen minutes answers 410: start again. Uploaded files are kept for seven years.
Review bills#
A fetched bill waits for a decision. The inbox lists the ones waiting, oldest first; decide per bill by naming its installment ids (needs BILL_READ and PARCEL_READ to read the inbox, and BILL_REVIEW as well to decide):
query ($after: String) {
inboxSummary { needsReview disputed withheld receivedThisWeek }
billInbox(first: 25, after: $after, filter: { view: WAITING }) {
totalCount
pageInfo { hasNextPage endCursor }
edges {
node {
key
version
status
withheld
totalBilled
parcel { id parcelNumber }
taxAccount { jurisdiction { name } accountNumber }
installments { id installmentLabel amountDue dueDate status }
validations { id code severity message resolvedAt }
}
}
}
}Accept a bill whose figures are right, naming every installment of the bill. Refused with UNRESOLVED_VALIDATIONS while an ERROR check stands against it:
mutation ($installmentIds: [ID!]!, $version: Int!) {
acceptBill(installmentIds: $installmentIds, expectedVersion: $version, note: "Matches the county's figures") {
bill { key version status installments { id status } }
notVisible
}
}Every decision names, as expectedVersion, the bill's version as you read it. When anything about the bill changed since (a fetch, a check resolved, another person's decision), the decision is refused with CONFLICT (extensions.reason is STALE_VERSION) and nothing is written: read the bill again and decide on what it says now.
Flag a bill for the client when a person on their side should look first. The note is what the client reads:
mutation ($installmentIds: [ID!]!, $version: Int!) {
flagBillForClient(installmentIds: $installmentIds, expectedVersion: $version, note: "The land value rose 40 percent; please confirm before we accept.") {
bill { key version status }
notVisible
}
}Reject a bill that was read wrongly. Rejection is final, apart from the undo below, and a later fetch does not reopen the bill; a re-issue with other charges is the bill's next revision, waiting for review:
mutation ($installmentIds: [ID!]!, $version: Int!) {
rejectBill(installmentIds: $installmentIds, expectedVersion: $version, reason: "Wrong parcel: the county page showed a neighbouring APN.") {
bill { key version status }
notVisible
}
}Resolve a check when the discrepancy is explained. The note is required because the next reader asks why, and the bill is no longer withheld once no check stands. Resolving moves the bill's version:
mutation ($id: ID!) {
resolveValidation(id: $id, note: "The supplemental bill explains the jump; confirmed with the assessor.") {
validation { id code resolvedAt }
bill { key version withheld status }
}
}Take back your own decision within 10 minutes with undoBillReview, naming the version the decision's answer carried; each installment returns to what it was, and both the decision and its undoing stay in the activity trail:
mutation ($installmentIds: [ID!]!, $version: Int!) {
undoBillReview(installmentIds: $installmentIds, expectedVersion: $version) {
bill { key version status installments { id status } }
notVisible
}
}notVisible lists the ids you named that the request could not act on, so a partial result is never silent; a response that also carries an error means the whole request was undone. Every decision is recorded against the parcel with who made it and when; activity on the parcel reads the trail.
When a reading is wrong and you have the right figures, correctBill records them as the bill's next revision, checked like any reading and waiting for review, and keeps the one it replaces as evidence. A tax bill page that no reading turned into a bill is entered by hand with enterBill.
Record what was paid#
What a county publishes as paid is on each installment. Your own records go beside it, never into it: record a payment, a refund or a signed adjustment against an installment (needs BILL_REVIEW and BILL_READ). The same entry sent twice is one entry:
mutation ($installmentId: ID!) {
recordPayment(installmentId: $installmentId, kind: PAYMENT, amount: "35743.16", effectiveDate: "2026-12-08", reference: "Check 10482") {
recorded
installment { id amountPaid paymentKnown paidPerRecords payments { kind amount effectiveDate reference } }
}
}Or a whole list at once with importPayments, up to 2,000 rows a call, each naming its parcel, year and installment (needs BILL_REVIEW and PARCEL_READ). Run it with dryRun: true first. A row whose number matches no parcel exactly comes back in rejected with the parcels it may mean, to send again with parcelId:
mutation {
importPayments(input: {
dryRun: true
rows: [
{ parcelNumber: "5144-012-023", assessmentYear: 2026, installmentNo: 1, amount: "35743.16", paidOn: "2026-12-08", reference: "Check 10482" }
{ parcelNumber: "5149-034-010", assessmentYear: 2026, installmentNo: 1, amount: "12210.40", paidOn: "2026-12-09" }
]
}) {
dryRun
recorded
alreadyRecorded
rejected { index reason }
}
}paidPerRecords on an installment, and installmentsPaidPerRecords on every outstanding total, say what your records show paid where the county has published no payment. They are never subtracted from what is owed, since the county has not confirmed them.
Deadlines, appeals and the calendar#
Payment and appeal dates across the whole portfolio, soonest first (needs PARCEL_READ; exposure, the balance at stake on a payment, is bill data and needs BILL_READ as well). Pass from and to on a large portfolio, which otherwise lists every date to come. A date the rules cannot work out is listed with on: null and the reason, never dropped; a parcel with no bill yet has no appeal dates. A unit's own appeal window belongs to no parcel, and is listed once with parcel: null:
query ($from: Date!, $to: Date!) {
deadlines(from: $from, to: $to) {
kind
on
exposure
qualifier
unevaluableReason
parcel { id parcelNumber }
jurisdiction { name }
}
}Record an appeal of one assessment year (needs APPEAL_WRITE and PARCEL_READ). The filing deadline comes from the unit's own window unless deadlineOn is given; CONFLICT means the year already has one:
mutation ($parcelId: ID!) {
openAppeal(parcelId: $parcelId, assessmentYear: 2026, requestedValue: "18500000.00", note: "Comparable sales support a lower value.") {
id
version
status
deadlineOn
deadlineReason
assessedValue
requestedValue
}
}File it once it is lodged, and record the decision when it comes, with the savings if known; once decided, only the appeal's note can change. Each names, as expectedVersion, the appeal's version as you read it, and is refused with CONFLICT when the appeal changed since:
mutation ($appealId: ID!, $version: Int!) {
updateAppealStatus(appealId: $appealId, expectedVersion: $version, action: FILE, filedOn: "2026-11-20") {
id
version
status
filedOn
}
}mutation ($appealId: ID!, $version: Int!) {
recordAppealOutcome(appealId: $appealId, expectedVersion: $version, decidedOn: "2027-03-02", decidedValue: "19200000.00", savings: "9400.00") {
id
version
status
decidedValue
valueReduction
savings
}
}While the appeal is open or filed, updateAppeal changes the value it asks for or when its window closes; its note can change at any status:
mutation ($appealId: ID!, $version: Int!) {
updateAppeal(appealId: $appealId, expectedVersion: $version, requestedValue: "18000000.00", note: "Lowered after the second appraisal.") {
id
version
requestedValue
deadlineOn
note
}
}A person who has dealt with a date marks it with acknowledgeDeadline (needs BILL_REVIEW). A date that later moves comes back unacknowledged, with supersedes saying what it was.
For a calendar application, create a subscribable iCal URL over the whole portfolio's dates (needs PARCEL_READ; each payment carries the balance at stake when the key also holds BILL_READ). The URL carries its own secret and is shown once; it answers until it is revoked with revokeCalendarFeed, or until the key that made it is revoked or expires:
mutation {
createCalendarFeed(label: "Outlook") {
url
feed { id label createdAt }
}
}Reports#
The catalogue lists each report with its columns, the formats it comes in and the scopes it reads with (any key):
{ reportCatalog { kind name category description formats columns scopes } }Request one (needs REPORT_READ and the scopes the catalogue lists for it). It is produced in the background with the access of the request that asked for it, the engagement's scopes capped by the key's, checked again when it runs; the filter takes the parcel list's conditions apart from the assessed value:
mutation {
requestReport(kind: DEADLINE_EXPOSURE_REGISTER, format: XLSX, filter: { states: ["CA"] }) {
id
status
name
}
}The appeal evidence pack (APPEAL_EVIDENCE_PACK) is made for one appeal or a few by naming them in the filter's appealIds; as a ZIP it holds the documents themselves beside the pack. rerunReport asks again for a report that failed or expired, as it was asked.
Poll the jobs until status is DONE, then fetch downloadUrl with a GET and the same headers:
{
reportJobs(first: 5) { id kind format status requestedAt finishedAt rowCount byteSize downloadUrl error }
}curl -OJ https://api.retrieveiq.net/reports/<id>/download \
-H "Authorization: Bearer $RETRIEVEIQ_API_KEY" \
-H "X-Engagement-ID: $ENGAGEMENT_ID"Watch the nightly fetch#
Which counties answered last night and which are failing, per county the portfolio holds parcels in, and the client's last nights of the file feed where one is set up (needs BILL_READ and PARCEL_READ, under the client's engagement):
{
ingestionStatus { jurisdiction { name } parcels lastAttemptAt lastAnsweredAt lastSuccessAt lastOutcome streak }
feedRuns(last: 7) { id feedName startedAt finishedAt outcome deliveredTo message }
}A county with a streak of failures has usually changed its website; lastOutcome says how it failed. Your bills from that county stand as last fetched until it answers again.