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.

This page as Markdown

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):

graphql
{
  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.

graphql
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.

graphql
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):

graphql
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: true means an ERROR check stands: the figures are shown here and left out of every total until a person resolves the check or rejects the bill.
  • amountPaid is what the county publishes; paymentKnown: false means it publishes nothing, which is not the same as nothing paid.
  • charges describe the whole bill and sum to totalBilled, never to one installment. levyingAgency and agencyPhone are the office to ask about a line.
  • revision counts 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 a key of their own and waiting for review, and the revision they replace reads SUPERSEDED or REJECTED and counts nowhere. A payment posted or interest grown updates the same revision.
  • version moves with every change to the bill, its figures and its decisions alike; a review decision names it (below).
  • The document's bytes are a GET of https://api.retrieveiq.net followed by its contentPath, with the same headers.

What is outstanding across the portfolio, and by county (needs PARCEL_READ and BILL_READ):

graphql
{
  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:

graphql
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:

bash
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 than maxBytes. 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):

graphql
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:

graphql
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:

graphql
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:

graphql
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:

graphql
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:

graphql
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:

graphql
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:

graphql
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:

graphql
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:

graphql
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:

graphql
mutation ($appealId: ID!, $version: Int!) {
  updateAppealStatus(appealId: $appealId, expectedVersion: $version, action: FILE, filedOn: "2026-11-20") {
    id
    version
    status
    filedOn
  }
}
graphql
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:

graphql
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:

graphql
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):

graphql
{ 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:

graphql
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:

graphql
{
  reportJobs(first: 5) { id kind format status requestedAt finishedAt rowCount byteSize downloadUrl error }
}
bash
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):

graphql
{
  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.