> ## Documentation Index
> Fetch the complete documentation index at: https://canopy.wnstn.space/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Cano Analytics REST API Integration and Usage Guide

> Use the Cano Analytics REST API from any language or runtime. Covers authentication, event tracking, batching, pagination, and error handling over HTTP.

If there's no official SDK for your language or runtime, you can integrate with Cano Analytics directly over HTTP — the REST API is straightforward, follows standard conventions, and works from any environment that can make an HTTPS request. This guide walks you through authentication, tracking events, identifying users, batching, and handling errors.

## Base URL and Authentication

All API requests go to the following base URL. Every request must include an `Authorization` header with your API key as a Bearer token and a `Content-Type` of `application/json`.

```text theme={null}
Base URL:      https://api.canoanalytics.io/v1
Authorization: Bearer YOUR_API_KEY
Content-Type:  application/json
```

You can find your API key in the [Cano dashboard](https://app.canoanalytics.io) under **Settings → API Keys**.

<Warning>
  Keep your API key secret. Never expose it in client-side JavaScript, public repositories, or logs. Use environment variables or a secrets manager to inject it at runtime.
</Warning>

## Track an Event

Send a `POST` request to `/events` with an event name, a user identifier, and any properties you want to capture.

```bash track-event.sh theme={null}
curl -X POST https://api.canoanalytics.io/v1/events \
  -H "Authorization: Bearer cano_live_sk_1234567890" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "page_viewed",
    "user_id": "usr_123",
    "properties": {
      "page": "/pricing",
      "referrer": "https://google.com"
    }
  }'
```

**Expected response** (`201 Created`):

```json theme={null}
{
  "status": "queued",
  "event_id": "evt_01hx9zk2b3c4d5e6f7g8h9j0"
}
```

The same request in other languages:

<CodeGroup>
  ```go track-event.go theme={null}
  package main

  import (
  	"bytes"
  	"encoding/json"
  	"fmt"
  	"net/http"
  )

  func main() {
  	payload := map[string]any{
  		"event":   "page_viewed",
  		"user_id": "usr_123",
  		"properties": map[string]string{
  			"page":     "/pricing",
  			"referrer": "https://google.com",
  		},
  	}

  	body, _ := json.Marshal(payload)

  	req, _ := http.NewRequest("POST", "https://api.canoanalytics.io/v1/events", bytes.NewBuffer(body))
  	req.Header.Set("Authorization", "Bearer cano_live_sk_1234567890")
  	req.Header.Set("Content-Type", "application/json")

  	resp, err := http.DefaultClient.Do(req)
  	if err != nil {
  		panic(err)
  	}
  	defer resp.Body.Close()

  	fmt.Println("Status:", resp.Status)
  }
  ```

  ```ruby track-event.rb theme={null}
  require 'net/http'
  require 'json'
  require 'uri'

  uri = URI('https://api.canoanalytics.io/v1/events')

  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  request = Net::HTTP::Post.new(uri)
  request['Authorization'] = 'Bearer cano_live_sk_1234567890'
  request['Content-Type']   = 'application/json'
  request.body = {
    event:      'page_viewed',
    user_id:    'usr_123',
    properties: {
      page:     '/pricing',
      referrer: 'https://google.com'
    }
  }.to_json

  response = http.request(request)
  puts "Status: #{response.code}"
  puts response.body
  ```

  ```php track-event.php theme={null}
  <?php

  $payload = json_encode([
      'event'      => 'page_viewed',
      'user_id'    => 'usr_123',
      'properties' => [
          'page'     => '/pricing',
          'referrer' => 'https://google.com',
      ],
  ]);

  $ch = curl_init('https://api.canoanalytics.io/v1/events');
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_POST           => true,
      CURLOPT_POSTFIELDS     => $payload,
      CURLOPT_HTTPHEADER     => [
          'Authorization: Bearer cano_live_sk_1234567890',
          'Content-Type: application/json',
      ],
  ]);

  $response = curl_exec($ch);
  $status   = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  curl_close($ch);

  echo "Status: $status\n";
  echo $response . "\n";
  ```

  ```java track-event.java theme={null}
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;

  public class TrackEvent {
      public static void main(String[] args) throws Exception {
          String body = """
              {
                "event": "page_viewed",
                "user_id": "usr_123",
                "properties": {
                  "page": "/pricing",
                  "referrer": "https://google.com"
                }
              }
              """;

          HttpRequest request = HttpRequest.newBuilder()
              .uri(URI.create("https://api.canoanalytics.io/v1/events"))
              .header("Authorization", "Bearer cano_live_sk_1234567890")
              .header("Content-Type", "application/json")
              .POST(HttpRequest.BodyPublishers.ofString(body))
              .build();

          HttpResponse<String> response = HttpClient.newHttpClient()
              .send(request, HttpResponse.BodyHandlers.ofString());

          System.out.println("Status: " + response.statusCode());
          System.out.println(response.body());
      }
  }
  ```
</CodeGroup>

## Identify a User

Send a `POST` to `/identify` to associate a user ID with a set of traits. Call this after a user signs up or logs in so that all future events from that ID are enriched with their profile data.

```bash identify-user.sh theme={null}
curl -X POST https://api.canoanalytics.io/v1/identify \
  -H "Authorization: Bearer cano_live_sk_1234567890" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "usr_123",
    "traits": {
      "name": "Ada Lovelace",
      "email": "ada@example.com",
      "plan": "pro",
      "created_at": "2024-01-15T09:00:00Z"
    }
  }'
```

**Expected response** (`200 OK`):

```json theme={null}
{
  "status": "ok",
  "user_id": "usr_123"
}
```

## Batch Events

To reduce HTTP round-trips, send multiple events in a single request by posting an array to `/events/batch`. You can include up to **500 events** per batch.

```bash batch-events.sh theme={null}
curl -X POST https://api.canoanalytics.io/v1/events/batch \
  -H "Authorization: Bearer cano_live_sk_1234567890" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "event": "page_viewed",
        "user_id": "usr_123",
        "properties": { "page": "/dashboard" }
      },
      {
        "event": "button_clicked",
        "user_id": "usr_123",
        "properties": { "button": "export_csv" }
      },
      {
        "event": "export_completed",
        "user_id": "usr_123",
        "properties": { "format": "csv", "rows": 1420 }
      }
    ]
  }'
```

**Expected response** (`200 OK`):

```json theme={null}
{
  "status": "queued",
  "accepted": 3,
  "rejected": 0
}
```

## Pagination

Endpoints that return lists of records (such as `GET /events`) use **cursor-based pagination**. Each response includes a `next_cursor` field — pass its value as the `cursor` query parameter to fetch the next page. When `next_cursor` is `null`, you have reached the last page.

```bash pagination.sh theme={null}
# First page — returns up to 100 events
curl 'https://api.canoanalytics.io/v1/events?limit=100' \
  -H 'Authorization: Bearer cano_live_sk_1234567890'

# Next page — pass the cursor from the previous response
curl 'https://api.canoanalytics.io/v1/events?limit=100&cursor=eyJpZCI6ImV2dF8xMjMifQ==' \
  -H 'Authorization: Bearer cano_live_sk_1234567890'
```

A paginated response looks like this:

```json theme={null}
{
  "data": [ /* array of event objects */ ],
  "next_cursor": "eyJpZCI6ImV2dF80NTYifQ==",
  "has_more": true
}
```

<Note>
  Cursors are opaque, base64-encoded strings. Do not attempt to decode or construct them manually — always use the `next_cursor` value returned by the previous response.
</Note>

## Error Handling

The API returns standard HTTP status codes. All error responses include a JSON body with a machine-readable `error_code` and a human-readable `message`.

```json theme={null}
{
  "error_code": "invalid_api_key",
  "message": "The API key provided is invalid or has been revoked.",
  "docs_url": "https://docs.canoanalytics.io/errors/invalid_api_key"
}
```

| Status | Error Code         | Meaning                                                          | How to Handle                                                                                    |
| ------ | ------------------ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `400`  | `bad_request`      | Malformed JSON or missing required field                         | Fix the request payload before retrying                                                          |
| `401`  | `invalid_api_key`  | API key is missing, invalid, or revoked                          | Verify your key in the dashboard; do not retry automatically                                     |
| `403`  | `forbidden`        | Valid key but insufficient permissions for this operation        | Check your key's scope in the dashboard                                                          |
| `404`  | `not_found`        | Resource does not exist                                          | Verify the resource ID; do not retry                                                             |
| `422`  | `validation_error` | Payload is well-formed but fails validation (e.g. unknown field) | Inspect the `details` array in the response body for field-level errors                          |
| `429`  | `rate_limited`     | Too many requests — rate limit exceeded                          | Respect the `Retry-After` header and implement exponential backoff                               |
| `500`  | `server_error`     | Unexpected error on Cano's side                                  | Retry with exponential backoff; check [status.canoanalytics.io](https://status.canoanalytics.io) |

For `429` responses, the API includes a `Retry-After` header with the number of seconds to wait before retrying:

```bash theme={null}
HTTP/2 429
Retry-After: 15
Content-Type: application/json

{
  "error_code": "rate_limited",
  "message": "Rate limit exceeded. Try again in 15 seconds."
}
```

<Tip>
  If an official SDK exists for your language, use it instead of calling the REST API directly. The [JavaScript SDK](/docs/sdks/javascript) and [Python SDK](/docs/sdks/python) handle batching, automatic retries, exponential backoff, and persistent queuing out of the box — saving you from re-implementing that logic yourself.
</Tip>
