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

# Amplitude

> Stream Amplitude events into Greenflash through an HTTP event streaming destination — your existing instrumentation becomes the outcome layer for your AI conversations.

If you already track `purchase`, `upgrade`, `signup`, `churn`, or any other product event in Amplitude, you can pipe those events into Greenflash with one destination — no SDKs, no app code. Conversation analyses immediately get a downstream-outcome layer: which conversations led to revenue, which led to refunds, which churned silently.

## How it works

Amplitude has built-in **Event Streaming** destinations that POST the Amplitude HTTP V2 batch format (`{ events: [...] }`) to a URL of your choice. Greenflash gives you a per-product URL to point it at. The flow:

1. Your app sends an event to Amplitude (`amplitude.track('purchase', { revenue: 49 })` or equivalent).
2. Amplitude evaluates your destination's event filters and decides whether to forward.
3. Amplitude POSTs a batch of events (up to 1,000) to your Greenflash URL.
4. Greenflash authenticates the URL token, maps each event in the batch to a Greenflash business event, infers positive / negative / neutral influence, and attaches it to the product you connected.
5. The next conversation analysis Greenflash runs has those events as outcome context.

Your Amplitude instrumentation stays the source of truth. Greenflash sees a copy of the events your destinations already see.

## Before you start

* **A Greenflash product** to enrich with Amplitude events. Create one under **Settings → Products** if you don't have one.
* **Amplitude org admin or destination admin access** so you can create an event streaming destination.
* (Optional) A list of the events worth forwarding. Forwarding everything works but is rarely what you want — page views and exposure events are noise for Greenflash.

## Setup

### 1. Create the connection in Greenflash

1. Open **Settings → Connect → Events** in the Greenflash web app.
2. Click **Connect** on the Amplitude card.
3. Pick the product the stream should feed and click **Create source**.
4. Copy the destination URL. Greenflash shows it once. If you lose it, rotate the token from the connection row and update Amplitude.

<Tip>
  The URL is the secret. Store it in Amplitude and your secrets manager — anyone with the URL can write events into the connected product.
</Tip>

### 2. Add the destination in Amplitude

1. In Amplitude, open **Data → Destinations** and click **Add Destination**.
2. Choose **Event Stream** (HTTP API destination) — the one that posts JSON over HTTP.
3. Set the destination URL to the Greenflash URL. Use `POST` and `Content-Type: application/json`.
4. Under the destination's **Event filters**, select which events should stream. Allow only your business events (signup, activate, purchase, subscribe, refund, cancel) — Amplitude defaults to forwarding everything, so this is where you control volume and quality.
5. Save the destination. The connection row in Greenflash flips to **Active** on the first event.

<Note>
  Greenflash ignores the `api_key` in Amplitude's request body — your URL token is the only auth. You can leave Amplitude's destination key field empty or set it to any placeholder.
</Note>

### 3. Send a test event

Trigger a real event from your app, replay one from Amplitude's destination logs, or post the HTTP V2 batch shape directly to confirm the round trip:

```bash theme={"theme":{"light":"github-light","dark":"vesper"}}
curl -X POST "<YOUR_GREENFLASH_URL>" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "user_id": "user_test_123",
        "event_type": "purchase",
        "time": 1747569600000,
        "insert_id": "amp_test_001",
        "event_properties": { "plan": "pro" },
        "revenue": 49.0
      }
    ]
  }'
```

A successful request returns:

```json theme={"theme":{"light":"github-light","dark":"vesper"}}
{ "received": 1, "accepted": 1, "dropped": 0 }
```

If `received` is `0`, the payload didn't match the HTTP V2 shape — check that you're using `{ events: [...] }` with each event containing `event_type`.

## How events are mapped

Greenflash applies these defaults to every Amplitude event:

| Amplitude field                                                                     | Greenflash field                                          |
| ----------------------------------------------------------------------------------- | --------------------------------------------------------- |
| `event_type`                                                                        | `eventType`                                               |
| `user_id` (falls back to `device_id`)                                               | `externalUserId`                                          |
| `time` (ms since epoch)                                                             | `eventAt`                                                 |
| `insert_id`                                                                         | `insertId` (prefixed `amplitude:`) — used for idempotency |
| `revenue` (or `price * quantity` if no revenue)                                     | `value` (with `valueType: currency`)                      |
| `event_properties` + revenue fields (`revenue`, `price`, `quantity`, `revenueType`) | `properties`                                              |

### Identity hints (used for user & organization attribution)

Greenflash reads a handful of fields from `user_properties` and `groups` to attach the event to the right user and organization. Sending these means Greenflash can auto-create the user / org if it hasn't seen them yet, instead of leaving the event unattached.

| Amplitude field                                                                        | Greenflash field         |
| -------------------------------------------------------------------------------------- | ------------------------ |
| `user_properties.email`                                                                | `userHints.email`        |
| `user_properties.first_name` + `user_properties.last_name` (or `user_properties.name`) | `userHints.name`         |
| `groups.organization` or `groups.company` (or `user_properties.organization_id`)       | `externalOrganizationId` |
| `user_properties.organization_name` (or `user_properties.company`)                     | `organizationHints.name` |

## Attribution: how events tie to users, orgs, and conversations

**Users.** Events match an existing Greenflash user by `externalUserId` first, then by `email`. If no match exists *and* the event carries an `email`, Greenflash **upserts** a new user automatically using the `user_properties` above. Anonymous events (no email) keep `userId = null` rather than creating a row per device.

**Organizations.** Same idea, scoped to `externalOrganizationId`. Org upsert only runs when `groups.organization` (or equivalent) is present — never by name.

**Conversations.** Add `gf_conversation_id` to `event_properties` to attach the event to a specific Greenflash conversation. Otherwise, the event auto-attaches to the user's most-recent conversation in this product within the last **15 minutes**; if that doesn't match, `conversationId` stays `null`.

> Users and orgs auto-created from analytics carry `properties.created_from = 'analytics'` and `properties.source = 'amplitude'`. When you later identify them via the Greenflash SDK using the same `externalUserId`, the existing row is reused.

### Influence (positive / negative / neutral)

* **Positive** when `revenue > 0` (or `price * quantity > 0`), *or* the event name matches `signup`, `signed_up`, `upgrade`, `purchase`, `subscribe`, `convert`, `activate`, `complete`, or `order_completed`.
* **Negative** when `revenue < 0` (Amplitude's refund convention), *or* the event name matches `churn`, `cancel`, `unsubscribe`, `refund`, `downgrade`, `delete`, `failed`, or `error`.
* **Neutral** otherwise.

Matching is case-insensitive and works across `snake_case`, `kebab-case`, `PascalCase`, and `Title Case With Spaces`.

<Tip>
  **Refunds in Amplitude** are conventionally a normal event with `revenueType: "Refund"` and a *negative* `revenue` number. Make sure your destination forwards the top-level `revenue` field, not just `event_properties` — otherwise Greenflash can't infer the negative dollar value.
</Tip>

## Override the defaults

Add any of these *magic properties* inside the event's `event_properties` and Greenflash will respect them — no Greenflash-side configuration needed.

| Property                  | Effect                                                                                 |
| ------------------------- | -------------------------------------------------------------------------------------- |
| `gf_event_type`           | Override the Greenflash event name (the original Amplitude name stays in `properties`) |
| `gf_influence`            | Force `positive`, `negative`, or `neutral`                                             |
| `gf_quality_impact_score` | Numeric override from `-1.0` to `1.0` — fine-grained weight                            |
| `gf_value`                | Override the event value (string or number)                                            |
| `gf_value_type`           | One of `currency`, `numeric`, `text`, `boolean`                                        |
| `gf_conversation_id`      | Attach this event to a specific Greenflash conversation (`externalConversationId`)     |

Example: forward a custom milestone but tell Greenflash to treat it as a strongly positive outcome.

```js theme={"theme":{"light":"github-light","dark":"vesper"}}
amplitude.track('weekly_active_milestone', {
  weeks: 4,
  gf_influence: 'positive',
  gf_quality_impact_score: 0.8,
});
```

## Filtering: what to forward

**Filter on the Amplitude side, not inside Greenflash.** Amplitude's destination **Event filter** is more expressive than anything we'd reasonably build — property comparisons, segment-style conditions, transformations. We deliberately don't expose a denylist UI inside Greenflash: the only Greenflash-side knob is the magic-property overrides above, applied per event in `event_properties`.

The fewer noisy events you forward, the sharper your Greenflash analyses get. Use Amplitude's destination filter to allow only the events that represent real outcomes (signup, activate, purchase, subscribe, refund, cancel) — Amplitude defaults to forwarding everything, which usually isn't what you want.

<AccordionGroup>
  <Accordion title="SaaS conversion funnel" icon="dollar-sign">
    Forward `signup`, `trial_started`, `activated`, `subscription_started`, `subscription_upgraded`, `subscription_cancelled`. Greenflash infers influence from these names automatically.
  </Accordion>

  <Accordion title="E-commerce" icon="shopping-cart">
    Forward `purchase` and `refund` (or your equivalents). Always include the top-level `revenue` field so Greenflash can dollar-weight outcomes per conversation. Use a negative `revenue` with `revenueType: "Refund"` for refunds.
  </Accordion>

  <Accordion title="Product-led growth" icon="rocket">
    Forward activation milestones (`workspace_created`, `integration_connected`, `aha_moment_reached`). Add `gf_influence: 'positive'` if the event name doesn't already match Greenflash's defaults.
  </Accordion>

  <Accordion title="Hard-mute a stream temporarily" icon="circle-pause">
    Don't want to edit Amplitude's filters for a short outage or experiment? Pause the connection from its row in **Settings → Connect → Events**. The Greenflash receiver returns `204` while paused so Amplitude stops retrying. Re-enable to resume — no token rotation, no Amplitude change.
  </Accordion>
</AccordionGroup>

## Response codes & limits

| Code  | Meaning                                                                                                                 |
| ----- | ----------------------------------------------------------------------------------------------------------------------- |
| `200` | Batch received and processed. Body includes `received`, `accepted`, `dropped`.                                          |
| `204` | Connection is paused or disconnected in Greenflash. Returned deliberately so Amplitude stops retrying.                  |
| `400` | Payload didn't match the Amplitude HTTP V2 schema, or JSON was invalid.                                                 |
| `401` | URL token is invalid, the connection has been rotated, or the optional shared secret header is missing / wrong.         |
| `404` | Provider segment of the URL is wrong (e.g. you pointed Amplitude at the `/posthog/` URL).                               |
| `413` | More than **1,000 events** in a single batch. The whole batch is dropped — configure Amplitude to split larger batches. |
| `500` | Greenflash couldn't look up the connection. Safe to retry.                                                              |

## Security

The URL is path-authenticated: the token segment is generated per connection, stored only as a `scrypt` hash, and verified in constant time on every request. The `api_key` in Amplitude's request body is ignored.

For defense in depth, you can configure a **shared secret** alongside the URL token. The secret is communicated to Greenflash on every request via a custom HTTP header — there's no body field or query parameter.

1. In Amplitude, open your destination and go to **Custom Headers**.
2. Add a header with name `x-greenflash-secret` and value `<your-secret>` (any string you pick, 8+ characters).
3. Provide the same secret to Greenflash on the connection so we can match it.

Once configured, Greenflash rejects any request whose `x-greenflash-secret` header is missing or doesn't match with `401`.

**Rotating the URL** invalidates the previous token immediately. Events sent to the old URL return `401`. Update the URL on the Amplitude destination to resume delivery.

## Troubleshooting

<AccordionGroup>
  <Accordion title="Connection still says ''Awaiting first event''" icon="hourglass">
    * The destination is enabled and your events match its filter.
    * The URL has **no trailing slash**.
    * You're using the **Event Stream** (HTTP API) destination type, not a managed integration that may transform the payload to a non-V2 shape.
    * Check Amplitude's destination logs for non-2xx responses to see what Greenflash returned.
  </Accordion>

  <Accordion title="Refunds aren't showing up as negative" icon="rotate-ccw">
    Amplitude refunds use `revenueType: "Refund"` with a negative `revenue` number. Make sure your destination's payload includes the top-level `revenue` field — Greenflash reads it from there, not from `event_properties`.
  </Accordion>

  <Accordion title="Getting 413 errors" icon="package">
    The batch contained more than 1,000 events. Greenflash drops the entire batch above that cap. Reduce the destination's batch size in Amplitude.
  </Accordion>

  <Accordion title="Anonymous users showing as ''unknown''" icon="user">
    Amplitude events without `user_id` fall back to `device_id` for the Greenflash `externalUserId`. If neither is set, the event arrives unattributed. Make sure your destination forwards both fields.
  </Accordion>

  <Accordion title="Lost the URL" icon="key">
    Open the Amplitude row under **Settings → Connect → Events** and click **Rotate URL**. The old token is invalidated immediately. Paste the new URL into the Amplitude destination.
  </Accordion>

  <Accordion title="Duplicate events" icon="copy">
    Greenflash deduplicates on the `amplitude:<insert_id>` insert id. Amplitude SDKs set `insert_id` automatically; if you're posting events directly to the URL, include a stable `insert_id` per event.
  </Accordion>
</AccordionGroup>

## Next

<CardGroup cols={2}>
  <Card title="How Greenflash uses events" icon="activity" href="/features/events">
    What changes in your analyses once events start flowing.
  </Card>

  <Card title="Public Events API" icon="bolt" href="/features/public-api">
    Skip the webhook and post events directly if you want full control.
  </Card>
</CardGroup>
