# Integrate without an SDK

> Send product events and read feature flags from any server over HTTP: two addresses, one key per purpose, stable IDs, envelope-preserving retries and an independent exposure.

Canonical page: https://anectico.com/docs/instrument/without-an-sdk/


Any server can integrate feature flags and product events with plain HTTP, whether its language has
no Anectico SDK, you do not want one, or the SDK path does not cover it yet. Your agent writes the
calls in your repository and runs your application. Anectico supplies the routes and the evidence
reads.

One recipe was tested: a small checkout service in Ruby 2.6.10 using only its standard library. Other
languages follow the same calls, but only this one was run, so say so when you hand it back. The code
is in [Runnable integration examples](/docs/instrument/integration-examples#the-ruby-example-no-sdk).

## Addresses and keys by purpose

Each address and key has one job. A key that reads cannot send, and a key that sends cannot read.

| Purpose | Address | Credential | Route |
| --- | --- | --- | --- |
| Flag decisions | The control address, for example `https://app.anectico.com` | A project-bound management key with only `flags:read`, in server secret configuration | `POST /api/v1/decide` |
| Events, identify, account and flag exposures | The capture address, for example `https://api.anectico.com` | A `server_capture` key with only `analytics:write` | `POST /api/v1/capture` |
| Existing OpenTelemetry telemetry | The capture address | A `native_ingest` key with only `ingest:write` | `/v1/traces`, `/v1/logs`, `/v1/metrics` |
| Verification and the agent's own work | The control address | The agent's management key, never held by the application | `POST /api/v1/instrumentation/verify` |

Send a key in `X-Anectico-API-Key`; `X-API-Key` and `Authorization: ApiKey` are also accepted. Read
both addresses and both keys from the application's configuration. Do not default an address, do not
fall back from one key to another, and refuse to start when a setting is missing or both keys are the
same string. The flags key reads every flag definition of the project, so it never goes to a browser,
a mobile app, a log or a repository. See [Native intake keys](/docs/instrument/native-intake-keys).

## Send an event

Choose the `message_id` and the `timestamp` before the first send, derived from something your test
controls. Put the account in the event's `$groups`. The identity comes from your server's own
session, never from the request body.

```json
{
  "events": [
    {
      "event": "checkout_completed",
      "distinct_id": "user_8842",
      "session_id": "sess_7d1c",
      "message_id": "run-42:checkout_completed",
      "timestamp": "2026-10-15T09:30:12.123Z",
      "properties": { "cart_value": 129, "$groups": { "company": "acme-corp" } }
    }
  ]
}
```

```bash
curl --fail-with-body -X POST "$ANECTICO_CAPTURE_URL/api/v1/capture" \
  -H "X-Anectico-API-Key: $ANECTICO_CAPTURE_API_KEY" \
  -H "Content-Type: application/json" \
  --data @event.json
```

`session_id` is optional; send it when your application owns the browsing session. Identifiers are
printable and at most 200 bytes.

## Identify, link, profile and account

| Moment | Event |
| --- | --- |
| Login | `{"event": "identify", "distinct_id": "user_8842", "anon_distinct_id": "visitor_19", "set": {"plan": "pro"}, "message_id": ..., "timestamp": ...}` links the visitor's earlier events to the person and updates the profile |
| Account | `{"event": "$groupidentify", "distinct_id": "user_8842", "group_type": "company", "group_key": "acme-corp", "message_id": ..., "timestamp": ...}` records the account |
| Profile fields | `set`, `set_once` and `unset` on an event update the canonical customer; `null` in `set` is a value, not a deletion |

`identify` and `$groupidentify` store no event row, so no verification can select them: their
outcome is the acknowledgement. See [Direct customer-property
updates](/docs/reference/rest-api#direct-customer-property-updates) and [Identify
customers](/docs/instrument/identity).

## Read the acknowledgement, and retry safely

HTTP 200 means a batch ledger came back, not that the event is stored. Read every item:

| Item | Meaning | Action |
| --- | --- | --- |
| `CAPTURE_DELIVERY_QUEUED` | Accepted for processing; not proof of storage or of a finished identity update | Never retry |
| `CAPTURE_DELIVERY_REJECTED` | Refused (`INVALID_EVENT` or `QUOTA_EXCEEDED`) | Final for that input; fix it, do not loop |
| `CAPTURE_DELIVERY_UNKNOWN`, HTTP 429 or 503, a timeout or a broken connection | Uncertain | Back off and resend the **same bytes**, with the same message ID, a bounded number of times |

Serialize the body once and resend those bytes. Reusing a message ID with different content refuses
that event, so never rebuild an event for a retry. A 2xx with a body you cannot read is uncertain, not
delivered. See [Capture acknowledgements](/docs/reference/rest-api#capture-acknowledgements).

## Flags: decide, serve, then record the exposure

1. `POST /api/v1/decide` to the control address with the flags key, the person's `distinct_id` and
   the explicit `person_properties` the flag's rules use. The answer carries `feature_flags`,
   `project_id`, `feature_flag_versions` and, per flag, `feature_flag_results` with the evaluation
   status, whether the value is a fallback, and the input context.
2. Serve the branch from the value. `false` from a successful decision is a value. A failed request,
   a non-200 answer or a missing flag is an unavailable decision: serve the safe default and send no
   exposure.
3. Independently, send a `$feature_flag_called` event to the capture address with the capture key.
   Copy the decision's own fields into the reserved properties, never compute a version, never write
   an unknown fallback as `false`, and leave absent context fields absent. The properties and the
   input binding the application computes are on [Feature-flag exposure
   verification](/docs/reference/flag-integration-verification).

Local evaluation from the snapshot is not covered over plain HTTP: it needs the SDK's frozen
bucketing and comparison rules.

## Existing OpenTelemetry telemetry

If you already export OTLP, keep it: point the exporter at the capture address with a `native_ingest`
key and set `anectico.distinct_id` or trusted W3C baggage at a boundary your server controls. See
[OpenTelemetry](/docs/instrument/opentelemetry). An OTLP attribute does not create or link an identity:
that is the `identify` event above. Checking traces and logs with the exact verification reads is
not covered yet.

## Prove it

The agent selects the samples whose delivery was queued and asks for the `product_events` and `flags`
jobs, as in [Integration job verification](/docs/reference/integration-job-verification). What a
verified sample does and does not establish, and the limits, are on [What integration verification
proves and what it does not](/docs/reference/integration-verification-limits).

## Other jobs over HTTP, and what is not offered

| Job | Where it is documented | Part of this verified workflow |
| --- | --- | --- |
| Survey delivery | [Surveys](/docs/investigate/surveys#through-rest-for-your-own-interface-or-another-platform) | Not yet |
| Experiment delivery | [Product experiments](/docs/investigate/product-experiments#delivery-an-application-key-only) | Not yet |
| Browser session replay behind a backend in any language | [Record session replay safely](/docs/instrument/session-replay) | Not yet |
| Native structural replay | [iOS, Android, React Native and Flutter setup](/docs/instrument/session-replay#native-structural-replay) | Outside these server examples; use the platform recorder or adapter |
| Replay over OTLP | Not offered | No |

Browser replay uses the JavaScript recorder. Native structural replay has its own format, consent,
privacy policy and upload credential. OTLP carries neither recording format.

## Next

- [Ask your agent to integrate Anectico](/docs/start/ask-your-agent-to-integrate)
- [Feature flags in your application](/docs/instrument/feature-flags)
- [Plan and prove product events](/docs/instrument/product-analytics)
