# Create a monitor

A standing keyword monitor. Creating it is free. Each later run charges per new mention it delivers. `schedule` `hourly` is `400`. A keyword is a string, or an object with `keyword` and the settings below. A keyword of more than one word is searched as that phrase. The search only uses the keywords. The settings are checked on the text of each post it found, before the relevance check, so they can only drop posts. A post becomes a mention when its text has an active keyword and it passes that keyword's settings. A post whose text has no active keyword is dropped, because LinkedIn also matches names. The word lists match whole words, in any case and with or without accents.

## Authentication

Send `Authorization: Bearer gtk_…`. Create the key under **Connect assistant → API keys**. A missing, invalid or revoked key is **401**. A valid key while this API is not enabled for your organization answers **403** `api_not_enabled`, with what the key can do instead. Every response includes `mySDR-Api-Version`, `mySDR-Request-Id` and `Cache-Control: no-store`, and a response to an accepted key also `X-RateLimit-*` (so not a 401, a 405, an OPTIONS or a 404 for an unknown path).

### Headers

- **`Idempotency-Key`** `string` — Optional. The same key and the same body replay the monitor that was created. The same key with a different body is `409` `idempotency_conflict`.

### Body parameters

- **`name`** `string` · **required** — 1 to 120 characters.
- **`keywords`** `array` · **required** — 1 to 10 keywords: strings, or objects with the fields below.
- **`keywords[].keyword`** `string` · **required** — The word or phrase, 2 to 100 characters.
- **`keywords[].context`** `string | null` — Up to 200 characters for the relevance check, such as what the keyword means for you.
- **`keywords[].tag`** `string | null` — A label for the keyword. One of: `own_brand`, `competitor`, `industry_term`.
- **`keywords[].exact_match`** `boolean` — Defaults to true: the keyword must stand on its own. A domain (`acme.com`), handle (`@acme`), hashtag (`#acme`, or `#AcmeAnalytics` for Acme Analytics) or possessive (`acme's`) counts. A plural (`acmes`) does not. A space and a hyphen between words count the same. `false`: the keyword may also sit inside a longer word (`acmes`, `support.acme.io`).
- **`keywords[].case_sensitive`** `boolean` — Defaults to false: any case, with or without accents. `true`: only as written (`RAG`, not `rag`). The word lists always ignore case.
- **`keywords[].also_include_any`** `string[]` — Up to 10 words. The post must also use at least one of them.
- **`keywords[].also_include_all`** `string[]` — Up to 10 words. The post must also use all of them.
- **`keywords[].exclude_words`** `string[]` — Up to 50 words. A post that uses one of them is dropped.
- **`keywords[].exclude_wildcards`** `string[]` — Up to 50 patterns. A post with a word that matches one is dropped. A pattern starts at the start of a word, and `*` stands for any characters within that word: `beta.*` drops `beta.2` and `beta.acme.io`.
- **`keywords[].exclude_authors`** `string[]` — Up to 100 LinkedIn profile URLs. Posts by these people are dropped.
- **`keywords[].paused`** `boolean` — Defaults to false. A paused keyword is not searched and counts for no post.
- **`schedule`** `string` — Defaults to `daily`. `hourly` is not available yet (`400`). One of: `daily`, `hourly`.
- **`min_relevance`** `string` — Defaults to `medium`. One of: `high`, `medium`, `low`.
- **`tags`** `string[]` — Tags to keep, such as `own_brand_mention`, `competitor_mention`, `buy_intent`.
- **`webhook_endpoint_id`** `null` — A monitor sends no webhooks: an id is `400`. Read new mentions with GET /mentions?monitor_id=… instead (free).
- **`max_credits_per_day`** `integer | null` — Daily credit cap for the runs, at least the price of one mention: a lower cap is `400`. A run also stops at the organization’s daily credit limit.

## Response

- **`id`** `string` · **required** — Monitor id, `mon_…`.
- **`object`** `string` · **required** — Always `monitor`. One of: `monitor`.
- **`name`** `string` · **required** — The name you gave it.
- **`status`** `string` · **required** — `active`, `paused`, or `deleted` after DELETE. One of: `active`, `paused`, `deleted`.
- **`schedule`** `string` · **required** — `daily`. `hourly` is not available yet. One of: `daily`, `hourly`.
- **`keywords`** `object[]` · **required** — Each keyword with all its settings, `exact_match` and `case_sensitive` included, and `paused`.
- **`min_relevance`** `string` · **required** — `high`, `medium` or `low`. One of: `high`, `medium`, `low`.
- **`max_credits_per_day`** `integer | null` — Daily cap, or null for none.
- **`credits_today`** `integer` · **required** — Credits this monitor spent today (Europe/Brussels calendar day).
- **`credits_charged`** `integer` · **required** — Credits charged so far.
- **`next_run_at`** `string | null` · **required** — When the next run starts; null while the monitor is paused or deleted.
- **`last_run`** `object | null` · **required** — What the last run did: `at`, `mentions_delivered`, `credits_charged` and `stopped_by`. `stopped_by` is null when the run read everything it found, or `max_credits_per_day` (the monitor’s cap), `daily_limit` (the organization’s daily limit), `balance` (the wallet), or `paused` or `deleted` (the monitor was paused or deleted during the run, which stops the charging). A run that a cap or the balance stopped before it read anything shows 0 mentions with that reason. Null before the first run.
- **`deleted_at`** `string | null` · **required** — When DELETE stopped the monitor.
- **`mentions_readable_until`** `string | null` · **required** — For a deleted monitor: until when it and its mentions stay readable (30 days after DELETE).

## Credits

Creating a monitor is free. A run charges 1 credit per new mention delivered. A run that finds nothing is free.

## Errors

- **400** `invalid_request` — A keyword is too short, or `schedule` is `hourly`.
- **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.
- **403** `api_not_enabled` — The key is valid, but the Signals API or the Company API is not enabled for your organization yet. The error says what this key can do.
- **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.
- **500** `internal_error` — Something went wrong on our side.

## Example

### cURL

```bash
curl -X POST "https://mysdr.app/api/v1/monitors" \
  -H "Authorization: Bearer $MYSDR_API_KEY" \
  -H "Idempotency-Key: idem-1" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme mentions",
    "keywords": [
      {
        "keyword": "Acme Analytics",
        "tag": "own_brand",
        "exact_match": true,
        "exclude_words": [
          "vacancy"
        ],
        "exclude_authors": [
          "https://www.linkedin.com/in/jane-example"
        ]
      }
    ],
    "max_credits_per_day": 50
  }'
```

### JavaScript

```javascript
const response = await fetch('https://mysdr.app/api/v1/monitors', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.MYSDR_API_KEY}`,
    'Idempotency-Key': 'idem-1',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "name": "Acme mentions",
    "keywords": [
      {
        "keyword": "Acme Analytics",
        "tag": "own_brand",
        "exact_match": true,
        "exclude_words": [
          "vacancy"
        ],
        "exclude_authors": [
          "https://www.linkedin.com/in/jane-example"
        ]
      }
    ],
    "max_credits_per_day": 50
  })
});
const body = await response.json();
```

### Python

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

payload = {
  "name": "Acme mentions",
  "keywords": [
    {
      "keyword": "Acme Analytics",
      "tag": "own_brand",
      "exact_match": True,
      "exclude_words": [
        "vacancy"
      ],
      "exclude_authors": [
        "https://www.linkedin.com/in/jane-example"
      ]
    }
  ],
  "max_credits_per_day": 50
}

request = urllib.request.Request(
    "https://mysdr.app/api/v1/monitors",
    headers={
    "Authorization": f"Bearer {os.environ['MYSDR_API_KEY']}",
    "Idempotency-Key": "idem-1",
    "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": "mon_01J9Z6P8C8V0K3W6T2N8R5M1XF",
  "object": "monitor",
  "name": "Acme mentions",
  "status": "active",
  "schedule": "daily",
  "credits_charged": 0
}
```
