# Log activity

Tell mySDR what you did or heard outside it: an email you sent, a reply, a bounce, a meeting, not interested, an out-of-office, an unsubscribe, a referral, or a note. It can be about a person (`lead`), a company (`company`, e.g. when you mailed its general address) or just an address (`email`). Someone not in the Leads or Companies table yet is added. Log every email you send, with `step`, `template` and `thread_id`, so POST /outreach/check knows what went out. Log your research steps too, as a `note` on the company with `source_url` (the careers page you counted, the annual accounts you read). A `note` without `lead`, `company` or `email` is your work log: a run you finished, a source you tried, a decision the user made. The user sees every line on the Activity tab. Send one activity, or `{ "activities": [...] }` with up to 100 in order; each is logged on its own. mySDR keeps your whole process, so the user never needs a spreadsheet next to it: the state of each company and person (status with a reason, next action, email and contact points, your own fields), every email, reply and bounce you log, the exclusions, and a check before every send. The user sees all of it in the Leads and Companies tabs.

What an activity does besides the log line:

- `email_sent` — with `text`, the email shows in the mySDR Inbox and is kept, so you can read it back (GET /outreach/activity?with_text=true); a new address becomes the person's email or a contact point of the company; a planned email (`next_action.email`) with the same `step` is cleared (logged without a `step`, one planned without a step).
- `email_replied` — stops LinkedIn for that person in the whole organization; a planned follow-up is cleared; with `lead_status` the row gets it (`later` with `until` = recontact on that day).
- `email_bounced` — the address gets the status `bounced` and goes on the exclusions; a planned follow-up is cleared.
- `meeting_booked` — stops LinkedIn; lead status `meeting`.
- `not_interested` — stops LinkedIn and blocks a new sequence; lead status `not_interested`; the next action is cleared.
- `out_of_office` — away until `until`; a next action before that day moves to it.
- `unsubscribed` — the address goes on the exclusions; stops LinkedIn; the next action is cleared.
- `referral` — `referral` (the person they sent you to) lands in Leads at the same company and in the same campaign (`referral_row` in the answer), with a line in their log. It counts as the referrer's answer: their planned follow-up is cleared, and POST /outreach/check does not block the person they referred you to over what already went to that company.
- `note` — only the log line. Without `lead`, `company` or `email` it is a line of your work log.

Someone not in the table yet is added with the address you give, also with `email_replied`, `not_interested` or `out_of_office`. Give their employer in `lead.company` and `lead.company_domain`; without a domain, the domain of a work address is used, so their emails count for that company in POST /outreach/check.

A step of your own that is none of these types (a call, a voicemail, a LinkedIn message you sent yourself, a visit) goes in `action`: a key you choose, such as `call` or `linkedin_message`, with `action_label` for how it reads (default from the key). Leave `type` out, or send `note`: an action is a note, so it stops nothing and changes no state, but the user can filter and count it on the Activity tab, and GET /outreach/activity filters on it. Use the same key every time.

`external_id` (your own id, e.g. the email message id) makes a repeated call count once: the same type about the same person or address. Another type or another person with the same `external_id` (a bounce that quotes the message id of your email) is logged on its own.

## Authentication

Send `Authorization: Bearer gtk_…`. Create the key under **Connect assistant → API keys**. Every `gtk_` key can call these routes, also while the Signals API is closed. A missing, invalid or revoked key is **401**.

### Body parameters

- **`type`** `string` — What happened. Without it, `action` is required (then it is a `note`). One of: `email_sent`, `email_replied`, `email_bounced`, `meeting_booked`, `not_interested`, `out_of_office`, `unsubscribed`, `referral`, `note`.
- **`action`** `string` — A step of your own: lowercase letters, digits and `_`, starting with a letter, up to 40 characters (`call`, `voicemail`, `linkedin_message`, `visit`). Only with `type` `note` or no `type`; not one of the types above.
- **`action_label`** `string` — How the action reads in the table, up to 60 characters (`Call`). Default from the key.
- **`lead`** `string | object` — The person: a row (`sav_…`, `per_…`), a LinkedIn URL or their address as a string; or `{ ref, lead_id, person_id, linkedin_url, name, first_name, last_name, company, company_domain, title, email }`.
- **`company`** `string | object` — Or the company: `sav_…`, `cmp_…`, its domain, page or name; or `{ ref, name, domain, linkedin_url }`. Not together with `lead`: a person's employer goes in `lead.company` and `lead.company_domain`.
- **`email`** `string` — The address you mailed or that wrote back. Alone it finds the person or company that has it.
- **`subject`** `string` — The subject, up to 200 characters.
- **`summary`** `string` — One or two sentences for the table, up to 1,000 characters.
- **`text`** `string` — `email_sent` and `email_replied`: the email itself (without the quoted history), up to 20,000 characters; it shows in the Inbox (an `email_sent` also on Templates, above the email you planned next), and GET /outreach/activity?with_text=true gives it back.
- **`occurred_at`** `string` — ISO time; default now. Never in the future.
- **`campaign_id`** `string` — The campaign it belongs to.
- **`external_id`** `string` — Your own id, to log it once (per type and person).
- **`step`** `integer` — Which step of your sequence, from 1 (the first email) to 20.
- **`template`** `string` — The name of your template, up to 80 characters.
- **`thread_id`** `string` — The email thread, so replies and follow-ups stay together.
- **`sentiment`** `string` — `email_replied`: how it reads. One of: `positive`, `neutral`, `negative`.
- **`until`** `string` — `out_of_office`: the day they are back. `email_replied` with `lead_status` `later`: the day to recontact.
- **`lead_status`** `string` — `email_replied`: what they said. One of: `interested`, `meeting`, `later`, `not_interested`.
- **`referral`** `object` — `referral`: `{ name, title, linkedin_url, email, note }` of the person they sent you to.
- **`source_url`** `string` — Where you found it or did it: an http(s) address of up to 1,000 characters (the careers page, the annual accounts). Another source, such as a file or "Stripe", goes in `summary`.

## Response

- **`id`** `string` · **required** — The log line.
- **`duplicate`** `boolean` · **required** — This activity (same `external_id`, type and person) was logged before; nothing changed. `id` and `row` are those of the earlier line.
- **`row`** `object | null` · **required** — The row it is about: `id`, `object`, `name`.
- **`added_to_table`** `boolean` · **required** — The row is new in Leads or Companies.
- **`stopped_linkedin_sequences`** `integer` · **required** — LinkedIn sequences that stopped.
- **`in_inbox`** `boolean` · **required** — The email shows in the Inbox.
- **`referral_row`** `object | null` · **required** — `referral`: the row of the person they sent you to (`id`, `name`), to mail or update.
- **`changes`** `string[]` · **required** — What changed on the row, in a few words.
- **`dashboard_url`** `string` · **required** — The table in mySDR; for a work log line or a list of activities, the Activity tab.
- **`logged / failed / results`** `integer / integer / object[]` — With `activities`: how many were logged, how many failed, and per item `index`, `ok` and the fields above or `error`.

## Credits

Free. This call never uses credits.

## Errors

- **400** `invalid_request` — An unknown field or type, no `type` and no `action`, an `action` with another type than `note` or one that is a type itself, `lead` and `company` together, nobody to log it about (only a `note` can be about nobody), `text` on a type that is not an email, `lead_status` without `email_replied`, a `source_url` that is not an http(s) address, an `email` that is not a bare address, or a value that is not valid.
- **404** `not_found` — The person, company or campaign is not in your organization, or no row has this address or its domain.
- **409** `invalid_state` — The key has no user behind it, so mySDR cannot record who did it.
- **401** `authentication_error` — The Authorization header is missing, or the key is unknown or revoked.
- **403** `permission_error` — API access is not part of your plan, or your account is suspended.
- **429** `rate_limit_error` — More than 60 requests per minute, or more than 3 at the same time, on one key. Wait for the Retry-After header.
- **503** `service_unavailable` — The API is temporarily unavailable. Retry later.
- **500** `internal_error` — Something went wrong on our side.

## Example

### cURL

```bash
curl -X POST "https://mysdr.app/api/v1/outreach/activity" \
  -H "Authorization: Bearer $MYSDR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "email_sent",
    "company": {
      "name": "Acme Logistics",
      "domain": "acme-logistics.example"
    },
    "email": "jobs@acme-logistics.example",
    "subject": "Your open roles",
    "text": "Hi team, …",
    "step": 1,
    "template": "step1_jobs_inbox",
    "thread_id": "thread-88",
    "external_id": "<msg-88@mail.example>"
  }'
```

### JavaScript

```javascript
const response = await fetch('https://mysdr.app/api/v1/outreach/activity', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.MYSDR_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "type": "email_sent",
    "company": {
      "name": "Acme Logistics",
      "domain": "acme-logistics.example"
    },
    "email": "jobs@acme-logistics.example",
    "subject": "Your open roles",
    "text": "Hi team, …",
    "step": 1,
    "template": "step1_jobs_inbox",
    "thread_id": "thread-88",
    "external_id": "<msg-88@mail.example>"
  })
});
const body = await response.json();
```

### Python

```python
import json
import os
import urllib.request

payload = {
  "type": "email_sent",
  "company": {
    "name": "Acme Logistics",
    "domain": "acme-logistics.example"
  },
  "email": "jobs@acme-logistics.example",
  "subject": "Your open roles",
  "text": "Hi team, …",
  "step": 1,
  "template": "step1_jobs_inbox",
  "thread_id": "thread-88",
  "external_id": "<msg-88@mail.example>"
}

request = urllib.request.Request(
    "https://mysdr.app/api/v1/outreach/activity",
    headers={
    "Authorization": f"Bearer {os.environ['MYSDR_API_KEY']}",
    "Content-Type": "application/json"
    },
    data=json.dumps(payload).encode(),
    method="POST",
)
with urllib.request.urlopen(request) as response:
    body = json.load(response)
```

### Example response

```json
{
  "id": "a71c2f40-8d2e-4b91-9a10-3c5e7d9f0b21",
  "duplicate": false,
  "row": {
    "id": "sav_2b9e8d10-7f4a-4a51-8c2e-5d1f0a3b6c77",
    "object": "company",
    "name": "Acme Logistics"
  },
  "added_to_table": true,
  "stopped_linkedin_sequences": 0,
  "in_inbox": true,
  "referral_row": null,
  "changes": [
    "contact point updated"
  ],
  "dashboard_url": "https://mysdr.app/en/gtm/companies"
}
```
