> ## Documentation index
> Fetch the complete index at: /llms.txt
> Use it to discover every page before exploring further.

# 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](/docs.md#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](/docs.md#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](/docs/mutations.md#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](/docs/mutations.md#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](/docs/mutations.md#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](/docs/mutations.md#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](/docs/mutations.md#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](/docs/mutations.md#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](/docs/mutations.md#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](/docs/mutations.md#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](/docs/mutations.md#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](/docs/mutations.md#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](/docs/mutations.md#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.
