Skip to main content

Checkpoint Service API

The checkpoint service ingests AxonFlow telemetry pings under the v1 schema. Two ingestion routes — one public, one internal-only — produce a single typed table downstream, plus a lightweight public version endpoint. For the conceptual framing of the four emitter classes (sdk, plugin, platform, synthetic) and the privacy boundary, start with Telemetry.

RouteAuthAcceptsUsed by
POST /v1/pingNone — public, unauthenticatedtelemetry_type ∈ {sdk, plugin, platform}, or empty during the v1 migration window. Rejects synthetic, any caller-supplied occurred_at, and every server-side bridge field.All SDK, plugin, and platform clients in the wild.
POST /v1/ping/internalSigV4 (AWS IAM)telemetry_type=synthetic only. Honors caller-supplied occurred_at (RFC3339) and the bridge aggregate fields.AxonFlow-operated server-side bridges.
GET /v1/versionNone — public, unauthenticatedn/a (no body)Clients that only want the latest SDK versions without emitting a ping.

Both ingestion routes terminate at the checkpoint Lambda handler and produce records in the same downstream table, distinguished by telemetry_type (and, for server-side rows, generation_source). Any other method on these routes returns HTTP 405 {"error":"method not allowed"}; unknown paths return HTTP 404.


POST /v1/ping

Ingests heartbeat pings from SDKs, plugins, and platform binaries. Public, unauthenticated by design — the entire payload is anonymous and does not include credentials, prompts, tool arguments, policy data, or PII. The server stamps Timestamp from API Gateway's request context; client-supplied timestamps are rejected (see below).

Request body

JSON, maximum 64 KB (larger bodies are rejected with HTTP 413). The shape is shared across the three accepted emitter classes; telemetry_type discriminates which fields are required and which are rejected. See Telemetry for worked examples per class.

FieldTypeRequiredNotes
telemetry_typestringRequired on v1 emitters; empty accepted during the migration windowOne of sdk, plugin, platform. Rejected with HTTP 400 if synthetic (use the internal route) or if outside the closed set.
sdkstringClass-dependentRequired for sdk and plugin (must be in the corresponding allowlist); must be empty for platform.
sdk_versionstringClass-dependentSemver. Required for sdk and plugin; must be empty for platform.
componentstringRequired when telemetry_type=platform; must be empty otherwiseOne of agent, orchestrator.
deployment_modestringYesTopology classification (self_hosted / community_saas / unknown).
endpoint_typestringOptionalSDK-derived classification of the configured AxonFlow URL (localhost, private_network, remote, unknown). The raw URL is never sent. Unknown values are coerced to unknown server-side (no HTTP 400).
streamstringOptionalOne of "" (defaults to heartbeat), heartbeat, sandbox. community_saas_operational is rejected with HTTP 400 — it is reserved for a future ingestion path and may not originate from a client.
generation_sourcestringOptionalWriter-path classification. On this route only "" (defaults to heartbeat_telemetry) or heartbeat_telemetry are accepted; the community_saas_* values are reserved for the internal route. Unrecognized values are rejected with HTTP 400.
license_tierstringOptionalOne of Community, Evaluation, Professional, Enterprise, EnterprisePlus, unknown. Server-side normalization: case variants and known aliases (PlusEnterprisePlus, communityCommunity) collapse to canonical form; unrecognized non-empty values bucket as unknown (no HTTP 400). Empty passes through.
environment_classstringOptionalOne of lambda, ecs_fargate, ecs_ec2, kubernetes, container, bare_metal, unknown. Same fail-soft normalization as license_tier — unrecognized values bucket as unknown.
os, archstringYesRuntime environment.
runtime_versionstringYesLanguage / runtime version.
platform_versionstringRequired when telemetry_type=platform; optional otherwiseFor SDK/plugin pings: detected from AxonFlow's /health endpoint (best-effort). For platform pings: the binary's own version — a platform ping without it is rejected with HTTP 400.
org_idstringOptionalThe org identity from the deployed license, when present. Used server-side for internal-vs-external classification only.
featuresstring arrayOptionalPlugin hook configuration summary.
instance_idstringYesRandom per-machine identifier for de-duplication.
occurred_atstringRejected if non-nullRFC3339. Reserved for the internal route. Sending it on the public route fails validation with HTTP 400 to prevent timestamp spoofing of analytics rows by unauthenticated callers.
Server-side bridge fieldsRejected if setEvery server-side aggregate field (including tenant_id and emitter) is rejected with HTTP 400 on the public route — an unauthenticated caller cannot fabricate server-side bridge rows or claim an internal emitter identity.

Validation matrix

The handler runs the following checks in order; the first failure returns HTTP 400 with a JSON {"error": "..."} body (oversized bodies return 413 before any validation).

  1. generation_source is in the closed enum ("", heartbeat_telemetry, or one of the community_saas_* values). On this route the community_saas_* values are additionally rejected as internal-only.
  2. Public-route guards. occurred_at must be unset, telemetry_type must not be synthetic, and every server-side bridge field (see the request table) must be absent.
  3. telemetry_type is one of {sdk, plugin, platform, ""}.
  4. instance_id is non-empty.
  5. stream ∈ {"", heartbeat, sandbox}. Empty defaults to heartbeat.
  6. Class-specific guards. When telemetry_type=sdk, sdk must be a recognized language SDK (go, python, typescript, java, rust) and sdk_version is required. When telemetry_type=plugin, sdk must be a recognized plugin identifier (openclaw-plugin, claude-code-plugin, cursor-plugin, codex-plugin) and sdk_version is required. When telemetry_type=platform, sdk AND sdk_version must be empty, component must be one of {agent, orchestrator}, and platform_version is required. In every class except platform, component must be empty.
  7. Migration-window inference. If telemetry_type is empty, the class is inferred from sdk-field membership: in the plugin allowlist → plugin; in the language-SDK allowlist → sdk; otherwise the request is rejected. The migration window remains open under the v1 contract; a future release will tighten the validator so an empty telemetry_type becomes a hard reject once pre-v1 emitters have aged out.

license_tier, environment_class, and endpoint_type are intentionally not in this list — all three fail soft. Unrecognized non-empty values bucket as unknown server-side rather than producing HTTP 400, so a lagging or misconfigured emitter still lands its row and stays visible in analytics.

Response

200 OK with a PingResponse body:

{
"latest_version": "9.0.0",
"alerts": [
{
"level": "info",
"message": "..."
}
]
}

latest_version is the latest released version of the calling SDK's language (matched on the sdk field for go / python / typescript / java / rust); clients can use it to surface "you're running a stale version" advisories. For plugin and platform pings it is the empty string — plugins track their own release cadence. alerts is always present as an array of {level, message} objects (level ∈ {info, warning, critical}) and is empty when there is nothing to advise.

Note that the DynamoDB write is best-effort: a storage failure is logged server-side but still returns 200 OK to the client — telemetry ingestion never blocks the caller on backend persistence.

Errors

StatusCause
400Validation failure — see the validation matrix above. Body: {"error": "..."}.
405Method other than POST. Body: {"error": "method not allowed"}.
413Request body larger than 64 KB. Body: {"error": "request body too large"}.
429Rate-limit exceeded — API Gateway stage-level throttling (50 burst / 100 requests/s sustained across all callers, unchanged by v1).
5xxBackend error (e.g. response serialization failure) — the client should treat this as transient and retry on the next 7-day boundary; do not advance the stamp file.

Example — SDK ping

curl -s -X POST https://checkpoint.getaxonflow.com/v1/ping \
-H 'Content-Type: application/json' \
-d '{
"telemetry_type": "sdk",
"sdk": "python",
"sdk_version": "9.0.0",
"platform_version": "9.7.0",
"os": "linux",
"arch": "arm64",
"runtime_version": "python 3.12.1",
"deployment_mode": "self_hosted",
"endpoint_type": "private_network",
"stream": "heartbeat",
"instance_id": "f3a81c12-4b2e-4d31-a8f3-12c45d6e7f89"
}'

For plugin, platform, and synthetic payload examples, see Telemetry — worked example payloads.


POST /v1/ping/internal

The SigV4-authenticated counterpart to /v1/ping. Accepts only the synthetic emitter class — server-side bridge events derived from operational data already inherent to running the hosted service. The route exists to bridge those derived analytics into the same typed table as the heartbeat path, so a single read query can answer questions across both.

This route is internal but not secret — security comes from the IAM grant model, not from obscurity. The route name and shape are documented here so a privacy-conscious reader can understand the full surface; the IAM grant + role configuration that enables a particular bridge to call it lives in private operator runbooks and is not published.

What's different from /v1/ping

AspectPublic /v1/pingInternal /v1/ping/internal
AuthNoneAWS SigV4, AWS_IAM API Gateway authorizer
telemetry_type acceptedsdk / plugin / platform (or empty during migration)synthetic only — any other value is rejected
occurred_atRejected if non-nullHonored (RFC3339, format-validated), so backdated aggregates land at the original event time
streamWire-allowlist "" / heartbeat / sandbox (default heartbeat)Same wire-allowlist — community_saas_operational is rejected on both routes (reserved for a future ingestion path). Synthetic rows are distinguished by telemetry_type and generation_source, not by stream.
generation_source"" or heartbeat_telemetry onlyAlso accepts community_saas_weekly_aggregate, community_saas_ingest_lambda, community_saas_canary — identifies the server-side writer path
sdk / sdk_versionRequired for sdk/plugin classesRequired for synthetic rows too, except when generation_source is one of the community_saas_* values — then both may be empty (the honest representation when the hosted service has no client SDK info)
emitterRejectedAccepted — identifies the server-side writer binary
Bridge aggregate fieldsRejectedAccepted (server-side aggregate fields carried by AxonFlow-operated bridges)
instance_idRandom per-machine identifierBridge-stable identifier (e.g., axonflow-csaas-bridge)

Privacy boundary

Synthetic events are not user telemetry — they are derived from operational data already processed inherent to running the hosted service (try.getaxonflow.com). They do not breach the SDK / plugin / platform heartbeat opt-out because they do not originate from a user-controlled binary. Every synthetic row carries telemetry_type=synthetic (and, for the hosted-service writers, a community_saas_* generation_source), so downstream readers can filter them out of the heartbeat analytics dataset with a single predicate — the same discriminator the write-side validator enforces. See Telemetry — Stream and the privacy boundary.


GET /v1/version

Public, unauthenticated. Returns the latest released version of each language SDK without recording anything — a lightweight, cacheable alternative when a client wants version advisories but has telemetry disabled.

Response (200 OK):

{
"go": "9.0.0",
"python": "9.0.0",
"typescript": "9.0.0",
"java": "9.0.0",
"rust": "0.8.1"
}

Any method other than GET returns 405.


Throttling and quotas

AspectLimit
Burst rate50 requests / second
Sustained rate100 requests / second
QuotaNone — the route is heartbeat-cadence by design (one ping per environment per 7 days for SDK / plugin); no per-account daily cap applies

These limits are enforced at the API Gateway stage level (shared across all callers and all three routes, not per source IP) and are unchanged by the v1 schema rollout.


Versioning

The /v1/ping, /v1/ping/internal, and /v1/version routes are stable under the v1 contract. Field additions follow the additive-with-omitempty rule used elsewhere in AxonFlow's APIs — clients should tolerate unknown fields. Field removals or rename require a major route bump (/v2/ping); the migration window note above is internal to the v1 contract (it only affects when the validator tightens, not the route shape).

See also

  • Telemetry — conceptual reference, opt-out semantics, four worked example payloads
  • Privacy Policy — what AXONFLOW_TELEMETRY=off covers and the synthetic-vs-heartbeat separation
  • Deployment Mode Matrix — how deployment_mode aligns to the deployment topology

Operational Readiness Checklist

Before relying on this page in a production rollout, pair it with the core operations docs: