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

# Send events from any source

> Push emails, calls, CRM deals and social touches into Octave from n8n, Clay, Make, or your own code

Octave connects directly to the common GTM tools. When your source isn't one of
them — an in-house system, a tool we don't support yet, or an automation you
built in n8n, Clay or Make — the generic events API takes the same four kinds of
activity over plain HTTP.

Events sent this way behave exactly like events from a native integration: they
extract findings, match to your library, attach to deals, and feed outcomes.

## Getting your webhook URL

In Octave, go to **Settings → Integrations** and find the section for the kind of
event you want to send. At the bottom of each section is **"Don't see your
provider? Send events directly via the Octave API."** — click **Set up**.

The panel gives you your workspace API key and the webhook URL for that event
type, already carrying the key.

<Note>
  The URL includes your API key as `?api_key=…`. To send it as a header instead,
  drop the query string and pass `api_key`.
</Note>

## The four event types

Each type has its own endpoint. POST a JSON body matching the schema for that
type — see the [Log Event reference](/v2-api-reference/events/log-event) for
every field.

| Type   | Endpoint                                           | Use it for                             |
| ------ | -------------------------------------------------- | -------------------------------------- |
| Email  | `/api/v2/analytics/webhook/receive/generic/emails` | Sends, replies, opens, clicks, bounces |
| Call   | `/api/v2/analytics/webhook/receive/generic/calls`  | Transcripts and meeting records        |
| CRM    | `/api/v2/analytics/webhook/receive/generic/crm`    | Deals, opportunities, meetings booked  |
| Social | `/api/v2/analytics/webhook/receive/generic/social` | LinkedIn connections and messages      |

<Info>
  Only `sent` and `reply` email events, and `transcript` call events, are
  processed for analytics. The rest are accepted and stored, but do not produce
  findings.
</Info>

<h2 id="email-events">
  Email events
</h2>

```json theme={null}
{
  "eventTimestamp": "2024-02-03T10:30:00.000Z",
  "eventType": "sent",
  "subject": "Follow up on our conversation",
  "body": { "text": "Hi John, just wanted to follow up..." },
  "from": { "email": "sarah@company.com", "name": "Sarah Johnson" },
  "to": [{ "email": "john@acme.com", "name": "John Smith" }],
  "crmOpportunityId": "opp_12345"
}
```

Required: `eventTimestamp`, `eventType`, `subject`, `body`, `from`, `to`.

<h2 id="call-events">
  Call events
</h2>

Send the transcript as an array of speaker turns when you have them. It is
faster and more accurate than a flat string, which Octave has to diarize with an
LLM before it can attribute anything.

```json theme={null}
{
  "eventTimestamp": "2024-02-03T14:00:00.000Z",
  "eventType": "transcript",
  "title": "Discovery Call - Acme Corp",
  "transcript": [
    {
      "speaker": { "name": "Sarah Johnson", "email": "sarah@company.com", "role": "internal" },
      "text": "Thanks for making the time. What pushed you to look at this now?"
    },
    {
      "speaker": { "name": "John Smith", "email": "john@acme.com", "role": "external" },
      "text": "Our current process breaks down past about fifty reps."
    }
  ],
  "participants": [
    { "name": "Sarah Johnson", "email": "sarah@company.com", "role": "internal" },
    { "name": "John Smith", "email": "john@acme.com", "role": "external" }
  ],
  "crmOpportunityId": "opp_12345"
}
```

Required: `eventTimestamp`, `eventType`, `title`, `transcript`, `participants`.

<h2 id="crm-events">
  CRM events
</h2>

CRM events are different from the other three in one important way: a deal is
**one object that changes over time**, not a one-off action. Read the
[Keeping deals up to date](#keeping-deals-up-to-date) section below before you
build against this endpoint.

```json theme={null}
{
  "eventTimestamp": "2024-02-03T16:00:00.000Z",
  "eventType": "opportunity_created",
  "opportunityId": "opp_12345",
  "opportunityName": "Acme Corp - Enterprise Plan",
  "amount": "50000.00",
  "currency": "USD",
  "stage": "Qualification",
  "stageCategory": "open",
  "accountName": "Acme Corp",
  "crmAccountId": "acct_67890",
  "crmAccountDomain": "acme.com",
  "contactEmail": "john@acme.com",
  "contactEmails": ["john@acme.com", "priya@acme.com"],
  "ownerEmail": "sarah@company.com",
  "closeDate": "2024-03-31T00:00:00.000Z",
  "crmLastModifiedAt": "2024-02-03T16:00:00.000Z"
}
```

Required: `eventTimestamp`, `eventType`, `opportunityId`, `opportunityName`.

### Keeping deals up to date

Re-send a deal every time it changes, and **include `crmLastModifiedAt`**.

Octave dedupes on a per-event ID. For emails and calls that is straightforward,
because one send is one event. A deal is not: the same `opportunityId` comes
back again and again as the amount moves, the stage advances and the close date
slips. `crmLastModifiedAt` is what tells those pushes apart — without it, an
updated deal can look identical to the one before it and be discarded as a
duplicate, leaving your pipeline view frozen at the values it was first created
with.

If your system doesn't expose a last-modified timestamp, Octave falls back to
hashing the deal's mutable fields, so genuine edits still land. Sending the
timestamp is more reliable, and it is cheaper: the hash covers every field that
can change, so a field Octave cannot see a timestamp for still counts as a new
state. A last-modified timestamp that only moves on real edits keeps the event
stream tighter than a hash that reacts to any field at all.

<Note>
  Sending the same deal repeatedly is expected and safe. Octave records a deal's
  won, lost and created milestones once per opportunity, so re-sending a closed
  deal after an owner reassignment or a late-entered loss reason updates the deal
  record without adding a second "won" to your reporting.
</Note>

### Fields worth sending

These are optional, but each one measurably improves what Octave can do:

* **`contactEmails`** — every contact on the deal, not just the primary. Event
  linking matches on all of them, so this is the single biggest lever on how
  many emails and calls attach to the opportunity.
* **`crmAccountId`** and **`crmAccountDomain`** — matching on account *name*
  alone cannot separate two deals at companies with the same name, or the same
  company under two domains.
* **`stageCategory`** — without it the stage bucket is inferred from
  `eventType`, so a deal deep in procurement still reads as simply "open".
* **`lossReason`** — powers win/loss analysis on closed-lost deals.
* **`mergedOpportunityIds`** — when your system merges deals, list the ones
  merged *into* this deal. Octave folds their history in and tombstones them,
  so a merge doesn't leave duplicates counting toward pipeline.

<h2 id="social-events">
  Social events
</h2>

```json theme={null}
{
  "eventTimestamp": "2024-02-03T11:00:00.000Z",
  "eventType": "message_sent",
  "body": { "text": "Thanks for connecting, John." },
  "from": { "email": "sarah@company.com", "name": "Sarah Johnson" },
  "to": [{ "email": "john@acme.com", "name": "John Smith" }],
  "crmOpportunityId": "opp_12345"
}
```

Required: `eventTimestamp`, `eventType`, `body`, `from`, `to`.

## Connecting activity to deals

By default Octave works out which opportunity an email, call or social touch
belongs to by matching participants and company domains inside a time window.
That is a reasonable guess, and it is still a guess — it misses when the person
on the thread isn't on the deal, and it can attach to the wrong one when two
deals share a domain.

If you already know the deal, say so. Every email, call and social event accepts:

* **`crmOpportunityId`** — your system's deal ID
* **`crmAccountId`** / **`crmAccountDomain`** — for account-level association

When `crmOpportunityId` matches a deal Octave knows about, the event is linked
to it directly and the inference is skipped. An ID that doesn't match anything
falls back to the normal matching, so a stale ID never leaves an event
unlinked.

<Tip>
  Use the same IDs you send on your CRM events. They're matched against the deals
  in your workspace, however those deals arrived — so this works even if your
  deals sync from HubSpot or Salesforce while your activity comes through this
  API.
</Tip>

## Deduplication

Every event gets an ID. Send one yourself as `eventId` if you have a stable
identifier — it is the most predictable option, and lets you re-send safely.

If you don't, Octave derives one from the event's content. Re-sending identical
content is treated as a duplicate and ignored; changed content is a new event.

## Troubleshooting

**Events accepted but nothing appears.** Check the event type is one that gets
processed. Emails produce findings on `sent` and `reply`, calls on `transcript`,
and CRM on `opportunity_created`, `deal_won`, `deal_lost` and `meeting_booked`.
Other event types are stored but do not feed extraction.

**A deal shows only one "won" despite several sends.** That is deliberate. Each
milestone is recorded once per opportunity, so later sends update the deal
record without duplicating the milestone in reporting.

**Deal values look stale.** You are almost certainly re-sending deals without
`crmLastModifiedAt`. See [Keeping deals up to date](#keeping-deals-up-to-date).

**Activity isn't attaching to the right deal.** Send `crmOpportunityId` on the
email, call or social event rather than relying on participant matching.

**Nothing arrives at all.** Confirm the API key in the URL matches the workspace
you're looking at, and that you're POSTing to the endpoint for the right event
type.
