# Save companies to the Companies table

Puts companies in the user's Companies table, the way `save_companies` does on the assistant server. The employers of the people in the Leads table are there already, so save a company on its own when the company itself is the result (it raised funding, it uses a technology), or when you work per company (its status, its general address, your own fields). Give a `cmp_…` (from a search, POST /leads or POST /technologies/companies), or its name with `domain` or `linkedin_url`: for a company you found yourself, and for one from the Company API, which gives no id. Saving the same company again updates its row: what you send replaces the old value, what you leave out keeps it. The Leads and Companies tables are what the user sees in mySDR. Your agent decides what goes in them: search results do not land there by themselves. The user keeps the overview there; your agent reads the same tables here and fills them with POST.

Each company can carry its state in the same call. 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.

## 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

- **`companies`** `object[]` · **required** — 1 to 100 companies.
- **`companies[].id`** `string` — A `cmp_…` from mySDR. When given, the other fields are ignored except `note`.
- **`companies[].name`** `string` — Company name, for one you found yourself.
- **`companies[].domain`** `string` — Website domain.
- **`companies[].linkedin_url`** `string` — LinkedIn company page.
- **`companies[].industry`** `string` — Industry.
- **`companies[].country_code`** `string` — ISO 3166-1 alpha-2 of the headquarters.
- **`companies[].note`** `string` — Why you save this one, up to 500 characters.
- **`companies[].status`** `string` — `new` (in the work), `needs_decision` (waits for the user; say why in `status_reason`), `snoozed` (until `snooze_until`, then `new` again) or `archived` (out; say why). One of: `new`, `needs_decision`, `snoozed`, `archived`.
- **`companies[].status_reason`** `string | null` — Why, in a few words: "Below 50 FTE", "Rescan 23/11". Up to 500 characters.
- **`companies[].snooze_until`** `string | null` — The day the row comes back as `new`: a date (`2026-11-23`, 09:00 Brussels time) or an ISO time. Alone it snoozes the row.
- **`companies[].lead_status`** `string | null` — What they said. `not_interested` also stops LinkedIn for them and blocks a new sequence. One of: `interested`, `meeting`, `later`, `not_interested`.
- **`companies[].next_action`** `object | null` — Your next step and when: `at` (a date or ISO time), `kind` (`follow_up`, `recontact`, `rescan`, `approval`, `other`), `note`, and `email` when that step is an email you will send yourself: `{ "text", "subject", "step", "template", "to" }` (`text` required, up to 20,000 characters; `subject` up to 200 and `template` up to 80, as in POST /outreach/activity; `step` 1 to 20; `to` only if it is not the row’s address; `kind` defaults to `follow_up`). The user sees the planned email on Templates, with the emails you already logged above it, and GET /agenda gives it back on its day, so you keep no copy. A list (GET /tables/leads, /tables/companies) gives it without `text`; one row (GET /tables/leads/{id}, /tables/companies/{id}), the response of a change and GET /agenda give the text. Logging `email_sent` with the same `step` clears it (logged without a step, it clears one planned without a step), and so do a reply, a meeting, a bounce, an unsubscribe, a referral and `not_interested`; plan the next one. A new `next_action` replaces the current one and the email planned for it (`replaced` in the response gives them back); `null` clears both. GET /agenda lists the due ones.
- **`companies[].out_of_office_until`** `string | null` — Away until this day (from an out-of-office reply).
- **`companies[].contact_points`** `object[]` — Replaces the list of general addresses and contact forms: `{ "email": "jobs@acme.com", "label": "jobs", "source_url": "…", "email_status": "verified" }` or `{ "form": "https://acme.com/contact" }`. Up to 20.
- **`companies[].add_contact_points`** `object[]` — Adds or updates these (the same address or form is updated), keeping the others.
- **`companies[].fields`** `object` — Your own columns, merged with what is there: a name in lowercase letters, digits and `_` (`fte`, `ats`, `open_roles`) with a value (text, number, true/false) or `{ "value", "label", "source_url", "observed_at" }`; `null` removes one. Up to 40 per row. The user sees them as columns.
- **`companies[].campaign_id`** `string` — Put the row in this campaign (POST /outreach/campaigns, or one from a LinkedIn sequence).
- **`companies[].campaign_ids`** `string[]` — Replaces the campaigns of the row.
- **`companies[].stage`** `string | null` — One of the organization's own stages (GET /stages), by its key (`mql`) or its name ("MQL"); `null` clears it. A stage sits next to `status`: it stops nothing and changes no status. A change is logged.
- **`companies[].owner`** `string | null` — Who in the team follows the row up: `me` (the user behind the key), or the email address or user id of a colleague (GET /team); `null` clears it. A change is logged.

## Response

- **`added`** `integer` · **required** — New rows.
- **`updated`** `integer` · **required** — Rows that were there already. Saving again updates the row: what you send replaces the old value, what you leave out keeps it.
- **`skipped`** `integer` · **required** — Items that were not saved.
- **`saved`** `object[]` · **required** — Each saved item: `ref` (the id, LinkedIn URL or name you sent), `id` (what DELETE takes), `name` and `status` (`added` or `updated`).
- **`not_saved`** `object[]` · **required** — Each skipped item: `ref`, `code` (`invalid_ref`, `not_found`, `no_name`, `duplicate`, `table_full`) and `reason`.
- **`saved_by_assistant`** `integer` · **required** — How many rows of this kind agents saved in total.
- **`dashboard_url`** `string` · **required** — The table in mySDR.
- **`state`** `object[]` — With a state in the body: per saved row its `id` and `status` (`set`, `unchanged`, `not_ready` or `failed`, with a `reason`), and `replaced` when a new `next_action` replaced one (with its planned email).

## Credits

Free. This call never uses credits.

## Errors

- **400** `invalid_request` — No `companies`, more than 100, an unknown field (the error names it in `param`), a value that is too long, or a state that is not valid (`param` names 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/tables/companies" \
  -H "Authorization: Bearer $MYSDR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "companies": [
      {
        "id": "cmp_01J9Z6P8C8V0K3W6T2N8R5M1XC",
        "note": "Raised a Series A on 14 October."
      }
    ]
  }'
```

### JavaScript

```javascript
const response = await fetch('https://mysdr.app/api/v1/tables/companies', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.MYSDR_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "companies": [
      {
        "id": "cmp_01J9Z6P8C8V0K3W6T2N8R5M1XC",
        "note": "Raised a Series A on 14 October."
      }
    ]
  })
});
const body = await response.json();
```

### Python

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

payload = {
  "companies": [
    {
      "id": "cmp_01J9Z6P8C8V0K3W6T2N8R5M1XC",
      "note": "Raised a Series A on 14 October."
    }
  ]
}

request = urllib.request.Request(
    "https://mysdr.app/api/v1/tables/companies",
    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
{
  "added": 1,
  "updated": 0,
  "skipped": 0,
  "saved": [
    {
      "ref": "cmp_01J9Z6P8C8V0K3W6T2N8R5M1XC",
      "id": "cmp_01J9Z6P8C8V0K3W6T2N8R5M1XC",
      "name": "Acme Analytics",
      "status": "added"
    }
  ],
  "not_saved": [],
  "saved_by_assistant": 3,
  "dashboard_url": "https://mysdr.app/en/gtm/companies"
}
```
