Skip to main content

Errors

Every error code the API emits, when it happens, and what to do about it.

Every error has the same shape, and every one carries a docsUrl pointing at its own entry on this page.

What a request you cannot afford looks like

bash
curl 'https://api.boardwalkai.com/api/v1/projects/search?states=UT&assetClasses=Apartment%20Building&dateRange=all&limit=100' \
  -H 'Authorization: Bearer bwk_live_YOUR_KEY'

Response · HTTP 402

json
{
  "error": {
    "code": "insufficient_credits",
    "message": "This request would deliver 100 records and cost 100 credits. You have 40.",
    "param": null,
    "details": {
      "quote": {
        "matchCount": 5000,
        "matchCountIsExact": true,
        "creditRate": 0,
        "creditsRequired": 100,
        "creditsRemaining": 40,
        "affordableRecordCount": 0,
        "countUrl": "https://api.boardwalkai.com/api/v1/projects/count?states=UT&assetClasses=Apartment%20Building&dateRange=all"
      }
    },
    "docsUrl": "https://boardwalkai.com/docs/api/errors#insufficient_credits",
    "requestId": "req_01K1QF3M0000EXAMPLE0001"
  }
}

The response shape on this page is the one the endpoint builds — the keys, their nesting and their types are asserted against the shipping code by a test. The numbers inside it are the scenario being walked through, not a measurement of the corpus.

FieldMeaning
error.codeA stable machine token. Branch on this.
error.messageHuman copy. May be reworded; do not parse it.
error.paramThe offending parameter, where there is one.
error.detailsStructured context — the quote on a 402, the problem list on a 400.
error.docsUrlA link to the entry below.
error.requestIdThe same id as the X-Request-Id header, our logs and your usage row. Quote it to support.

You can send your own X-Request-Id (characters A-Z a-z 0-9 _ . : -, up to 255 of them) and we will echo it back as requestId, which lets you join our logs to yours. When you do not send one, Boardwalk creates an id beginning req_ — so a requestId without that prefix is one you or your infrastructure supplied, not one we generated.

Every code

account_disabledHTTP 401

The key resolved, but the owning account has been disabled.

What to do: Ask a team owner or Boardwalk support to restore the account. Rotating the key does not change the account state.

alert_geography_requiredHTTP 403

A create or update left an alert with no geography at all, on a plan whose licensed geography is narrower than nationwide. A blank geography is a nationwide standing instruction, which is almost never what a blank field meant. Nothing was saved and nothing was charged. Pools whose plan grants nationwide geography — API Only and National — are exempt and may hold a nationwide alert.

What to do: Name at least one state, county or city in filters. If you genuinely want nationwide standing coverage, that needs a plan whose licensed geography is nationwide.

alert_geography_too_broadHTTP 403

An alert named more states, or more counties and cities, than a narrower plan allows in one rule. error.details.maxStates and error.details.maxLocalities state the ceilings. Nothing was saved and nothing was charged. API Only and National pools are exempt — on those plans one alert may span as many states as you like.

What to do: Split the rule into several alerts, each within the ceiling, or move to a plan whose licensed geography is nationwide. The 25-active-alert cap still applies to everyone, so splitting is not free of limits.

alert_limit_reachedHTTP 403

Your team already holds the maximum number of active alerts. The ceiling counts every alert on the account — including ones not shared with you — because it is a billing cap, not a visibility question.

What to do: Pause or delete one. error.details.maxActiveAlerts states the ceiling; GET /alerts reports meta.activeAlerts — how many the whole account holds, which can be more than the alerts your key can see.

credits_exhaustedHTTP 402

The balance cannot pay for even one record, and the response would have billed at least one. No limit could make such a page affordable, so there is nothing to quote and the response carries no quote — that is what separates it from insufficient_credits, which refuses one specific page and names the limit to retry with. Records you already own and that have not changed are not billable, so even at exactly 0 credits you keep reading them back free — a page or a record that bills 0 is served, not refused. The body is the standard error envelope. Nothing was delivered and nothing was charged.

What to do: Wait for the paid reset or add credits — no narrower filter or smaller limit changes the answer. Paid balances refill on the 1st of the calendar month (meta.credits.periodEnd / resetsAt on /account/credits name the exact moment); trial credits are a one-off allowance. The free count endpoint still works, so you can price the next spend before the balance refills, and re-reads of unchanged records you already purchased (including include=contacts&contactOptions.cachedOnly=true read-backs) keep working at 0 credits.

delivery_ledger_unavailableHTTP 503

A format=csv search could not read your delivery history, so the export was refused rather than reserved as all-new. Billing a CSV page classifies each row against what your account already owns; without that read, a page of records you already purchased would have been reserved at full price. Nothing was queued and no credits were reserved.

What to do: Retry — the outage is on our side and is usually brief. format=json remains available in the meantime; it serves during a ledger outage and discloses the state in meta.delivery.ledgerApplied so any over-charge is found and retro-credited.

enrichment_allowance_exceededHTTP 402

A POST /enrichments job would push the pool past its monthly enrichment allowance (500 attempts per calendar month on the API Only plan). The request is refused whole — nothing is queued, no allowance is consumed and no credits are involved, because enrichment of purchased projects never spends credits. error.details.requested is the attempt count that did not fit, and error.details.allowance carries used, limit, remaining and resetsAt.

What to do: Send fewer projectIdserror.details.allowance.remaining is the number that still fits this month — or wait for resetsAt, the first of the next calendar month. IDs already enriched recently are skipped free as skipped.alreadyEnriched, so re-submitting a mixed batch does not burn allowance on the ones we already looked up.

enrichment_incompatible_representationHTTP 400

include=contacts was combined with a representation that carries no contact fields — asking to pay for enrichment and then not receive it.

What to do: Use standard or full.

enrichment_ledger_unavailableHTTP 503

A POST /enrichments could not read the purchased-projects ledger, so the job was refused rather than accepted blind. Enrichment is free because it is restricted to records you already bought; without the ledger read, that restriction cannot be enforced. Fails closed: nothing was queued and no allowance was consumed.

What to do: Retry — the outage is on our side and is usually brief. Quote the X-Request-Id if it persists.

enrichment_requires_asyncHTTP 400

include=contacts was sent to a synchronous JSON route without contactOptions.cachedOnly=true. Finding contact info calls out to third-party sources and cannot finish inside a request.

What to do: Re-run the search without include=contacts, then send the ids it returns to POST /enrichments — enriching projects the pool has already bought is free and charges no credits. If you do not yet have the ids, queue POST /projects/ai-search/jobs with include: "contacts", or GET /projects/search?format=csv&include=contacts. To read contacts we already hold, keep the GET and add contactOptions.cachedOnly=true. error.details names every endpoint.

enrichment_requires_paid_planHTTP 402

include=contacts was sent on an unpaid plan (no live trial, no paid seat). Finding contact info is a paid-plan or live-trial toggle. Record credits may still remain.

What to do: Drop include=contacts and keep searching or exporting, start a free trial, or upgrade to a paid plan. Names, titles and companies from the filing still ship without the toggle. A live trial spends the 5-lookup grant via POST /enrichments. Nothing was charged.

enrichment_unavailableHTTP 501

Finding contact info is compiled in but not switched on for this environment.

What to do: Drop include=contacts and retry without it. If you expected this to be live, contact support. Nothing was charged.

export_expiredHTTP 409

The file lived out its retention window and was swept. expiresAt on the job is the deadline, and it had passed.

What to do: Run the search again with format=csv to build a fresh file. Records you already hold unchanged are free to receive again, so a rebuild usually costs less than the original — price it first with /projects/count.

export_not_readyHTTP 409

The export job exists but has not finished building. format=csv is asynchronous — the queue response is a 202 with an id, not a file.

What to do: Poll GET /api/v1/exports/{id} until status is ready, then download. The job carries creditsReserved from the moment it is queued, so there is nothing further to spend.

export_unavailableHTTP 409

The job finished and reported ready, but its file could not be read back from storage, so it was never delivered. This is not expiry — the retention window had not run out. It is a failure on our side.

What to do: Nothing to do about the credits: the reservation is returned automatically when this is detected, and error.details.creditsRefunded reports how many came back. The job then reads failed with an explanation rather than continuing to advertise itself as downloadable. Run the search again to rebuild the file, and quote the X-Request-Id if it recurs.

forbiddenHTTP 403

The authenticated account is not permitted to perform this operation, and no more specific policy code applies.

What to do: Check the key's account and plan permissions. Nothing was charged.

geographic_access_deniedHTTP 403

A requested state or county falls outside the geography licensed to this pool.

What to do: Remove the denied geography or expand the plan. details.deniedStates and details.allowedStates carry the id, name and abbreviation of each, so you do not have to resolve ids to answer "what can I query instead?" (deniedStateIds and allowedStateIds remain alongside them). To avoid the error entirely, call the free GET /locations/states and read the entitled flag on each state before you build a filter.

idempotency_in_flightHTTP 409

The same Idempotency-Key is currently being processed by another request.

What to do: Wait for Retry-After seconds and retry. Do not fan out the same key in parallel; the point of the key is that only one of them charges.

idempotency_key_reuseHTTP 409

An Idempotency-Key was reused with a different request body or filter set. Replaying a key is safe; changing the request under it is not.

What to do: Use a fresh key for a different request. Keys are scoped to the credit pool, so a teammate's key cannot collide with yours by accident and cannot be replayed by you either.

insufficient_creditsHTTP 402

The page this request would deliver costs more credits than the pool has left. A paid page is delivered whole or refused whole — never trimmed to fit — so the refusal is about one page, not the whole match set, and on the JSON search route limit caps that page's cost at 100 credits. Nothing was delivered and nothing was charged — the charge is attempted inside a transaction that is rolled back, so a 402 leaves your balance exactly where it was.

What to do: Read error.details.quote. affordableRecordCount is the limit to retry with — that page fits the balance and delivers the affordable part. creditsRequired is the refused page's cost (not the set's), creditsRemaining shows the gap, and countUrl re-prices this exact filter set for free. An agent can recover from this response without a human, which is why the URL is in the body.

internal_errorHTTP 500

Something failed on our side.

What to do: Retry. Nothing was charged — 4xx and 5xx responses never spend credits. Quote the X-Request-Id header when you contact support; it is the same id in your response body, in our logs and on the usage row.

invalid_cursorHTTP 400

A /projects/sync cursor was malformed, or was issued against a different filter set or a different entitlement scope than the one now being requested. A cursor is only lossless for the query it was created for.

What to do: error.details.recovery says it: repeat the request without the cursor. Records you already hold are not re-billed, so restarting a walk is cheap.

invalid_filterHTTP 400

One or more filter parameters were rejected: an unknown parameter name, a value outside the enum, a non-numeric range bound, or a place name that resolved ambiguously. Unknown parameters are refused, never ignored — silently dropping one would bill you for a broader query than you asked for.

What to do: error.details.problems[] names every offending parameter with its own message. The filter reference lists every accepted name, alias and value.

invalid_requestHTTP 400

A parameter was present but unusable — an unknown representation, a sort on the sync feed, a malformed body — or a required header was missing: POST /enrichments and POST /alerts/{alertId}/run refuse with this code and error.param: "Idempotency-Key" when the key is absent, because a key-less retry of either is indistinguishable from a second submission. error.param names the offending parameter or header.

What to do: Read error.param and error.details.supported, which lists the accepted values where there is a closed set. When error.param is Idempotency-Key, resend with the header — any opaque string up to 255 characters of A-Z a-z 0-9 _ . : -.

key_expiredHTTP 401

A rotated key reached the end of its overlap window — 1, 24 (the default) or 168 hours, whichever was chosen at rotation.

What to do: Switch to the replacement key issued at rotation. The old key cannot be reactivated, but no account or subscription change is required.

manual_runs_exhaustedHTTP 429

A POST /alerts/{alertId}/run after the alert already spent its daily manual-run allowance (3 per alert per calendar day of the service clock, US Mountain Time). Nothing ran, nothing was charged and the alert's cadence is untouched. Distinct from rate_limit_exceeded: that is a per-minute transport bucket, this is a product allowance that refills at that clock's next midnight — read error.details.resetsAt rather than computing the boundary yourself. Replaying an Idempotency-Key that already ran does not spend an attempt, and neither does a run that answered already_running.

What to do: Wait for error.details.resetsAt (also the Retry-After header) — or stop triggering manually: a cadence (cadence.frequency) is the intended way to run an alert repeatedly, and scheduled runs are not limited by this allowance. error.details.limit and error.details.used carry the arithmetic.

method_not_allowedHTTP 405

The path exists, but the request used an HTTP method that route does not accept.

What to do: Read the Allow response header and retry with one of the listed methods. The request was not processed and nothing was charged.

missing_api_keyHTTP 401

No credential was presented, or the value was not a Boardwalk API key.

What to do: Send Authorization: Bearer bwk_live_... or X-API-Key: bwk_live_.... A web session cookie or a login JWT is not a credential for this API; there is exactly one, and it is an API key.

rate_limit_exceededHTTP 429

More requests than the applicable allowance in a trailing 60 seconds. Valid keys are limited per key; missing credentials use a source-IP bucket and unknown or unusable Boardwalk-shaped keys use a credential-digest bucket.

What to do: Back off for Retry-After seconds. X-RateLimit-Remaining and X-RateLimit-Reset are on every response, so a well-behaved client never needs to see this code.

rate_limiter_unavailableHTTP 503

The rate limiter itself could not be reached. The request was refused rather than waved through.

What to do: Retry after Retry-After. Nothing was charged.

resource_not_foundHTTP 404

The project, document, alert, list or job id does not exist, or is not visible to this key. Both cases return 404 — an id that exists but that this key's user holds no membership on must not be distinguishable from one that does not exist. For alerts and lists, visibility follows the key's user: their own, plus shares that include them.

What to do: Check the id. If a teammate can see the alert or list and you cannot, ask them to share it with you (share.mode), or call with a key created by a user who is a member. If you are following a documents[].meetingDocumentId from a record, use that value rather than documents[].id.

search_unavailableHTTP 503

The search backend could not be reached, so no count and no results could be produced. This is deliberately not reported as zero matches: "we found nothing" and "we could not look" are opposite answers.

What to do: Retry. Nothing was charged.

service_unavailableHTTP 503

The requested API subsystem is temporarily unable to serve the request.

What to do: Retry with backoff. Nothing was charged. Quote the request id if the problem persists.

sharing_requires_paid_planHTTP 403

share.mode was team or custom on an account that cannot share — no team exists yet, or the plan is a trial or a tier without teams. The alert is untouched: a refused share rolls the whole create or update back, so nothing was half-applied and nothing was charged.

What to do: Either create the alert with share.mode: "private" (or omit share — private is the default), or make the account shareable first: create a team on the Team page, or upgrade to a paid plan. error.message says which of the two is missing.

subscription_requiredHTTP 402

The key is valid and the account has no active plan or trial — usually a lapsed subscription. The key is not revoked. It keeps working the moment the plan is reactivated.

What to do: Reactivate the plan. You do not need to re-issue keys or redeploy.

sync_tie_unresolvedHTTP 503

More records share one last-updated timestamp than a single sync page can position, so advancing the cursor could not be done without risking a skip.

What to do: error.details.suggestion: split the walk by geography and run one cursor per slice. Nothing was charged.

unauthorizedHTTP 401

The key was presented but did not resolve to an account — most often a revoked key, one whose rotation grace window has closed (1, 24 or 168 hours, chosen at rotation), or one still attributed to an account that has been closed.

What to do: Mint a new key on your profile. A rotating key's responses carry X-Boardwalk-Key-Rotating: true and a Warning header naming the moment it stops, so this is avoidable.

Warnings: a `200` that is telling you something

A successful response can carry meta.warnings[], each entry {code, message, params}. A warning never fails a request and never changes what you were charged — but two of them below change which records you got, which is the same thing by the time it reaches your pipeline. Branch on code; the message is prose.

CodeWhat happenedChanged the result set?
default_date_window_appliedYou sent no dateRange and no meetingDateFrom/To, so the default last-12-months window was applied for you. It is also echoed in meta.appliedDefaults.Yes — older records were excluded. Send dateRange=all if you meant everything.
taxonomy_filter_rolled_upYou sent a parent Building Type alongside one of its children; the parent supersedes the child, so the narrower value was dropped.Yes — the result set is the parent's, wider than the pair you sent.
geography_narrowed_to_smallest_scopeYou sent states together with counties/cities. The narrower scope supersedes; the states are not searched in full.Yes — check meta.appliedFilters before paging, especially with numeric ids, which skip name resolution.
limit_clamped_to_page_sizeYour limit was above the maximum page size and was clamped to it. Emitted on /projects/search and /projects/sync.No — you got a full page, just a smaller one. Keep paging.
sync_records_unpositionableGET /projects/sync only: some candidate records could not be positioned in the cursor ordering and were skipped for this page.Yes, for this page. The walk still terminates; keep following the cursor.
filter_evidence_unconfirmedA filter you sent currently matches the full index because its attribute could not be confirmed on every record. params names the filters.Yes — a returned record with a null value for that attribute is one we could not confirm, not one that matched.
budget_exceeds_monthly_allowanceOn alert create/update: the maxCredits budget you set is larger than your plan's whole monthly allowance.No — the alert was saved. It is a warning that one run could spend the month.
alert_nl_enrichment_failedOn alert create/update with specificRequirement: the natural-language criteria could not be enriched, so the alert falls back to its structured filters.Yes, on every run — the NL half of the criteria is not being applied.

Ready to make a call?

A free Boardwalk trial includes API access, 50 record-export credits (search, CSV, MCP), and 5 Find Contact Info lookups. Counting, taxonomy, location lookups and the analytics plane cost nothing, so you can evaluate the data before you spend a credit.