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

# Add a connector

> Pull your own metrics into Tappify on a schedule and draw them on Tappify's charts.

A connector is a schedule and a list of metrics. Tappify calls your server on the cadence you
declare, stores the numbers it gets back, and draws them beside its own on the chart you name.

```bash theme={null}
tappify extension add connector --sync 1h
```

Leave the flag off and the command asks how often Tappify should pull. It writes the connector
with one example metric and a `server/metrics.ts` to fill in; the metric list, its units and its
breakdowns are yours to edit afterwards.

## The manifest entry

```json theme={null}
{
  "scopes": [{ "key": "ui:render" }, { "key": "metrics:write" }],
  "contributes": {
    "connector": {
      "sync": "1h",
      "metrics": [
        {
          "key": "active_users",
          "label": "Active users",
          "unit": "count",
          "kind": "gauge",
          "dimensions": [{ "key": "channel", "label": "Channel" }],
          "backfillDays": 30
        }
      ]
    },
    "series": [
      {
        "id": "active-users",
        "label": "Active users",
        "chart": "installs-activity",
        "metric": "active_users"
      }
    ]
  },
  "server": { "baseUrl": "https://funnel-lab.dev" }
}
```

The command writes this entry. You can hand-edit `tappify.extension.json` instead — the
`$schema` line gives your editor completion and validation, and
[`tappify extension doctor`](/extensions/test/doctor-checks) checks the result.

| Field          | Means                                                                                                               |
| -------------- | ------------------------------------------------------------------------------------------------------------------- |
| `sync`         | How often Tappify calls you: `5m`, `15m`, `1h`, `6h` or `24h`. The command offers the last four                     |
| `key`          | The name you return in the response, `snake_case`. Points are stored under it, so renaming one starts a new history |
| `unit`         | `count`, `ratio`, `currency`, `seconds` or `bytes`. The host formats by this                                        |
| `kind`         | `gauge` reports a level, `counter` reports how many happened in the bucket                                          |
| `dimensions`   | Breakdowns the host offers owners for this metric, next to its own. At most 10                                      |
| `backfillDays` | How far back your first sync may reach. At most 90                                                                  |

Declaring metrics needs the `metrics:write` scope. At most 50 metrics.

A `series` contribution is what puts the metric on a chart. Its `metric` must be one you declare
here, and its `chart` is one of the host charts in
[Pages and slots](/extensions/reference/pages-and-slots).

## The handler

`createTappifyHandler` routes `POST /tappify/metrics` to your `metrics` handler. The request
carries the window Tappify wants and the metric keys it is asking about.

```ts theme={null}
import { createTappifyHandler } from "@tappify/extension-sdk/server";

export default createTappifyHandler({
  extensionId: "funnel-lab",
  async metrics(request) {
    const { projectId, platform, metrics, from, to } = request.input;
    const rows = await countActiveUsers({ projectId, platform, from, to });

    return {
      series: [
        {
          metric: "active_users",
          unit: "count",
          dimensions: { channel: "organic" },
          points: rows.map((row) => [row.hour, row.value]),
        },
      ],
    };
  },
});
```

A point is `[ISO timestamp, number]`, and Tappify truncates each one to the hour it falls in
before storing it, so two points in the same hour under the same breakdown are one row. Return
one entry per breakdown: two `dimensions` values means two entries, each with its own `points`.

The window is not always your full backfill. A metric that already has points is asked for from
its newest bucket onward; one with none is asked for its declared `backfillDays`, and the whole
window is clamped to ninety days.

## What Tappify checks on every sync

| Check                                                          | What fails                                 |
| -------------------------------------------------------------- | ------------------------------------------ |
| The response is `{ "series": [...] }`                          | Anything else                              |
| Every `metric` is one you declared                             | An undeclared key                          |
| Every `unit` matches its declaration                           | A unit that disagrees                      |
| Every point is `[ISO timestamp, number]`                       | A string value, a missing timestamp, `NaN` |
| Every `dimensions` key is declared on that metric              | A breakdown the manifest does not name     |
| A `counter` never reports a negative                           | A negative in a counter                    |
| No point is older than `backfillDays` or later than the window | A point outside the window                 |

A response that fails any of these writes no points and the run is marked failed, with the first
three problems named on the run. Owners read the failed run, its attempts and your status code on
the extension's page in their workspace; your Runtime page counts the installs whose last sync
could not reach you.

## Retries, and when a metric goes stale

A failed pull is retried three times, waiting 30 seconds, then 2 minutes, then 10 minutes. All
four attempts are one run, so the run carries the attempt count; after the last one the run is
failed and the next scheduled sync starts fresh.

A metric with no successful sync in 24 hours is stale. Its points stay and the line is still
drawn, dashed, with `Last synced 3 days ago` beside it in the chart legend and the same reading
on the install's sync health panel.

## Owner credentials

When your metrics come from a service the owner pays for, declare what you need and Tappify
collects it on the install screen, stores it, and sends it with every call.

```json theme={null}
{
  "contributes": {
    "connector": {
      "sync": "6h",
      "auth": {
        "method": "api_key",
        "fields": [
          {
            "name": "api_key",
            "label": "API key",
            "type": "password",
            "required": true,
            "help": "Settings → Developers → API keys"
          }
        ]
      },
      "metrics": [
        { "key": "sessions", "label": "Sessions", "unit": "count", "kind": "counter" }
      ]
    }
  }
}
```

The values arrive as `request.credentials` on every call Tappify makes to you, keyed by each
field's `name`. They are never sent to a browser and never sent to anyone else. A field is
`text`, `password`, `textarea`, `file` or `select`; at most 20 of them.

For `"method": "oauth"`, add an `auth.oauth` block with `authorizationUrl`, `tokenUrl` and
`scopes` — that service's scopes, not Tappify's — plus fields named `client_id` and
`client_secret`. The owner registers your extension with their own provider, pastes those in, and
presses Connect on the extension's page.

The provider sends the owner back to Tappify, not to you, so the owner has to register this
redirect URI with the provider:

```
https://api.tappify.ai/api/v1/extensions/oauth/callback
```

Without it the provider refuses the handshake with `redirect_uri_mismatch`. Tappify shows the
address beside the Connect button on the install's page, and on your OAuth client page in the
portal so you can put it in your own instructions. On staging the host is the staging API origin
rather than `api.tappify.ai`; the path is the same.

## Health

Tappify calls `GET /tappify/health` every fifteen minutes and at every publish. Answer 200 within
three seconds. The last 90 days of those answers are the uptime number on your listing, and under
99.5 % the listing is flagged.

```ts theme={null}
export default createTappifyHandler({
  extensionId: "funnel-lab",
  health: () => ({ ok: true }),
  async metrics(request) {
    /* … */
  },
});
```
