slidecold 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- slidecold-0.1.0/.gitignore +12 -0
- slidecold-0.1.0/CHANGELOG.md +14 -0
- slidecold-0.1.0/LICENSE +21 -0
- slidecold-0.1.0/PKG-INFO +249 -0
- slidecold-0.1.0/README.md +220 -0
- slidecold-0.1.0/openapi.yaml +1916 -0
- slidecold-0.1.0/pyproject.toml +85 -0
- slidecold-0.1.0/scripts/generate.py +449 -0
- slidecold-0.1.0/src/slidecold/__init__.py +32 -0
- slidecold-0.1.0/src/slidecold/_client.py +235 -0
- slidecold-0.1.0/src/slidecold/_config.py +44 -0
- slidecold-0.1.0/src/slidecold/_exceptions.py +48 -0
- slidecold-0.1.0/src/slidecold/_files.py +21 -0
- slidecold-0.1.0/src/slidecold/_mcp.py +121 -0
- slidecold-0.1.0/src/slidecold/_operations.py +1387 -0
- slidecold-0.1.0/src/slidecold/_resources.py +1307 -0
- slidecold-0.1.0/src/slidecold/_version.py +1 -0
- slidecold-0.1.0/src/slidecold/_webhooks.py +12 -0
- slidecold-0.1.0/src/slidecold/cli.py +195 -0
- slidecold-0.1.0/src/slidecold/mcp_server.py +25 -0
- slidecold-0.1.0/src/slidecold/py.typed +0 -0
- slidecold-0.1.0/src/slidecold/types.py +565 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- `SlideCold` and `AsyncSlideCold` clients covering all 49 operations of the SlideCold API v1, grouped by
|
|
8
|
+
resource (`campaigns`, `audiences`, `conversations`, `accounts`, `proxies`, `folders`), generated from
|
|
9
|
+
`openapi.yaml`, with TypedDicts for models and request bodies.
|
|
10
|
+
- Typed errors (`AuthenticationError`, `PermissionDeniedError`, `NotFoundError`, `RateLimitError`, `APIError`,
|
|
11
|
+
`APIConnectionError`), GET retries with exponential backoff, `wait_for_login_job`.
|
|
12
|
+
- `verify_webhook_signature` for `X-SlideCold-Signature`.
|
|
13
|
+
- `slidecold` CLI with `login` / `logout` and one command per operation.
|
|
14
|
+
- `slidecold-mcp` stdio MCP server (`pip install 'slidecold[mcp]'`) with one tool per operation.
|
slidecold-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SlideCold
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
slidecold-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: slidecold
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK, CLI and MCP server for the SlideCold API (Instagram cold-DM outreach)
|
|
5
|
+
Project-URL: Homepage, https://slidecold.com
|
|
6
|
+
Project-URL: Documentation, https://slidecold.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/slidecold/slidecold-python
|
|
8
|
+
Project-URL: Changelog, https://github.com/slidecold/slidecold-python/blob/main/CHANGELOG.md
|
|
9
|
+
Author: SlideCold
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: cli,cold-dm,instagram,mcp,outreach,sdk,slidecold
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: httpx<1,>=0.23
|
|
26
|
+
Provides-Extra: mcp
|
|
27
|
+
Requires-Dist: mcp<3,>=2.1; (python_version >= '3.10') and extra == 'mcp'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# SlideCold Python SDK, CLI and MCP server
|
|
31
|
+
|
|
32
|
+
The official Python client for the [SlideCold](https://slidecold.com) API (Instagram cold-DM outreach).
|
|
33
|
+
One package gives you:
|
|
34
|
+
|
|
35
|
+
- **SDK**: `SlideCold` and `AsyncSlideCold` clients with typed methods for every API operation.
|
|
36
|
+
- **CLI**: `slidecold campaigns list`, `slidecold audiences export <id>`, and so on.
|
|
37
|
+
- **MCP server**: `slidecold-mcp`, which gives Claude, Cursor and other MCP clients one tool per API operation.
|
|
38
|
+
|
|
39
|
+
Everything is generated from the [OpenAPI spec](openapi.yaml), so the SDK, the CLI and the MCP tools always match the API.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install slidecold # SDK and CLI (Python 3.9+, the only dependency is httpx)
|
|
45
|
+
pip install 'slidecold[mcp]' # also the MCP server (Python 3.10+)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Create an API key at <https://slidecold.com/settings>. Keys look like `cs_...`.
|
|
49
|
+
|
|
50
|
+
## Authentication
|
|
51
|
+
|
|
52
|
+
The API key is resolved in this order:
|
|
53
|
+
|
|
54
|
+
1. the `api_key=` argument (or `--api-key` on the CLI)
|
|
55
|
+
2. the `SLIDECOLD_API_KEY` environment variable
|
|
56
|
+
3. the config file written by `slidecold login`: `~/.config/slidecold/config.json`
|
|
57
|
+
(`$XDG_CONFIG_HOME/slidecold/config.json` if set, `%APPDATA%\slidecold\config.json` on Windows)
|
|
58
|
+
|
|
59
|
+
The base URL follows the same order (`base_url=`, `SLIDECOLD_BASE_URL`, config `baseUrl`), then defaults to
|
|
60
|
+
`https://slidecold.com/api/v1`. The config file is `{"apiKey": "cs_...", "baseUrl": "..."}`, the same format
|
|
61
|
+
the SlideCold Node and Go CLIs use.
|
|
62
|
+
|
|
63
|
+
## SDK quickstart
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from slidecold import SlideCold
|
|
67
|
+
|
|
68
|
+
with SlideCold() as client: # or SlideCold(api_key="cs_...")
|
|
69
|
+
for campaign in client.campaigns.list():
|
|
70
|
+
print(campaign["id"], campaign["name"], campaign["status"])
|
|
71
|
+
|
|
72
|
+
campaign = client.campaigns.create(
|
|
73
|
+
name="Spring outreach",
|
|
74
|
+
audienceIds=["aud_123"],
|
|
75
|
+
accountIds=["acc_456"],
|
|
76
|
+
templates=[{"body": "Hey, quick question about your page"}],
|
|
77
|
+
)
|
|
78
|
+
print(client.campaigns.stats(campaign["id"]))
|
|
79
|
+
|
|
80
|
+
csv_text = client.audiences.export("aud_123") # str
|
|
81
|
+
members = client.audiences.list_members("aud_123", min_followers=1000, limit=50)
|
|
82
|
+
client.conversations.send_reply("conv_789", message="Thanks, sending details now")
|
|
83
|
+
client.conversations.send_photo("conv_789", file="offer.jpg")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Request bodies use the API's field names. Pass them as keyword arguments, as a dict, or both:
|
|
87
|
+
`client.campaigns.update(id, {"status": "ACTIVE"})` is the same as `client.campaigns.update(id, status="ACTIVE")`.
|
|
88
|
+
Path parameters are positional and query parameters are snake_case keyword arguments. Every method's
|
|
89
|
+
docstring lists the scope it needs. Responses are plain dicts and lists, typed with the TypedDicts in
|
|
90
|
+
`slidecold.types`.
|
|
91
|
+
|
|
92
|
+
### Async
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
import asyncio
|
|
96
|
+
from slidecold import AsyncSlideCold
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def main():
|
|
100
|
+
async with AsyncSlideCold() as client:
|
|
101
|
+
accounts, campaigns = await asyncio.gather(client.accounts.list(), client.campaigns.list())
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
asyncio.run(main())
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Instagram login jobs
|
|
108
|
+
|
|
109
|
+
Adding an account, re-logging in and session checks run in the background and return `{"jobId": ...}`.
|
|
110
|
+
`wait_for_login_job` polls `accounts.login_status` until the job finishes:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
job = client.accounts.create(username="mybrand", password="...", proxyId="prx_1")
|
|
114
|
+
status = client.wait_for_login_job(job["jobId"], interval=2, timeout=120)
|
|
115
|
+
if status["state"] == "failed":
|
|
116
|
+
print(status.get("error"))
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
It returns the final status (`completed`, `failed` or `unknown`) and raises `TimeoutError` after `timeout` seconds.
|
|
120
|
+
`AsyncSlideCold.wait_for_login_job` is the async version.
|
|
121
|
+
|
|
122
|
+
### Options
|
|
123
|
+
|
|
124
|
+
`SlideCold(api_key=None, base_url=None, timeout=30, max_retries=2)`. GET requests are retried on 429, 5xx and
|
|
125
|
+
connection errors with exponential backoff (honouring `Retry-After`). Other methods are never retried, so a
|
|
126
|
+
DM is never sent twice. `client.request("operationId", path=..., query=..., body=...)` calls any operation by
|
|
127
|
+
its OpenAPI `operationId`.
|
|
128
|
+
|
|
129
|
+
## Scopes
|
|
130
|
+
|
|
131
|
+
Each API key has scopes of the form `<resource>:<read|write>` for `accounts`, `audiences`, `campaigns` and
|
|
132
|
+
`conversations`. A `:write` scope also grants `:read`. Proxies use the `accounts` scopes. The scope for each
|
|
133
|
+
operation is shown in method docstrings, in `slidecold <resource> <action> --help`, and in each MCP tool's
|
|
134
|
+
description.
|
|
135
|
+
|
|
136
|
+
## Errors
|
|
137
|
+
|
|
138
|
+
All errors derive from `slidecold.SlideColdError` and carry `status`, `message` (the API's `error` string)
|
|
139
|
+
and `body` (the parsed response).
|
|
140
|
+
|
|
141
|
+
| Exception | When |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| `AuthenticationError` | 401: the key is missing or invalid |
|
|
144
|
+
| `PermissionDeniedError` | 403: the key lacks a scope (`This API key is missing the campaigns:write scope`) or a plan limit was hit |
|
|
145
|
+
| `NotFoundError` | 404 |
|
|
146
|
+
| `RateLimitError` | 429 |
|
|
147
|
+
| `APIError` | any other error status (base class of the four above) |
|
|
148
|
+
| `APIConnectionError` | no response: DNS, connection or timeout |
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from slidecold import PermissionDeniedError
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
client.campaigns.delete("cmp_1")
|
|
155
|
+
except PermissionDeniedError as e:
|
|
156
|
+
print(e.status, e.message)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Webhooks
|
|
160
|
+
|
|
161
|
+
SlideCold signs each webhook with an `X-SlideCold-Signature` header: the hex HMAC-SHA256 of the raw JSON
|
|
162
|
+
body, keyed with the endpoint secret (`whsec_...`). Verify it against the raw bytes, before parsing:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from slidecold import verify_webhook_signature
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@app.post("/webhooks/slidecold") # Flask
|
|
169
|
+
def slidecold_webhook():
|
|
170
|
+
if not verify_webhook_signature(
|
|
171
|
+
request.get_data(), request.headers.get("X-SlideCold-Signature", ""), WHSEC
|
|
172
|
+
):
|
|
173
|
+
abort(401)
|
|
174
|
+
event = request.get_json()
|
|
175
|
+
...
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## CLI
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
slidecold login # prompts for the key, checks it, saves it (0600)
|
|
182
|
+
slidecold login --key cs_... # non-interactive
|
|
183
|
+
slidecold logout
|
|
184
|
+
|
|
185
|
+
slidecold campaigns list
|
|
186
|
+
slidecold campaigns list --archived true
|
|
187
|
+
slidecold campaigns get cmp_123
|
|
188
|
+
slidecold campaigns stats cmp_123
|
|
189
|
+
slidecold campaigns create --data '{"name": "Spring", "audienceIds": ["aud_1"], "accountIds": ["acc_1"], "templates": [{"body": "Hi!"}]}'
|
|
190
|
+
slidecold campaigns update cmp_123 --data @patch.json
|
|
191
|
+
slidecold audiences list-members aud_1 --min-followers 1000 --limit 50
|
|
192
|
+
slidecold audiences export aud_1 > members.csv
|
|
193
|
+
slidecold audiences import-members aud_1 --file members.csv
|
|
194
|
+
slidecold conversations send-photo conv_1 --file offer.jpg
|
|
195
|
+
slidecold accounts login-status --job-id job_1
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The shape is `slidecold <resource> <action> [ids] [--param value] [--data JSON | --data @file.json] [--file path]`.
|
|
199
|
+
Run `slidecold --help`, `slidecold campaigns --help` or `slidecold campaigns create --help` (which shows the
|
|
200
|
+
required scope). Output is pretty-printed JSON, or raw CSV for exports. Errors go to stderr with exit code 1.
|
|
201
|
+
`--api-key` and `--base-url` override the key and URL for one command.
|
|
202
|
+
|
|
203
|
+
## MCP server
|
|
204
|
+
|
|
205
|
+
`slidecold-mcp` is a stdio MCP server with one tool per API operation (`list_campaigns`,
|
|
206
|
+
`get_campaign_stats`, `send_conversation_reply`, ...). Read-only tools are marked `readOnlyHint`, and deletes
|
|
207
|
+
and DM sends are marked `destructiveHint`. Upload tools take a local `file_path`.
|
|
208
|
+
|
|
209
|
+
**Claude Code**
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
claude mcp add slidecold -e SLIDECOLD_API_KEY=cs_... -- uvx --from 'slidecold[mcp]' slidecold-mcp
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Claude Desktop** (`claude_desktop_config.json`)
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"mcpServers": {
|
|
220
|
+
"slidecold": {
|
|
221
|
+
"command": "uvx",
|
|
222
|
+
"args": ["--from", "slidecold[mcp]", "slidecold-mcp"],
|
|
223
|
+
"env": { "SLIDECOLD_API_KEY": "cs_..." }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Cursor** (`~/.cursor/mcp.json` or `.cursor/mcp.json` in a project) uses the same `mcpServers` block as
|
|
230
|
+
Claude Desktop.
|
|
231
|
+
|
|
232
|
+
If you ran `slidecold login`, you can leave out `SLIDECOLD_API_KEY` and the server will use the saved key.
|
|
233
|
+
Give the server a key with only the scopes you want the assistant to have, for example `campaigns:read` and
|
|
234
|
+
`conversations:read` for a read-only assistant.
|
|
235
|
+
|
|
236
|
+
## Development
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
uv sync --all-extras
|
|
240
|
+
uv run python scripts/generate.py # regenerate from openapi.yaml
|
|
241
|
+
uv run ruff check . && uv run ruff format --check . && uv run mypy && uv run pytest
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`src/slidecold/_operations.py`, `_resources.py` and `types.py` are generated from `openapi.yaml` and committed.
|
|
245
|
+
CI fails if they are stale.
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
MIT
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# SlideCold Python SDK, CLI and MCP server
|
|
2
|
+
|
|
3
|
+
The official Python client for the [SlideCold](https://slidecold.com) API (Instagram cold-DM outreach).
|
|
4
|
+
One package gives you:
|
|
5
|
+
|
|
6
|
+
- **SDK**: `SlideCold` and `AsyncSlideCold` clients with typed methods for every API operation.
|
|
7
|
+
- **CLI**: `slidecold campaigns list`, `slidecold audiences export <id>`, and so on.
|
|
8
|
+
- **MCP server**: `slidecold-mcp`, which gives Claude, Cursor and other MCP clients one tool per API operation.
|
|
9
|
+
|
|
10
|
+
Everything is generated from the [OpenAPI spec](openapi.yaml), so the SDK, the CLI and the MCP tools always match the API.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install slidecold # SDK and CLI (Python 3.9+, the only dependency is httpx)
|
|
16
|
+
pip install 'slidecold[mcp]' # also the MCP server (Python 3.10+)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Create an API key at <https://slidecold.com/settings>. Keys look like `cs_...`.
|
|
20
|
+
|
|
21
|
+
## Authentication
|
|
22
|
+
|
|
23
|
+
The API key is resolved in this order:
|
|
24
|
+
|
|
25
|
+
1. the `api_key=` argument (or `--api-key` on the CLI)
|
|
26
|
+
2. the `SLIDECOLD_API_KEY` environment variable
|
|
27
|
+
3. the config file written by `slidecold login`: `~/.config/slidecold/config.json`
|
|
28
|
+
(`$XDG_CONFIG_HOME/slidecold/config.json` if set, `%APPDATA%\slidecold\config.json` on Windows)
|
|
29
|
+
|
|
30
|
+
The base URL follows the same order (`base_url=`, `SLIDECOLD_BASE_URL`, config `baseUrl`), then defaults to
|
|
31
|
+
`https://slidecold.com/api/v1`. The config file is `{"apiKey": "cs_...", "baseUrl": "..."}`, the same format
|
|
32
|
+
the SlideCold Node and Go CLIs use.
|
|
33
|
+
|
|
34
|
+
## SDK quickstart
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from slidecold import SlideCold
|
|
38
|
+
|
|
39
|
+
with SlideCold() as client: # or SlideCold(api_key="cs_...")
|
|
40
|
+
for campaign in client.campaigns.list():
|
|
41
|
+
print(campaign["id"], campaign["name"], campaign["status"])
|
|
42
|
+
|
|
43
|
+
campaign = client.campaigns.create(
|
|
44
|
+
name="Spring outreach",
|
|
45
|
+
audienceIds=["aud_123"],
|
|
46
|
+
accountIds=["acc_456"],
|
|
47
|
+
templates=[{"body": "Hey, quick question about your page"}],
|
|
48
|
+
)
|
|
49
|
+
print(client.campaigns.stats(campaign["id"]))
|
|
50
|
+
|
|
51
|
+
csv_text = client.audiences.export("aud_123") # str
|
|
52
|
+
members = client.audiences.list_members("aud_123", min_followers=1000, limit=50)
|
|
53
|
+
client.conversations.send_reply("conv_789", message="Thanks, sending details now")
|
|
54
|
+
client.conversations.send_photo("conv_789", file="offer.jpg")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Request bodies use the API's field names. Pass them as keyword arguments, as a dict, or both:
|
|
58
|
+
`client.campaigns.update(id, {"status": "ACTIVE"})` is the same as `client.campaigns.update(id, status="ACTIVE")`.
|
|
59
|
+
Path parameters are positional and query parameters are snake_case keyword arguments. Every method's
|
|
60
|
+
docstring lists the scope it needs. Responses are plain dicts and lists, typed with the TypedDicts in
|
|
61
|
+
`slidecold.types`.
|
|
62
|
+
|
|
63
|
+
### Async
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import asyncio
|
|
67
|
+
from slidecold import AsyncSlideCold
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def main():
|
|
71
|
+
async with AsyncSlideCold() as client:
|
|
72
|
+
accounts, campaigns = await asyncio.gather(client.accounts.list(), client.campaigns.list())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
asyncio.run(main())
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Instagram login jobs
|
|
79
|
+
|
|
80
|
+
Adding an account, re-logging in and session checks run in the background and return `{"jobId": ...}`.
|
|
81
|
+
`wait_for_login_job` polls `accounts.login_status` until the job finishes:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
job = client.accounts.create(username="mybrand", password="...", proxyId="prx_1")
|
|
85
|
+
status = client.wait_for_login_job(job["jobId"], interval=2, timeout=120)
|
|
86
|
+
if status["state"] == "failed":
|
|
87
|
+
print(status.get("error"))
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
It returns the final status (`completed`, `failed` or `unknown`) and raises `TimeoutError` after `timeout` seconds.
|
|
91
|
+
`AsyncSlideCold.wait_for_login_job` is the async version.
|
|
92
|
+
|
|
93
|
+
### Options
|
|
94
|
+
|
|
95
|
+
`SlideCold(api_key=None, base_url=None, timeout=30, max_retries=2)`. GET requests are retried on 429, 5xx and
|
|
96
|
+
connection errors with exponential backoff (honouring `Retry-After`). Other methods are never retried, so a
|
|
97
|
+
DM is never sent twice. `client.request("operationId", path=..., query=..., body=...)` calls any operation by
|
|
98
|
+
its OpenAPI `operationId`.
|
|
99
|
+
|
|
100
|
+
## Scopes
|
|
101
|
+
|
|
102
|
+
Each API key has scopes of the form `<resource>:<read|write>` for `accounts`, `audiences`, `campaigns` and
|
|
103
|
+
`conversations`. A `:write` scope also grants `:read`. Proxies use the `accounts` scopes. The scope for each
|
|
104
|
+
operation is shown in method docstrings, in `slidecold <resource> <action> --help`, and in each MCP tool's
|
|
105
|
+
description.
|
|
106
|
+
|
|
107
|
+
## Errors
|
|
108
|
+
|
|
109
|
+
All errors derive from `slidecold.SlideColdError` and carry `status`, `message` (the API's `error` string)
|
|
110
|
+
and `body` (the parsed response).
|
|
111
|
+
|
|
112
|
+
| Exception | When |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| `AuthenticationError` | 401: the key is missing or invalid |
|
|
115
|
+
| `PermissionDeniedError` | 403: the key lacks a scope (`This API key is missing the campaigns:write scope`) or a plan limit was hit |
|
|
116
|
+
| `NotFoundError` | 404 |
|
|
117
|
+
| `RateLimitError` | 429 |
|
|
118
|
+
| `APIError` | any other error status (base class of the four above) |
|
|
119
|
+
| `APIConnectionError` | no response: DNS, connection or timeout |
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from slidecold import PermissionDeniedError
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
client.campaigns.delete("cmp_1")
|
|
126
|
+
except PermissionDeniedError as e:
|
|
127
|
+
print(e.status, e.message)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Webhooks
|
|
131
|
+
|
|
132
|
+
SlideCold signs each webhook with an `X-SlideCold-Signature` header: the hex HMAC-SHA256 of the raw JSON
|
|
133
|
+
body, keyed with the endpoint secret (`whsec_...`). Verify it against the raw bytes, before parsing:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from slidecold import verify_webhook_signature
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@app.post("/webhooks/slidecold") # Flask
|
|
140
|
+
def slidecold_webhook():
|
|
141
|
+
if not verify_webhook_signature(
|
|
142
|
+
request.get_data(), request.headers.get("X-SlideCold-Signature", ""), WHSEC
|
|
143
|
+
):
|
|
144
|
+
abort(401)
|
|
145
|
+
event = request.get_json()
|
|
146
|
+
...
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## CLI
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
slidecold login # prompts for the key, checks it, saves it (0600)
|
|
153
|
+
slidecold login --key cs_... # non-interactive
|
|
154
|
+
slidecold logout
|
|
155
|
+
|
|
156
|
+
slidecold campaigns list
|
|
157
|
+
slidecold campaigns list --archived true
|
|
158
|
+
slidecold campaigns get cmp_123
|
|
159
|
+
slidecold campaigns stats cmp_123
|
|
160
|
+
slidecold campaigns create --data '{"name": "Spring", "audienceIds": ["aud_1"], "accountIds": ["acc_1"], "templates": [{"body": "Hi!"}]}'
|
|
161
|
+
slidecold campaigns update cmp_123 --data @patch.json
|
|
162
|
+
slidecold audiences list-members aud_1 --min-followers 1000 --limit 50
|
|
163
|
+
slidecold audiences export aud_1 > members.csv
|
|
164
|
+
slidecold audiences import-members aud_1 --file members.csv
|
|
165
|
+
slidecold conversations send-photo conv_1 --file offer.jpg
|
|
166
|
+
slidecold accounts login-status --job-id job_1
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The shape is `slidecold <resource> <action> [ids] [--param value] [--data JSON | --data @file.json] [--file path]`.
|
|
170
|
+
Run `slidecold --help`, `slidecold campaigns --help` or `slidecold campaigns create --help` (which shows the
|
|
171
|
+
required scope). Output is pretty-printed JSON, or raw CSV for exports. Errors go to stderr with exit code 1.
|
|
172
|
+
`--api-key` and `--base-url` override the key and URL for one command.
|
|
173
|
+
|
|
174
|
+
## MCP server
|
|
175
|
+
|
|
176
|
+
`slidecold-mcp` is a stdio MCP server with one tool per API operation (`list_campaigns`,
|
|
177
|
+
`get_campaign_stats`, `send_conversation_reply`, ...). Read-only tools are marked `readOnlyHint`, and deletes
|
|
178
|
+
and DM sends are marked `destructiveHint`. Upload tools take a local `file_path`.
|
|
179
|
+
|
|
180
|
+
**Claude Code**
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
claude mcp add slidecold -e SLIDECOLD_API_KEY=cs_... -- uvx --from 'slidecold[mcp]' slidecold-mcp
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Claude Desktop** (`claude_desktop_config.json`)
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"mcpServers": {
|
|
191
|
+
"slidecold": {
|
|
192
|
+
"command": "uvx",
|
|
193
|
+
"args": ["--from", "slidecold[mcp]", "slidecold-mcp"],
|
|
194
|
+
"env": { "SLIDECOLD_API_KEY": "cs_..." }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Cursor** (`~/.cursor/mcp.json` or `.cursor/mcp.json` in a project) uses the same `mcpServers` block as
|
|
201
|
+
Claude Desktop.
|
|
202
|
+
|
|
203
|
+
If you ran `slidecold login`, you can leave out `SLIDECOLD_API_KEY` and the server will use the saved key.
|
|
204
|
+
Give the server a key with only the scopes you want the assistant to have, for example `campaigns:read` and
|
|
205
|
+
`conversations:read` for a read-only assistant.
|
|
206
|
+
|
|
207
|
+
## Development
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
uv sync --all-extras
|
|
211
|
+
uv run python scripts/generate.py # regenerate from openapi.yaml
|
|
212
|
+
uv run ruff check . && uv run ruff format --check . && uv run mypy && uv run pytest
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`src/slidecold/_operations.py`, `_resources.py` and `types.py` are generated from `openapi.yaml` and committed.
|
|
216
|
+
CI fails if they are stale.
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
MIT
|