2026-07-28
One constraint relaxed, one cross-field rule made visible, and the conformance
checklist rewritten so every item is a test you can run.
/pipelines— an integer sent as a numeric string is accepted. Both the pipeline-level and stage-levelsortwere documented as failing the page for anything that is not an integer. That is one step too strict:"3"is coerced to3and accepted. A fractional value and a genuinely non-numeric value are still rejected and still fail the page, andsortstill defaults to0when omitted ornull.sortnow behaves likepriceon a lead, and unlikeduration_secondson a call, which continues to reject a numeric string./calls—lead_idandcontact_idare conditionally required, and now say so where you look. Each isRequired: Noon its own and always has been, but every call must carry at least one of them. That rule was stated in the constraint cell and in Association; it is now also stated immediately above the field table, so reading only the Required column no longer misleads. No requirement changed — a call with neither has always been rejected.- The conformance checklist is rebuilt around
checkable tests. Every box is now one concrete thing to send and one concrete thing
to look for in the response, grouped by endpoint and linked to the page that states the
rule. Items that asked you to review, intend, or judge something were dropped, and the
field-level requirements the old list omitted —
stagesbeing non-nullable,sortbeing an integer,custom_fieldsbeing an object,lead_idon an event, integerduration_seconds,idlength — were added. The design decisions that no response can reveal are listed separately at the end of the page. - Writing conventions applied to the non-endpoint pages. Internal role names, encryption internals, and internal process topology were removed, and the canonical vocabulary is now used throughout. No requirement changed. This includes the rate-limit wording introduced in v1.0.10 below, which now states the honest disclosure — treat the declared rate as a target rather than a hard ceiling, since sustained load may reach a small multiple of it — without publishing how HunterAI is deployed.
2026-07-28
Correction release. Three published statements about how a run is retried and reported
did not match the adapter; they are corrected here. No field requirement changed — if
your API already conforms, nothing you send needs to change.
- Any entity failure fails the whole sync — being optional does not contain it.
How sync works → Partial success and the
three optional endpoint pages previously said that a failing optional entity
(
/tasks,/calls,/events) left the overall sync successful. It does not: a500, a503that outlives its retries, a malformed record, or a tripped pagination guard on any entity marks the whole run failed.optionalgrants exactly one concession — you may answer404, which is skipped cleanly and keeps the sync green. Committed data is unaffected either way: pages that committed stay committed and the remaining entities still run. This supersedes the “optional-entity failure is contained” bullets in v1.0.3 and v1.0.2, which were wrong. - Only
502,503, and504are retried. Errors and every endpoint’s failure modes previously said “5xx… retried up to 5 times”. Only those three transient codes are. A500,501,505, or any other5xxis terminal on the first response, with no retry, exactly like a hard4xx. Return503for a transient problem —500is the reflex choice and it costs you every retry. Request timeouts are still retried the same way as a503. - The rate limiter is per connection, not per base URL. Rate limits said concurrent connections to one host shared a single budget. Each connection has its own budget, so two connections against the same base URL are throttled independently and the traffic they send you adds up. The endpoint pages already said this; the rate-limits page now agrees with them.
- The declared rate is a target, not a hard ceiling. Rate limits now
states that sustained load may reach a small multiple of the declared rate. Size with
headroom, and return
429if you need a firm limit enforced on your side. /events—value_before/value_afterare typedanyand are nullable. Both were documented asobject, non-nullable. They are genuinely polymorphic — a scalar is wrapped as{ "value": <scalar> }, an object or array is stored unchanged — and an explicitnullis stored asnull, not rejected. The accepted shapes were already correct in the constraint cell; the type and nullability now match them.
2026-07-25
A missing or
null stages is now enforced as an error, not just documented as one.- Missing or
nullstages→ the page fails. A pipeline whosestagesis absent ornullis now rejected: it fails its/pipelinespage and, because/pipelinesis required, fails the sync.stagesis required and non-nullable, so an absent key is treated exactly like an explicitnull. - No more silent empty funnel. The adapter used to tolerate a missing or
nullstages, quietly persisting the pipeline with no stages so every lead under it landed unclassifiable (is_won = is_lost = false) with no error. That silent path is closed — the pipeline now fails loudly instead. This matches what/pipelinesalready documented; the adapter now enforces it. - An empty
stages: []is unchanged. An explicitly empty array is still valid — a genuinely empty funnel — and only its leads cannot be classified won or lost. A source that sends each pipeline’s stages is unaffected.
2026-07-25
A non-numeric
price is now enforced as an error, not just documented as one.- A present non-numeric
price→ the page fails. Apricethat is present but not numeric — text such as"abc", a partly-numeric string like"12abc", an empty string, or any valuefloat()cannot read — is now rejected: it fails its/leadspage and, because/leadsis required, fails the sync. A numeric string such as"48000000"is still accepted, and any fractional part is still dropped. - No more silent
0. The adapter used to coerce any unparseablepriceto0, quietly discarding a real lead amount so every revenue and average-value metric trusted the wrong value. That silent path is closed — the amount now fails loudly instead. This matches what/leadsalready documented; the adapter now enforces it. - Absent price is unchanged. An absent or
nullpricestill defaults to0— only a present, non-numeric value is rejected. A source that emits numeric prices (or numeric strings) is unaffected.
2026-07-25
An unrecognised or missing stage
kind is now enforced as an error, not just
documented as one.- Unknown or missing
kind→ the page fails. A stage whosekindis not exactlyopen,won, orlost(matched case-insensitively) — or is absent/null— is now rejected: it fails its/pipelinespage and, because/pipelinesis required, fails the sync.kindis required and non-nullable, so there is no default; a missing value is rejected exactly like a typo such as"win"or"closed". - No more silent
open. The adapter used to fall back toopenfor any unrecognised or missingkind, quietly mis-classifying leads so every downstream win-rate and conversion metric trusted the wrong value. That silent path is closed — the classification now fails loudly instead. This matches what/pipelinesalready documented; the adapter now enforces it. - No contract change for a conformant source. A source that already emits exactly
open,won, orlostfor every stage is unaffected.
2026-07-25
Ascending
updated_at order is now enforced, not just required.- Out-of-order pages are rejected. For an entity whose watermark advances page by
page (
/leads,/contacts), HunterAI now verifies each page is ascending byupdated_at— within a page and across page boundaries. A backwards step marks that entity’s sync failed (no retry) and leaves the watermark at the last correctly-ordered point. Previously the order was trusted silently, so a mis-sorted response could advance the watermark past records on a later page and lose them. - No contract change for a conformant source. A source that already returns ascending
updated_at(as every endpoint page requires) is unaffected.
2026-07-25
A null or missing
id is now documented as rejected, and the adapter enforces it.- Null/missing
id→ the entity fails. Errors now documents that a record whoseidisnullor absent — and, for/events, anull/absentlead_id— is rejected (it fails that entity for the run, with no retry), matching every endpoint page’s “required, non-nullable”id. This is intended contract, previously stated only per field. - No more
"None"coercion. The adapter used to stringify anullid to the literal"None", so two null-id records collided on the same key and one silently overwrote the other. That silent data-loss path is closed — a null id now fails loudly instead.
2026-07-25
/leads — the price default is now documented. No adapter change.- Absent or
nullpricedefaults to0. Thepricerow previously listed no default; its Notes now state that an absent ornullvalue is stored as0. This documents existing, intended behaviour — a gap in the reference, not a contract change. A present non-numeric value is still rejected (the intended contract of §5, unchanged).
2026-07-24
Internal consistency pass — the Limitations and How-sync-works pages now match the
endpoint reference exactly. No change to the adapter contract; these corrections remove
documentation that contradicted the endpoint pages.
- Stage
kind— Limitations now matches/pipelines. An unrecognised or missing stagekindis rejected as an error (it fails the page, and because/pipelinesis required, the sync) — not silently classified asopen. The Limitations page had still described the old silent-fallback behaviour. - Partial success — optional-entity failure is contained.
How sync works now states that the overall
sync is marked failed only when a required entity fails; an optional entity
(
/tasks,/calls,/events) failing takes down only that entity and the sync still succeeds. The section previously said any entity failure failed the sync. - Page-level atomicity stated on the sync page. How sync works now spells out that a page is written as one transaction, so one malformed record fails its whole page (page-level, not per-record) — matching every endpoint page.
- Timestamp formats aligned. The Limitations timestamp section now defers to the
endpoint pages’ Timestamp format list instead of
a narrower one, so every page accepts the same forms (explicit offset, trailing
Z, no zone,T-or-space separator, bare date). - Redaction. The internal repo path was removed from the introduction, leaving the reference to the production adapter (writing conventions).
2026-07-24
The three optional endpoints brought to the reference standard, with several
field requirements now stated precisely.
/events,/tasks,/callsrewritten to the full endpoint-page standard: complete §4 field tables (type, required, nullability, constraint, example, notes), full response envelopes with paging, and explicit identity, incremental, deletion, and failure-mode sections.- Optional-entity failure is contained. For all three, a page that fails (a malformed record) fails only that entity and reports it failed; because the endpoint is optional, the overall sync still succeeds. A required entity’s page failure still fails the whole sync.
/events—lead_idis hard-required. An event without alead_idfails its page and thus the events entity.typeis never null — an absent value becomes an empty string (truncated to 100 chars).value_before/value_afteraccept a scalar, object, or array: a scalar is wrapped as{ "value": <scalar> }, an object or array is stored unchanged. The watermark prefersupdated_atand falls back tocreated_at;updated_atis read for the watermark only and is not stored./tasks— association rule stated unambiguously.lead_idwins when bothlead_idandcontact_idare present;contact_idis used only whenlead_idis absent; a task with neither is accepted but linked to nothing and is invisible in every report. A task’s owncreated_atis ignored entirely (no column) and is now listed under fields the adapter ignores./calls— stricter than/tasks, by design. A call with neitherlead_idnorcontact_idis rejected and fails its page — the opposite of a task, which tolerates it; the asymmetry is now called out on both pages.duration_secondsmust be an integer; a non-numeric value fails the page — stricter thanprice, which tolerates a numeric string (a known inconsistency).directionacceptsinorout; any other value,null, or an omitted value silently becomesout.
2026-07-24
Field-spec corrections and two endpoint pages brought to the reference standard.
/pipelines—sortmust be an integer. Both the pipeline-level and stage-levelsortmust be an integer; a fractional or non-numeric value now raises a validation error and fails the page. No minimum or maximum is enforced, and an omitted ornullvalue still defaults to0./pipelines—stagesis now required and non-nullable. A missing ornullstagesis rejected as an error and fails the page (an intended contract; the adapter tolerates it today). An emptystages: []stays valid — a genuinely empty funnel — but its leads cannot be classified won or lost./leads—idlimit stated. The leadidis documented asmax 255 chars, matching/pipelines; an over-lengthidfails its page. No behaviour change — the limit was always enforced by the shared identifier column./usersand/contactsrewritten to the full endpoint-page standard: complete field tables (types, required/optional, nullability, constraints), full response envelopes, and explicit identity, incremental, deletion, and failure-mode sections./usersis not incremental and dropsphone;/contactsis incremental onupdated_atand carries no deletion signal at all.custom_fieldson a contact must be a JSON object — a non-object value is silently stored asnull.
2026-07-24
First stable release, backed by a working adapter.The custom-CRM adapter is implemented. Its field tables — types, required/optional,
nullability, and truncation limits — follow the field-table standard and are verified
against that implementation. At the time of this release a small number of conformance
requirements stated the intended contract for behaviours still to ship: a numeric
price (a non-numeric value rejected, not coerced to 0); a required, always-advancing
updated_at; ascending updated_at result ordering; page-atomic ingestion; a strict
stage kind; a required stages; request timeouts retried like a transient 5xx; and
per-connection rate limiting. All of them have since shipped — see v1.0.6 through
v1.0.10. Every page now documents current behaviour only.- Endpoints — four required (
/pipelines,/users,/contacts,/leads) and three optional (/events,/tasks,/calls), allGET, all read-only. - Authentication — long-lived read-only Bearer token, encrypted at rest. OAuth 2.0 documented as available on request, not implemented.
- Sync model — one-directional pull; initial full sync, then incremental via
updated_sincewith a per-entity watermark;has_morepaging; idempotent upsert. - Reference — full field tables with types, required/optional, and truncation limits for every entity.
- Limitations — documented in full, each with its consequence: currency not stored,
first-contact-only, lead-only tombstones, strict stage
kind, dropped fields, truncation, timestamp handling, and managers not yet linked to logins. - Onboarding — the real HunterAI connection endpoints (
/api/v1/crm/custom/…) and the test-connection call.
Pre-release
Internal draft written before the adapter existed. Superseded in full by v1.0, which is
the first version backed by a running implementation. Do not rely on any v0.9 artifact.