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.
| Route | Auth | Accepts | Used by |
|---|---|---|---|
POST /v1/ping | None — public, unauthenticated | telemetry_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/internal | SigV4 (AWS IAM) | telemetry_type=synthetic only. Honors caller-supplied occurred_at (RFC3339) and the bridge aggregate fields. | AxonFlow-operated server-side bridges. |
GET /v1/version | None — public, unauthenticated | n/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.
| Field | Type | Required | Notes |
|---|---|---|---|
telemetry_type | string | Required on v1 emitters; empty accepted during the migration window | One of sdk, plugin, platform. Rejected with HTTP 400 if synthetic (use the internal route) or if outside the closed set. |
sdk | string | Class-dependent | Required for sdk and plugin (must be in the corresponding allowlist); must be empty for platform. |
sdk_version | string | Class-dependent | Semver. Required for sdk and plugin; must be empty for platform. |
component | string | Required when telemetry_type=platform; must be empty otherwise | One of agent, orchestrator. |
deployment_mode | string | Yes | Topology classification (self_hosted / community_saas / unknown). |
endpoint_type | string | Optional | SDK-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). |
stream | string | Optional | One 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_source | string | Optional | Writer-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_tier | string | Optional | One of Community, Evaluation, Professional, Enterprise, EnterprisePlus, unknown. Server-side normalization: case variants and known aliases (Plus → EnterprisePlus, community → Community) collapse to canonical form; unrecognized non-empty values bucket as unknown (no HTTP 400). Empty passes through. |
environment_class | string | Optional | One of lambda, ecs_fargate, ecs_ec2, kubernetes, container, bare_metal, unknown. Same fail-soft normalization as license_tier — unrecognized values bucket as unknown. |
os, arch | string | Yes | Runtime environment. |
runtime_version | string | Yes | Language / runtime version. |
platform_version | string | Required when telemetry_type=platform; optional otherwise | For 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_id | string | Optional | The org identity from the deployed license, when present. Used server-side for internal-vs-external classification only. |
features | string array | Optional | Plugin hook configuration summary. |
instance_id | string | Yes | Random per-machine identifier for de-duplication. |
occurred_at | string | Rejected if non-null | RFC3339. 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 fields | — | Rejected if set | Every 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).
generation_sourceis in the closed enum ("",heartbeat_telemetry, or one of thecommunity_saas_*values). On this route thecommunity_saas_*values are additionally rejected as internal-only.- Public-route guards.
occurred_atmust be unset,telemetry_typemust not besynthetic, and every server-side bridge field (see the request table) must be absent. telemetry_typeis one of {sdk,plugin,platform,""}.instance_idis non-empty.stream∈ {"",heartbeat,sandbox}. Empty defaults toheartbeat.- Class-specific guards. When
telemetry_type=sdk,sdkmust be a recognized language SDK (go,python,typescript,java,rust) andsdk_versionis required. Whentelemetry_type=plugin,sdkmust be a recognized plugin identifier (openclaw-plugin,claude-code-plugin,cursor-plugin,codex-plugin) andsdk_versionis required. Whentelemetry_type=platform,sdkANDsdk_versionmust be empty,componentmust be one of {agent,orchestrator}, andplatform_versionis required. In every class exceptplatform,componentmust be empty. - Migration-window inference. If
telemetry_typeis empty, the class is inferred fromsdk-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 emptytelemetry_typebecomes 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
| Status | Cause |
|---|---|
| 400 | Validation failure — see the validation matrix above. Body: {"error": "..."}. |
| 405 | Method other than POST. Body: {"error": "method not allowed"}. |
| 413 | Request body larger than 64 KB. Body: {"error": "request body too large"}. |
| 429 | Rate-limit exceeded — API Gateway stage-level throttling (50 burst / 100 requests/s sustained across all callers, unchanged by v1). |
| 5xx | Backend 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
| Aspect | Public /v1/ping | Internal /v1/ping/internal |
|---|---|---|
| Auth | None | AWS SigV4, AWS_IAM API Gateway authorizer |
telemetry_type accepted | sdk / plugin / platform (or empty during migration) | synthetic only — any other value is rejected |
occurred_at | Rejected if non-null | Honored (RFC3339, format-validated), so backdated aggregates land at the original event time |
stream | Wire-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 only | Also accepts community_saas_weekly_aggregate, community_saas_ingest_lambda, community_saas_canary — identifies the server-side writer path |
sdk / sdk_version | Required for sdk/plugin classes | Required 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) |
emitter | Rejected | Accepted — identifies the server-side writer binary |
| Bridge aggregate fields | Rejected | Accepted (server-side aggregate fields carried by AxonFlow-operated bridges) |
instance_id | Random per-machine identifier | Bridge-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
| Aspect | Limit |
|---|---|
| Burst rate | 50 requests / second |
| Sustained rate | 100 requests / second |
| Quota | None — 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=offcovers and the synthetic-vs-heartbeat separation - Deployment Mode Matrix — how
deployment_modealigns to the deployment topology
Operational Readiness Checklist
Before relying on this page in a production rollout, pair it with the core operations docs:
- Deployment Mode Matrix for self-hosted, Evaluation, Enterprise, SaaS, and In-VPC fit
- Failure Modes And Recovery for degraded-provider, connector, approval, and runtime behavior
- Capacity Planning for sizing and growth signals
- Community vs Evaluation vs Enterprise for limits, support surfaces, and upgrade triggers
