Skip to main content
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-level sort were documented as failing the page for anything that is not an integer. That is one step too strict: "3" is coerced to 3 and accepted. A fractional value and a genuinely non-numeric value are still rejected and still fail the page, and sort still defaults to 0 when omitted or null. sort now behaves like price on a lead, and unlike duration_seconds on a call, which continues to reject a numeric string.
  • /callslead_id and contact_id are conditionally required, and now say so where you look. Each is Required: No on 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 — stages being non-nullable, sort being an integer, custom_fields being an object, lead_id on an event, integer duration_seconds, id length — 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: a 500, a 503 that outlives its retries, a malformed record, or a tripped pagination guard on any entity marks the whole run failed. optional grants exactly one concession — you may answer 404, 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, and 504 are retried. Errors and every endpoint’s failure modes previously said “5xx … retried up to 5 times”. Only those three transient codes are. A 500, 501, 505, or any other 5xx is terminal on the first response, with no retry, exactly like a hard 4xx. Return 503 for a transient problem500 is the reflex choice and it costs you every retry. Request timeouts are still retried the same way as a 503.
  • 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 429 if you need a firm limit enforced on your side.
  • /eventsvalue_before / value_after are typed any and are nullable. Both were documented as object, non-nullable. They are genuinely polymorphic — a scalar is wrapped as { "value": <scalar> }, an object or array is stored unchanged — and an explicit null is stored as null, 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 null stages → the page fails. A pipeline whose stages is absent or null is now rejected: it fails its /pipelines page and, because /pipelines is required, fails the sync. stages is required and non-nullable, so an absent key is treated exactly like an explicit null.
  • No more silent empty funnel. The adapter used to tolerate a missing or null stages, 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 /pipelines already 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. A price that is present but not numeric — text such as "abc", a partly-numeric string like "12abc", an empty string, or any value float() cannot read — is now rejected: it fails its /leads page and, because /leads is 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 unparseable price to 0, 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 /leads already documented; the adapter now enforces it.
  • Absent price is unchanged. An absent or null price still defaults to 0 — 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 whose kind is not exactly open, won, or lost (matched case-insensitively) — or is absent/null — is now rejected: it fails its /pipelines page and, because /pipelines is required, fails the sync. kind is 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 to open for any unrecognised or missing kind, 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 /pipelines already documented; the adapter now enforces it.
  • No contract change for a conformant source. A source that already emits exactly open, won, or lost for 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 by updated_atwithin 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 whose id is null or absent — and, for /events, a null/absent lead_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 a null id 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 null price defaults to 0. The price row previously listed no default; its Notes now state that an absent or null value is stored as 0. 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 stage kind is rejected as an error (it fails the page, and because /pipelines is required, the sync) — not silently classified as open. 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, /calls rewritten 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.
  • /eventslead_id is hard-required. An event without a lead_id fails its page and thus the events entity. type is never null — an absent value becomes an empty string (truncated to 100 chars). value_before / value_after accept a scalar, object, or array: a scalar is wrapped as { "value": <scalar> }, an object or array is stored unchanged. The watermark prefers updated_at and falls back to created_at; updated_at is read for the watermark only and is not stored.
  • /tasks — association rule stated unambiguously. lead_id wins when both lead_id and contact_id are present; contact_id is used only when lead_id is absent; a task with neither is accepted but linked to nothing and is invisible in every report. A task’s own created_at is ignored entirely (no column) and is now listed under fields the adapter ignores.
  • /calls — stricter than /tasks, by design. A call with neither lead_id nor contact_id is rejected and fails its page — the opposite of a task, which tolerates it; the asymmetry is now called out on both pages. duration_seconds must be an integer; a non-numeric value fails the page — stricter than price, which tolerates a numeric string (a known inconsistency). direction accepts in or out; any other value, null, or an omitted value silently becomes out.
2026-07-24
Field-spec corrections and two endpoint pages brought to the reference standard.
  • /pipelinessort must be an integer. Both the pipeline-level and stage-level sort must 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 or null value still defaults to 0.
  • /pipelinesstages is now required and non-nullable. A missing or null stages is rejected as an error and fails the page (an intended contract; the adapter tolerates it today). An empty stages: [] stays valid — a genuinely empty funnel — but its leads cannot be classified won or lost.
  • /leadsid limit stated. The lead id is documented as max 255 chars, matching /pipelines; an over-length id fails its page. No behaviour change — the limit was always enforced by the shared identifier column.
  • /users and /contacts rewritten 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. /users is not incremental and drops phone; /contacts is incremental on updated_at and carries no deletion signal at all. custom_fields on a contact must be a JSON object — a non-object value is silently stored as null.
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), all GET, 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_since with a per-entity watermark; has_more paging; 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.
This release supersedes the earlier v0.9 draft. No prior published draft remains.
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.