upload-post 2.0.0__tar.gz → 2.1.1__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.
- {upload_post-2.0.0 → upload_post-2.1.1}/PKG-INFO +16 -2
- {upload_post-2.0.0 → upload_post-2.1.1}/README.md +15 -1
- {upload_post-2.0.0 → upload_post-2.1.1}/setup.py +1 -1
- {upload_post-2.0.0 → upload_post-2.1.1}/upload_post/__init__.py +1 -1
- {upload_post-2.0.0 → upload_post-2.1.1}/upload_post/api_client.py +427 -49
- {upload_post-2.0.0 → upload_post-2.1.1}/upload_post.egg-info/PKG-INFO +16 -2
- {upload_post-2.0.0 → upload_post-2.1.1}/setup.cfg +0 -0
- {upload_post-2.0.0 → upload_post-2.1.1}/upload_post/cli.py +0 -0
- {upload_post-2.0.0 → upload_post-2.1.1}/upload_post.egg-info/SOURCES.txt +0 -0
- {upload_post-2.0.0 → upload_post-2.1.1}/upload_post.egg-info/dependency_links.txt +0 -0
- {upload_post-2.0.0 → upload_post-2.1.1}/upload_post.egg-info/requires.txt +0 -0
- {upload_post-2.0.0 → upload_post-2.1.1}/upload_post.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: upload-post
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: Cross-platform social media upload for TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, and X (Twitter)
|
|
5
5
|
Home-page: https://www.upload-post.com/
|
|
6
6
|
Author: Upload-Post
|
|
@@ -162,6 +162,13 @@ status = client.get_status("request_id_from_upload")
|
|
|
162
162
|
print(status)
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
+
For scheduled or queued posts, check the status using the job_id:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
status = client.get_job_status("job_id_from_scheduled_post")
|
|
169
|
+
print(status)
|
|
170
|
+
```
|
|
171
|
+
|
|
165
172
|
### Get Upload History
|
|
166
173
|
|
|
167
174
|
```python
|
|
@@ -280,7 +287,8 @@ boards = client.get_pinterest_boards("my-profile")
|
|
|
280
287
|
### Facebook
|
|
281
288
|
- `facebook_page_id` - Facebook Page ID (required)
|
|
282
289
|
- `video_state` - PUBLISHED, DRAFT
|
|
283
|
-
- `facebook_media_type` - REELS, STORIES
|
|
290
|
+
- `facebook_media_type` - REELS, STORIES, or VIDEO (normal page video)
|
|
291
|
+
- `thumbnail_url` - Thumbnail URL for normal page videos (only when `facebook_media_type` is VIDEO)
|
|
284
292
|
- `facebook_link_url` - URL for text posts
|
|
285
293
|
|
|
286
294
|
### Pinterest
|
|
@@ -303,9 +311,12 @@ boards = client.get_pinterest_boards("my-profile")
|
|
|
303
311
|
- `share_with_followers` - Share community post with followers
|
|
304
312
|
- `card_uri` - Card URI for Twitter Cards
|
|
305
313
|
- `x_long_text_as_post` - Post long text as single post
|
|
314
|
+
- `x_thread_image_layout` - Comma-separated image layout for thread (e.g. "4,4" or "2,3,1"). Each value 1-4, total must equal image count. Auto-chunks into groups of 4 when >4 images.
|
|
306
315
|
|
|
307
316
|
### Threads
|
|
308
317
|
- `threads_long_text_as_post` - Post long text as single post (vs thread)
|
|
318
|
+
- `threads_thread_media_layout` - Comma-separated list of how many media items to include in each Threads post (e.g. "5,5" or "3,4,3"). Each value 1-10, total must equal media count. Auto-chunks into groups of 10 when >10 items.
|
|
319
|
+
- `threads_topic_tag` - Topic tag for the Threads post (1-50 characters, no periods or ampersands). One tag per post. Helps increase reach.
|
|
309
320
|
|
|
310
321
|
### Reddit
|
|
311
322
|
- `subreddit` - Subreddit name (without r/)
|
|
@@ -325,6 +336,7 @@ These options work across all upload methods:
|
|
|
325
336
|
| `scheduled_date` | ISO date for scheduling |
|
|
326
337
|
| `timezone` | Timezone for scheduled date |
|
|
327
338
|
| `add_to_queue` | Add to posting queue |
|
|
339
|
+
| `max_posts_per_slot` | Max posts per queue slot (overrides profile setting) |
|
|
328
340
|
| `async_upload` | Process asynchronously (default: True) |
|
|
329
341
|
|
|
330
342
|
## Error Handling
|
|
@@ -350,3 +362,5 @@ except UploadPostError as e:
|
|
|
350
362
|
## License
|
|
351
363
|
|
|
352
364
|
MIT
|
|
365
|
+
|
|
366
|
+
<!-- deployed 2026-03-16 17:49 UTC -->
|
|
@@ -124,6 +124,13 @@ status = client.get_status("request_id_from_upload")
|
|
|
124
124
|
print(status)
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
+
For scheduled or queued posts, check the status using the job_id:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
status = client.get_job_status("job_id_from_scheduled_post")
|
|
131
|
+
print(status)
|
|
132
|
+
```
|
|
133
|
+
|
|
127
134
|
### Get Upload History
|
|
128
135
|
|
|
129
136
|
```python
|
|
@@ -242,7 +249,8 @@ boards = client.get_pinterest_boards("my-profile")
|
|
|
242
249
|
### Facebook
|
|
243
250
|
- `facebook_page_id` - Facebook Page ID (required)
|
|
244
251
|
- `video_state` - PUBLISHED, DRAFT
|
|
245
|
-
- `facebook_media_type` - REELS, STORIES
|
|
252
|
+
- `facebook_media_type` - REELS, STORIES, or VIDEO (normal page video)
|
|
253
|
+
- `thumbnail_url` - Thumbnail URL for normal page videos (only when `facebook_media_type` is VIDEO)
|
|
246
254
|
- `facebook_link_url` - URL for text posts
|
|
247
255
|
|
|
248
256
|
### Pinterest
|
|
@@ -265,9 +273,12 @@ boards = client.get_pinterest_boards("my-profile")
|
|
|
265
273
|
- `share_with_followers` - Share community post with followers
|
|
266
274
|
- `card_uri` - Card URI for Twitter Cards
|
|
267
275
|
- `x_long_text_as_post` - Post long text as single post
|
|
276
|
+
- `x_thread_image_layout` - Comma-separated image layout for thread (e.g. "4,4" or "2,3,1"). Each value 1-4, total must equal image count. Auto-chunks into groups of 4 when >4 images.
|
|
268
277
|
|
|
269
278
|
### Threads
|
|
270
279
|
- `threads_long_text_as_post` - Post long text as single post (vs thread)
|
|
280
|
+
- `threads_thread_media_layout` - Comma-separated list of how many media items to include in each Threads post (e.g. "5,5" or "3,4,3"). Each value 1-10, total must equal media count. Auto-chunks into groups of 10 when >10 items.
|
|
281
|
+
- `threads_topic_tag` - Topic tag for the Threads post (1-50 characters, no periods or ampersands). One tag per post. Helps increase reach.
|
|
271
282
|
|
|
272
283
|
### Reddit
|
|
273
284
|
- `subreddit` - Subreddit name (without r/)
|
|
@@ -287,6 +298,7 @@ These options work across all upload methods:
|
|
|
287
298
|
| `scheduled_date` | ISO date for scheduling |
|
|
288
299
|
| `timezone` | Timezone for scheduled date |
|
|
289
300
|
| `add_to_queue` | Add to posting queue |
|
|
301
|
+
| `max_posts_per_slot` | Max posts per queue slot (overrides profile setting) |
|
|
290
302
|
| `async_upload` | Process asynchronously (default: True) |
|
|
291
303
|
|
|
292
304
|
## Error Handling
|
|
@@ -312,3 +324,5 @@ except UploadPostError as e:
|
|
|
312
324
|
## License
|
|
313
325
|
|
|
314
326
|
MIT
|
|
327
|
+
|
|
328
|
+
<!-- deployed 2026-03-16 17:49 UTC -->
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name="upload-post",
|
|
8
|
-
version="2.
|
|
8
|
+
version="2.1.1",
|
|
9
9
|
author="Upload-Post",
|
|
10
10
|
author_email="hi@img2html.com",
|
|
11
11
|
description="Cross-platform social media upload for TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, and X (Twitter)",
|
|
@@ -6,7 +6,7 @@ Facebook, Pinterest, Threads, Reddit, Bluesky, and X (Twitter).
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Dict, List, Union, Optional, Any
|
|
9
|
+
from typing import Dict, List, Union, Optional, Any
|
|
10
10
|
import requests
|
|
11
11
|
|
|
12
12
|
|
|
@@ -45,7 +45,8 @@ class UploadPostClient:
|
|
|
45
45
|
self.session = requests.Session()
|
|
46
46
|
self.session.headers.update({
|
|
47
47
|
"Authorization": f"Apikey {self.api_key}",
|
|
48
|
-
"User-Agent": "upload-post-python-client/2.0.0"
|
|
48
|
+
"User-Agent": "upload-post-python-client/2.0.0",
|
|
49
|
+
"X-Upload-Post-Source": "pip",
|
|
49
50
|
})
|
|
50
51
|
|
|
51
52
|
def _request(
|
|
@@ -85,7 +86,7 @@ class UploadPostClient:
|
|
|
85
86
|
try:
|
|
86
87
|
error_data = e.response.json()
|
|
87
88
|
error_msg = error_data.get('message') or error_data.get('detail') or str(error_data)
|
|
88
|
-
except:
|
|
89
|
+
except (ValueError, KeyError):
|
|
89
90
|
pass
|
|
90
91
|
raise UploadPostError(f"API request failed: {error_msg}") from e
|
|
91
92
|
|
|
@@ -93,22 +94,24 @@ class UploadPostClient:
|
|
|
93
94
|
self,
|
|
94
95
|
data: List[tuple],
|
|
95
96
|
user: str,
|
|
96
|
-
title: str,
|
|
97
|
+
title: Optional[str],
|
|
97
98
|
platforms: List[str],
|
|
98
99
|
first_comment: Optional[str] = None,
|
|
99
100
|
alt_text: Optional[str] = None,
|
|
100
101
|
scheduled_date: Optional[str] = None,
|
|
101
102
|
timezone: Optional[str] = None,
|
|
102
103
|
add_to_queue: Optional[bool] = None,
|
|
104
|
+
max_posts_per_slot: Optional[int] = None,
|
|
103
105
|
async_upload: Optional[bool] = None,
|
|
104
106
|
**kwargs
|
|
105
107
|
):
|
|
106
108
|
"""Add common upload parameters."""
|
|
107
109
|
data.append(("user", user))
|
|
108
|
-
|
|
110
|
+
if title:
|
|
111
|
+
data.append(("title", title))
|
|
109
112
|
for p in platforms:
|
|
110
113
|
data.append(("platform[]", p))
|
|
111
|
-
|
|
114
|
+
|
|
112
115
|
if first_comment:
|
|
113
116
|
data.append(("first_comment", first_comment))
|
|
114
117
|
if alt_text:
|
|
@@ -119,6 +122,8 @@ class UploadPostClient:
|
|
|
119
122
|
data.append(("timezone", timezone))
|
|
120
123
|
if add_to_queue is not None:
|
|
121
124
|
data.append(("add_to_queue", str(add_to_queue).lower()))
|
|
125
|
+
if max_posts_per_slot is not None:
|
|
126
|
+
data.append(("max_posts_per_slot", str(max_posts_per_slot)))
|
|
122
127
|
if async_upload is not None:
|
|
123
128
|
data.append(("async_upload", str(async_upload).lower()))
|
|
124
129
|
|
|
@@ -144,7 +149,7 @@ class UploadPostClient:
|
|
|
144
149
|
comment_overrides = [
|
|
145
150
|
"instagram_first_comment", "facebook_first_comment", "x_first_comment",
|
|
146
151
|
"threads_first_comment", "youtube_first_comment", "reddit_first_comment",
|
|
147
|
-
"bluesky_first_comment"
|
|
152
|
+
"bluesky_first_comment", "linkedin_first_comment"
|
|
148
153
|
]
|
|
149
154
|
for key in comment_overrides:
|
|
150
155
|
if kwargs.get(key):
|
|
@@ -178,7 +183,7 @@ class UploadPostClient:
|
|
|
178
183
|
if kwargs.get("photo_cover_index") is not None:
|
|
179
184
|
data.append(("photo_cover_index", str(kwargs["photo_cover_index"])))
|
|
180
185
|
|
|
181
|
-
def _add_instagram_params(self, data: List[tuple], is_video: bool = True, **kwargs):
|
|
186
|
+
def _add_instagram_params(self, data: List[tuple], is_video: bool = True, files: List[tuple] | None = None, **kwargs):
|
|
182
187
|
"""Add Instagram-specific parameters."""
|
|
183
188
|
if kwargs.get("media_type"):
|
|
184
189
|
data.append(("media_type", kwargs["media_type"]))
|
|
@@ -188,12 +193,20 @@ class UploadPostClient:
|
|
|
188
193
|
data.append(("user_tags", kwargs["user_tags"]))
|
|
189
194
|
if kwargs.get("location_id"):
|
|
190
195
|
data.append(("location_id", kwargs["location_id"]))
|
|
191
|
-
|
|
196
|
+
|
|
192
197
|
if is_video:
|
|
193
198
|
if kwargs.get("share_to_feed") is not None:
|
|
194
199
|
data.append(("share_to_feed", str(kwargs["share_to_feed"]).lower()))
|
|
195
200
|
if kwargs.get("cover_url"):
|
|
196
|
-
|
|
201
|
+
cover_val = str(kwargs["cover_url"])
|
|
202
|
+
if cover_val.lower().startswith(("http://", "https://")):
|
|
203
|
+
data.append(("cover_url", cover_val))
|
|
204
|
+
elif files is not None:
|
|
205
|
+
cover_path = Path(cover_val)
|
|
206
|
+
if cover_path.exists():
|
|
207
|
+
files.append(("cover_image", (cover_path.name, cover_path.open("rb"))))
|
|
208
|
+
else:
|
|
209
|
+
data.append(("cover_url", cover_val))
|
|
197
210
|
if kwargs.get("audio_name"):
|
|
198
211
|
data.append(("audio_name", kwargs["audio_name"]))
|
|
199
212
|
if kwargs.get("thumb_offset"):
|
|
@@ -236,12 +249,15 @@ class UploadPostClient:
|
|
|
236
249
|
if kwargs.get("recordingDate"):
|
|
237
250
|
data.append(("recordingDate", kwargs["recordingDate"]))
|
|
238
251
|
|
|
239
|
-
def _add_linkedin_params(self, data: List[tuple], **kwargs):
|
|
252
|
+
def _add_linkedin_params(self, data: List[tuple], is_text: bool = False, **kwargs):
|
|
240
253
|
"""Add LinkedIn-specific parameters."""
|
|
241
254
|
if kwargs.get("visibility"):
|
|
242
255
|
data.append(("visibility", kwargs["visibility"]))
|
|
243
256
|
if kwargs.get("target_linkedin_page_id"):
|
|
244
257
|
data.append(("target_linkedin_page_id", kwargs["target_linkedin_page_id"]))
|
|
258
|
+
if is_text and (kwargs.get("linkedin_link_url") or kwargs.get("link_url")):
|
|
259
|
+
link = kwargs.get("linkedin_link_url") or kwargs.get("link_url")
|
|
260
|
+
data.append(("linkedin_link_url", link))
|
|
245
261
|
|
|
246
262
|
def _add_facebook_params(self, data: List[tuple], is_video: bool = False, is_text: bool = False, **kwargs):
|
|
247
263
|
"""Add Facebook-specific parameters."""
|
|
@@ -253,7 +269,9 @@ class UploadPostClient:
|
|
|
253
269
|
data.append(("video_state", kwargs["video_state"]))
|
|
254
270
|
if kwargs.get("facebook_media_type"):
|
|
255
271
|
data.append(("facebook_media_type", kwargs["facebook_media_type"]))
|
|
256
|
-
|
|
272
|
+
if kwargs.get("thumbnail_url"):
|
|
273
|
+
data.append(("thumbnail_url", kwargs["thumbnail_url"]))
|
|
274
|
+
|
|
257
275
|
if is_text and kwargs.get("facebook_link_url"):
|
|
258
276
|
data.append(("facebook_link_url", kwargs["facebook_link_url"]))
|
|
259
277
|
|
|
@@ -307,6 +325,8 @@ class UploadPostClient:
|
|
|
307
325
|
data.append(("tagged_user_ids[]", uid))
|
|
308
326
|
if kwargs.get("place_id"):
|
|
309
327
|
data.append(("place_id", kwargs["place_id"]))
|
|
328
|
+
if kwargs.get("x_thread_image_layout"):
|
|
329
|
+
data.append(("x_thread_image_layout", kwargs["x_thread_image_layout"]))
|
|
310
330
|
else:
|
|
311
331
|
if kwargs.get("post_url"):
|
|
312
332
|
data.append(("post_url", kwargs["post_url"]))
|
|
@@ -328,20 +348,28 @@ class UploadPostClient:
|
|
|
328
348
|
"""Add Threads-specific parameters."""
|
|
329
349
|
if kwargs.get("threads_long_text_as_post") is not None:
|
|
330
350
|
data.append(("threads_long_text_as_post", str(kwargs["threads_long_text_as_post"]).lower()))
|
|
351
|
+
if kwargs.get("threads_thread_media_layout"):
|
|
352
|
+
data.append(("threads_thread_media_layout", kwargs["threads_thread_media_layout"]))
|
|
353
|
+
if kwargs.get("threads_topic_tag"):
|
|
354
|
+
data.append(("threads_topic_tag", kwargs["threads_topic_tag"]))
|
|
331
355
|
|
|
332
|
-
def _add_reddit_params(self, data: List[tuple], **kwargs):
|
|
356
|
+
def _add_reddit_params(self, data: List[tuple], is_text: bool = False, **kwargs):
|
|
333
357
|
"""Add Reddit-specific parameters."""
|
|
334
358
|
if kwargs.get("subreddit"):
|
|
335
359
|
data.append(("subreddit", kwargs["subreddit"]))
|
|
336
360
|
if kwargs.get("flair_id"):
|
|
337
361
|
data.append(("flair_id", kwargs["flair_id"]))
|
|
362
|
+
if is_text:
|
|
363
|
+
reddit_link = kwargs.get("reddit_link_url") or kwargs.get("link_url")
|
|
364
|
+
if reddit_link:
|
|
365
|
+
data.append(("reddit_link_url", reddit_link))
|
|
338
366
|
|
|
339
367
|
def upload_video(
|
|
340
368
|
self,
|
|
341
369
|
video_path: Union[str, Path],
|
|
342
|
-
title: str,
|
|
343
|
-
user: str,
|
|
344
|
-
platforms: List[str],
|
|
370
|
+
title: Optional[str] = None,
|
|
371
|
+
user: str = "",
|
|
372
|
+
platforms: Optional[List[str]] = None,
|
|
345
373
|
**kwargs
|
|
346
374
|
) -> Dict:
|
|
347
375
|
"""
|
|
@@ -349,9 +377,10 @@ class UploadPostClient:
|
|
|
349
377
|
|
|
350
378
|
Args:
|
|
351
379
|
video_path: Path to video file or video URL.
|
|
352
|
-
title: Video title/caption.
|
|
380
|
+
title: Video title/caption. Required for YouTube and Reddit.
|
|
381
|
+
Optional for TikTok, Instagram, Facebook, LinkedIn, X, Threads, Bluesky, Pinterest.
|
|
353
382
|
user: User identifier (profile name).
|
|
354
|
-
platforms: Target platforms. Supported: tiktok, instagram, youtube,
|
|
383
|
+
platforms: Target platforms. Supported: tiktok, instagram, youtube,
|
|
355
384
|
linkedin, facebook, pinterest, threads, bluesky, x
|
|
356
385
|
|
|
357
386
|
Keyword Args:
|
|
@@ -378,7 +407,7 @@ class UploadPostClient:
|
|
|
378
407
|
media_type: REELS or STORIES
|
|
379
408
|
share_to_feed: Share to feed
|
|
380
409
|
collaborators: Comma-separated collaborator usernames
|
|
381
|
-
cover_url: Custom cover URL
|
|
410
|
+
cover_url: Custom cover URL or file path. URLs are sent directly; file paths are uploaded as binary.
|
|
382
411
|
audio_name: Audio track name
|
|
383
412
|
user_tags: Comma-separated user tags
|
|
384
413
|
location_id: Location ID
|
|
@@ -408,7 +437,8 @@ class UploadPostClient:
|
|
|
408
437
|
Facebook:
|
|
409
438
|
facebook_page_id: Facebook Page ID
|
|
410
439
|
video_state: PUBLISHED or DRAFT
|
|
411
|
-
facebook_media_type: REELS or
|
|
440
|
+
facebook_media_type: REELS, STORIES, or VIDEO
|
|
441
|
+
thumbnail_url: Thumbnail URL for normal page videos (VIDEO type only)
|
|
412
442
|
|
|
413
443
|
Pinterest:
|
|
414
444
|
pinterest_board_id: Board ID
|
|
@@ -429,6 +459,7 @@ class UploadPostClient:
|
|
|
429
459
|
|
|
430
460
|
Threads:
|
|
431
461
|
threads_long_text_as_post: Post long text as single post
|
|
462
|
+
threads_topic_tag: Topic tag for the post (1-50 chars, no periods or ampersands)
|
|
432
463
|
|
|
433
464
|
Returns:
|
|
434
465
|
API response with request_id for async uploads.
|
|
@@ -456,7 +487,7 @@ class UploadPostClient:
|
|
|
456
487
|
if "tiktok" in platforms:
|
|
457
488
|
self._add_tiktok_params(data, is_video=True, **kwargs)
|
|
458
489
|
if "instagram" in platforms:
|
|
459
|
-
self._add_instagram_params(data, is_video=True, **kwargs)
|
|
490
|
+
self._add_instagram_params(data, is_video=True, files=files, **kwargs)
|
|
460
491
|
if "youtube" in platforms:
|
|
461
492
|
self._add_youtube_params(data, **kwargs)
|
|
462
493
|
if "linkedin" in platforms:
|
|
@@ -479,9 +510,9 @@ class UploadPostClient:
|
|
|
479
510
|
def upload_photos(
|
|
480
511
|
self,
|
|
481
512
|
photos: List[Union[str, Path]],
|
|
482
|
-
title: str,
|
|
483
|
-
user: str,
|
|
484
|
-
platforms: List[str],
|
|
513
|
+
title: Optional[str] = None,
|
|
514
|
+
user: str = "",
|
|
515
|
+
platforms: Optional[List[str]] = None,
|
|
485
516
|
**kwargs
|
|
486
517
|
) -> Dict:
|
|
487
518
|
"""
|
|
@@ -489,7 +520,8 @@ class UploadPostClient:
|
|
|
489
520
|
|
|
490
521
|
Args:
|
|
491
522
|
photos: List of photo file paths or URLs.
|
|
492
|
-
title: Post title/caption.
|
|
523
|
+
title: Post title/caption. Required for Reddit.
|
|
524
|
+
Optional for TikTok, Instagram, Facebook, LinkedIn, X, Threads, Bluesky, Pinterest.
|
|
493
525
|
user: User identifier (profile name).
|
|
494
526
|
platforms: Target platforms. Supported: tiktok, instagram, linkedin,
|
|
495
527
|
facebook, pinterest, threads, reddit, bluesky, x
|
|
@@ -533,14 +565,27 @@ class UploadPostClient:
|
|
|
533
565
|
nullcast: Promoted-only post
|
|
534
566
|
tagged_user_ids: User IDs to tag
|
|
535
567
|
x_long_text_as_post: Post long text as single post
|
|
536
|
-
|
|
568
|
+
x_thread_image_layout: Comma-separated image layout for thread
|
|
569
|
+
(e.g. "4,4", "2,3,1", or "0,1"). Each value 0-4, total must
|
|
570
|
+
equal image count. 0 means no images for that tweet (useful
|
|
571
|
+
for URL preview cards). Auto-chunks into groups of 4 if >4
|
|
572
|
+
images and no layout specified.
|
|
573
|
+
|
|
537
574
|
Threads:
|
|
538
575
|
threads_long_text_as_post: Post long text as single post
|
|
539
|
-
|
|
576
|
+
threads_thread_media_layout: Comma-separated media layout for thread
|
|
577
|
+
(e.g. "5,5", "3,4,3", or "0,1"). Each value 0-10, total must
|
|
578
|
+
equal media count. 0 means no media for that post. Auto-chunks
|
|
579
|
+
into groups of 10 if >10 items and no layout specified.
|
|
580
|
+
threads_topic_tag: Topic tag for the post (1-50 chars, no periods or ampersands)
|
|
581
|
+
|
|
540
582
|
Reddit:
|
|
541
583
|
subreddit: Subreddit name (without r/)
|
|
542
584
|
flair_id: Flair template ID
|
|
543
585
|
|
|
586
|
+
first_comment_media: List of file paths to attach as images in
|
|
587
|
+
the first comment. Supported on Reddit and X.
|
|
588
|
+
|
|
544
589
|
Returns:
|
|
545
590
|
API response.
|
|
546
591
|
|
|
@@ -550,7 +595,7 @@ class UploadPostClient:
|
|
|
550
595
|
data: List[tuple] = []
|
|
551
596
|
files: List[tuple] = []
|
|
552
597
|
opened_files: List = []
|
|
553
|
-
|
|
598
|
+
|
|
554
599
|
try:
|
|
555
600
|
for photo in photos:
|
|
556
601
|
photo_str = str(photo)
|
|
@@ -563,9 +608,9 @@ class UploadPostClient:
|
|
|
563
608
|
photo_file = photo_p.open("rb")
|
|
564
609
|
opened_files.append(photo_file)
|
|
565
610
|
files.append(("photos[]", (photo_p.name, photo_file)))
|
|
566
|
-
|
|
611
|
+
|
|
567
612
|
self._add_common_params(data, user, title, platforms, **kwargs)
|
|
568
|
-
|
|
613
|
+
|
|
569
614
|
if "tiktok" in platforms:
|
|
570
615
|
self._add_tiktok_params(data, is_video=False, **kwargs)
|
|
571
616
|
if "instagram" in platforms:
|
|
@@ -582,9 +627,19 @@ class UploadPostClient:
|
|
|
582
627
|
self._add_threads_params(data, **kwargs)
|
|
583
628
|
if "reddit" in platforms:
|
|
584
629
|
self._add_reddit_params(data, **kwargs)
|
|
585
|
-
|
|
630
|
+
|
|
631
|
+
first_comment_media = kwargs.get("first_comment_media")
|
|
632
|
+
if first_comment_media:
|
|
633
|
+
for media_path in first_comment_media:
|
|
634
|
+
p = Path(media_path)
|
|
635
|
+
if not p.exists():
|
|
636
|
+
raise UploadPostError(f"First comment media file not found: {media_path}")
|
|
637
|
+
f = open(p, "rb")
|
|
638
|
+
opened_files.append(f)
|
|
639
|
+
files.append(("first_comment_media[]", (p.name, f)))
|
|
640
|
+
|
|
586
641
|
return self._request("/upload_photos", "POST", data=data, files=files if files else None)
|
|
587
|
-
|
|
642
|
+
|
|
588
643
|
finally:
|
|
589
644
|
for f in opened_files:
|
|
590
645
|
f.close()
|
|
@@ -611,14 +666,20 @@ class UploadPostClient:
|
|
|
611
666
|
timezone: Timezone for scheduled date
|
|
612
667
|
add_to_queue: Add to posting queue
|
|
613
668
|
async_upload: Process asynchronously
|
|
614
|
-
|
|
669
|
+
link_url: Generic URL for link preview card (works for LinkedIn,
|
|
670
|
+
Bluesky, Facebook). Platform-specific params take priority.
|
|
671
|
+
|
|
615
672
|
LinkedIn:
|
|
616
673
|
target_linkedin_page_id: Page ID for organization posts
|
|
617
|
-
|
|
674
|
+
linkedin_link_url: URL to attach as link preview on LinkedIn
|
|
675
|
+
|
|
618
676
|
Facebook:
|
|
619
677
|
facebook_page_id: Facebook Page ID
|
|
620
|
-
facebook_link_url: URL to attach as link preview
|
|
621
|
-
|
|
678
|
+
facebook_link_url: URL to attach as link preview on Facebook
|
|
679
|
+
|
|
680
|
+
Bluesky:
|
|
681
|
+
bluesky_link_url: URL to attach as external embed link preview
|
|
682
|
+
|
|
622
683
|
X (Twitter):
|
|
623
684
|
reply_settings: Who can reply
|
|
624
685
|
post_url: URL to attach
|
|
@@ -628,13 +689,20 @@ class UploadPostClient:
|
|
|
628
689
|
poll_reply_settings: Who can reply to poll
|
|
629
690
|
card_uri: Card URI for Twitter Cards
|
|
630
691
|
x_long_text_as_post: Post long text as single post
|
|
631
|
-
|
|
692
|
+
|
|
632
693
|
Threads:
|
|
633
694
|
threads_long_text_as_post: Post long text as single post
|
|
634
|
-
|
|
695
|
+
threads_topic_tag: Topic tag for the post (1-50 chars, no periods or ampersands)
|
|
696
|
+
|
|
635
697
|
Reddit:
|
|
636
698
|
subreddit: Subreddit name (without r/)
|
|
637
699
|
flair_id: Flair template ID
|
|
700
|
+
reddit_link_url: URL for link post. Creates a Reddit link post
|
|
701
|
+
(kind: "link") instead of a text post. Overrides `link_url`
|
|
702
|
+
for Reddit.
|
|
703
|
+
|
|
704
|
+
first_comment_media: List of file paths to attach as images in
|
|
705
|
+
the first comment. Supported on Reddit and X.
|
|
638
706
|
|
|
639
707
|
Returns:
|
|
640
708
|
API response.
|
|
@@ -643,11 +711,16 @@ class UploadPostClient:
|
|
|
643
711
|
UploadPostError: If upload fails.
|
|
644
712
|
"""
|
|
645
713
|
data: List[tuple] = []
|
|
646
|
-
|
|
714
|
+
files: Optional[List[tuple]] = None
|
|
715
|
+
|
|
647
716
|
self._add_common_params(data, user, title, platforms, **kwargs)
|
|
648
|
-
|
|
717
|
+
|
|
718
|
+
# Generic link_url support
|
|
719
|
+
if kwargs.get("link_url"):
|
|
720
|
+
data.append(("link_url", kwargs["link_url"]))
|
|
721
|
+
|
|
649
722
|
if "linkedin" in platforms:
|
|
650
|
-
self._add_linkedin_params(data, **kwargs)
|
|
723
|
+
self._add_linkedin_params(data, is_text=True, **kwargs)
|
|
651
724
|
if "facebook" in platforms:
|
|
652
725
|
self._add_facebook_params(data, is_video=False, is_text=True, **kwargs)
|
|
653
726
|
if "x" in platforms:
|
|
@@ -655,9 +728,29 @@ class UploadPostClient:
|
|
|
655
728
|
if "threads" in platforms:
|
|
656
729
|
self._add_threads_params(data, **kwargs)
|
|
657
730
|
if "reddit" in platforms:
|
|
658
|
-
self._add_reddit_params(data, **kwargs)
|
|
659
|
-
|
|
660
|
-
|
|
731
|
+
self._add_reddit_params(data, is_text=True, **kwargs)
|
|
732
|
+
if "bluesky" in platforms:
|
|
733
|
+
bluesky_link = kwargs.get("bluesky_link_url")
|
|
734
|
+
if bluesky_link:
|
|
735
|
+
data.append(("bluesky_link_url", bluesky_link))
|
|
736
|
+
|
|
737
|
+
first_comment_media = kwargs.get("first_comment_media")
|
|
738
|
+
opened_files: List = []
|
|
739
|
+
if first_comment_media:
|
|
740
|
+
files = []
|
|
741
|
+
for media_path in first_comment_media:
|
|
742
|
+
p = Path(media_path)
|
|
743
|
+
if not p.exists():
|
|
744
|
+
raise UploadPostError(f"First comment media file not found: {media_path}")
|
|
745
|
+
f = open(p, "rb")
|
|
746
|
+
opened_files.append(f)
|
|
747
|
+
files.append(("first_comment_media[]", (p.name, f)))
|
|
748
|
+
|
|
749
|
+
try:
|
|
750
|
+
return self._request("/upload_text", "POST", data=data, files=files)
|
|
751
|
+
finally:
|
|
752
|
+
for f in opened_files:
|
|
753
|
+
f.close()
|
|
661
754
|
|
|
662
755
|
def upload_document(
|
|
663
756
|
self,
|
|
@@ -737,7 +830,7 @@ class UploadPostClient:
|
|
|
737
830
|
|
|
738
831
|
def get_status(self, request_id: str) -> Dict:
|
|
739
832
|
"""
|
|
740
|
-
Get the status of an async upload.
|
|
833
|
+
Get the status of an async upload by request ID.
|
|
741
834
|
|
|
742
835
|
Args:
|
|
743
836
|
request_id: The request_id from an async upload.
|
|
@@ -747,6 +840,18 @@ class UploadPostClient:
|
|
|
747
840
|
"""
|
|
748
841
|
return self._request("/uploadposts/status", "GET", params={"request_id": request_id})
|
|
749
842
|
|
|
843
|
+
def get_job_status(self, job_id: str) -> Dict:
|
|
844
|
+
"""
|
|
845
|
+
Get the status of a scheduled or queued upload by job ID.
|
|
846
|
+
|
|
847
|
+
Args:
|
|
848
|
+
job_id: The job_id from a scheduled or queued upload.
|
|
849
|
+
|
|
850
|
+
Returns:
|
|
851
|
+
Upload status.
|
|
852
|
+
"""
|
|
853
|
+
return self._request("/uploadposts/status", "GET", params={"job_id": job_id})
|
|
854
|
+
|
|
750
855
|
def get_history(self, page: int = 1, limit: int = 20) -> Dict:
|
|
751
856
|
"""
|
|
752
857
|
Get upload history.
|
|
@@ -760,22 +865,114 @@ class UploadPostClient:
|
|
|
760
865
|
"""
|
|
761
866
|
return self._request("/uploadposts/history", "GET", params={"page": page, "limit": limit})
|
|
762
867
|
|
|
763
|
-
def get_analytics(self, profile_username: str, platforms: Optional[List[str]] = None
|
|
868
|
+
def get_analytics(self, profile_username: str, platforms: Optional[List[str]] = None,
|
|
869
|
+
page_id: Optional[str] = None, page_urn: Optional[str] = None) -> Dict:
|
|
764
870
|
"""
|
|
765
871
|
Get analytics for a profile.
|
|
766
872
|
|
|
767
873
|
Args:
|
|
768
874
|
profile_username: Profile username.
|
|
769
|
-
platforms: Filter by platforms (instagram, linkedin, facebook, x).
|
|
875
|
+
platforms: Filter by platforms (instagram, linkedin, facebook, x, youtube, tiktok, threads, pinterest, reddit).
|
|
876
|
+
page_id: Facebook Page ID (required for Facebook analytics).
|
|
877
|
+
page_urn: LinkedIn page URN (defaults to "me" for personal profile).
|
|
770
878
|
|
|
771
879
|
Returns:
|
|
772
|
-
Analytics data.
|
|
880
|
+
Analytics data per platform. For Instagram, the response includes both
|
|
881
|
+
'views' (official Instagram metric) and 'impressions' (backwards-compatible alias).
|
|
773
882
|
"""
|
|
774
883
|
params = {}
|
|
775
884
|
if platforms:
|
|
776
885
|
params["platforms"] = ",".join(platforms)
|
|
886
|
+
if page_id:
|
|
887
|
+
params["page_id"] = page_id
|
|
888
|
+
if page_urn:
|
|
889
|
+
params["page_urn"] = page_urn
|
|
777
890
|
return self._request(f"/analytics/{profile_username}", "GET", params=params if params else None)
|
|
778
891
|
|
|
892
|
+
def get_total_impressions(
|
|
893
|
+
self,
|
|
894
|
+
profile_username: str,
|
|
895
|
+
period: Optional[str] = None,
|
|
896
|
+
start_date: Optional[str] = None,
|
|
897
|
+
end_date: Optional[str] = None,
|
|
898
|
+
date: Optional[str] = None,
|
|
899
|
+
platforms: Optional[List[str]] = None,
|
|
900
|
+
breakdown: bool = False,
|
|
901
|
+
metrics: Optional[List[str]] = None,
|
|
902
|
+
) -> Dict:
|
|
903
|
+
"""
|
|
904
|
+
Get total impressions for a profile from daily snapshots.
|
|
905
|
+
|
|
906
|
+
Args:
|
|
907
|
+
profile_username: Profile username.
|
|
908
|
+
period: Period shortcut (last_day, last_week, last_month, last_3months, last_year).
|
|
909
|
+
start_date: Start date in YYYY-MM-DD format.
|
|
910
|
+
end_date: End date in YYYY-MM-DD format.
|
|
911
|
+
date: Single date in YYYY-MM-DD format.
|
|
912
|
+
platforms: Filter by platforms.
|
|
913
|
+
breakdown: Include per-platform and per-day breakdown.
|
|
914
|
+
metrics: Specific metrics to aggregate (e.g., ["likes", "comments", "shares", "views"]).
|
|
915
|
+
|
|
916
|
+
Returns:
|
|
917
|
+
Total impressions data with optional breakdown. For Instagram, uses 'reach' as the primary metric.
|
|
918
|
+
"""
|
|
919
|
+
params: Dict[str, Any] = {}
|
|
920
|
+
if period:
|
|
921
|
+
params["period"] = period
|
|
922
|
+
if start_date:
|
|
923
|
+
params["start_date"] = start_date
|
|
924
|
+
if end_date:
|
|
925
|
+
params["end_date"] = end_date
|
|
926
|
+
if date:
|
|
927
|
+
params["date"] = date
|
|
928
|
+
if platforms:
|
|
929
|
+
params["platform"] = ",".join(platforms)
|
|
930
|
+
if breakdown:
|
|
931
|
+
params["breakdown"] = "true"
|
|
932
|
+
if metrics:
|
|
933
|
+
params["metrics"] = ",".join(metrics)
|
|
934
|
+
return self._request(f"/uploadposts/total-impressions/{profile_username}", "GET", params=params if params else None)
|
|
935
|
+
|
|
936
|
+
def get_post_analytics(self, request_id: str) -> Dict:
|
|
937
|
+
"""
|
|
938
|
+
Get analytics for a specific post across all platforms it was published to.
|
|
939
|
+
|
|
940
|
+
Args:
|
|
941
|
+
request_id: The request_id from the upload.
|
|
942
|
+
|
|
943
|
+
Returns:
|
|
944
|
+
Post analytics with per-platform metrics, profile snapshots, and post-level metrics.
|
|
945
|
+
"""
|
|
946
|
+
return self._request(f"/uploadposts/post-analytics/{request_id}", "GET")
|
|
947
|
+
|
|
948
|
+
def get_post_analytics_by_platform_id(self, platform_post_id: str, platform: str, user: str) -> Dict:
|
|
949
|
+
"""
|
|
950
|
+
Get analytics for any post (including organic posts) using its native platform ID.
|
|
951
|
+
|
|
952
|
+
Args:
|
|
953
|
+
platform_post_id: The native post ID on the platform (e.g., Instagram media ID).
|
|
954
|
+
platform: The platform to query (instagram, youtube, tiktok, facebook, linkedin, x, threads, pinterest, reddit).
|
|
955
|
+
user: The profile_username that owns the social account.
|
|
956
|
+
|
|
957
|
+
Returns:
|
|
958
|
+
Post analytics with live per-post metrics from the platform API.
|
|
959
|
+
"""
|
|
960
|
+
params = {
|
|
961
|
+
"platform_post_id": platform_post_id,
|
|
962
|
+
"platform": platform,
|
|
963
|
+
"user": user,
|
|
964
|
+
}
|
|
965
|
+
return self._request("/uploadposts/post-analytics", "GET", params=params)
|
|
966
|
+
|
|
967
|
+
def get_platform_metrics(self) -> Dict:
|
|
968
|
+
"""
|
|
969
|
+
Get available metrics configuration for all supported platforms.
|
|
970
|
+
|
|
971
|
+
Returns:
|
|
972
|
+
Platform metrics config with primary fields, available metrics, and labels.
|
|
973
|
+
"""
|
|
974
|
+
return self._request("/uploadposts/platform-metrics", "GET")
|
|
975
|
+
|
|
779
976
|
# ==================== Scheduled Posts ====================
|
|
780
977
|
|
|
781
978
|
def list_scheduled(self) -> Dict:
|
|
@@ -864,7 +1061,11 @@ class UploadPostClient:
|
|
|
864
1061
|
redirect_url: Optional[str] = None,
|
|
865
1062
|
logo_image: Optional[str] = None,
|
|
866
1063
|
redirect_button_text: Optional[str] = None,
|
|
867
|
-
platforms: Optional[List[str]] = None
|
|
1064
|
+
platforms: Optional[List[str]] = None,
|
|
1065
|
+
show_calendar: Optional[bool] = None,
|
|
1066
|
+
readonly_calendar: Optional[bool] = None,
|
|
1067
|
+
connect_title: Optional[str] = None,
|
|
1068
|
+
connect_description: Optional[str] = None
|
|
868
1069
|
) -> Dict:
|
|
869
1070
|
"""
|
|
870
1071
|
Generate a JWT for platform integration.
|
|
@@ -876,6 +1077,10 @@ class UploadPostClient:
|
|
|
876
1077
|
logo_image: Logo image URL for the linking page.
|
|
877
1078
|
redirect_button_text: Text for redirect button.
|
|
878
1079
|
platforms: Platforms to show for connection.
|
|
1080
|
+
show_calendar: Whether to show the calendar view.
|
|
1081
|
+
readonly_calendar: Show only a read-only calendar (no editing, no account connection).
|
|
1082
|
+
connect_title: Custom title for the connection page.
|
|
1083
|
+
connect_description: Custom description for the connection page.
|
|
879
1084
|
|
|
880
1085
|
Returns:
|
|
881
1086
|
JWT and connection URL.
|
|
@@ -889,6 +1094,14 @@ class UploadPostClient:
|
|
|
889
1094
|
body["redirect_button_text"] = redirect_button_text
|
|
890
1095
|
if platforms:
|
|
891
1096
|
body["platforms"] = platforms
|
|
1097
|
+
if show_calendar is not None:
|
|
1098
|
+
body["show_calendar"] = show_calendar
|
|
1099
|
+
if readonly_calendar is not None:
|
|
1100
|
+
body["readonly_calendar"] = readonly_calendar
|
|
1101
|
+
if connect_title:
|
|
1102
|
+
body["connect_title"] = connect_title
|
|
1103
|
+
if connect_description:
|
|
1104
|
+
body["connect_description"] = connect_description
|
|
892
1105
|
return self._request("/uploadposts/users/generate-jwt", "POST", json_data=body)
|
|
893
1106
|
|
|
894
1107
|
def validate_jwt(self, jwt: str) -> Dict:
|
|
@@ -903,6 +1116,66 @@ class UploadPostClient:
|
|
|
903
1116
|
"""
|
|
904
1117
|
return self._request("/uploadposts/users/validate-jwt", "POST", json_data={"jwt": jwt})
|
|
905
1118
|
|
|
1119
|
+
def get_user_preferences(self) -> Dict:
|
|
1120
|
+
"""
|
|
1121
|
+
Get user preferences (including calendar settings).
|
|
1122
|
+
|
|
1123
|
+
Returns:
|
|
1124
|
+
User preferences including week_start_day.
|
|
1125
|
+
"""
|
|
1126
|
+
return self._request("/uploadposts/users/preferences", "GET")
|
|
1127
|
+
|
|
1128
|
+
def update_user_preferences(
|
|
1129
|
+
self,
|
|
1130
|
+
week_start_day: Optional[int] = None
|
|
1131
|
+
) -> Dict:
|
|
1132
|
+
"""
|
|
1133
|
+
Update user preferences (including calendar settings).
|
|
1134
|
+
|
|
1135
|
+
Args:
|
|
1136
|
+
week_start_day: Week start day (0=Sunday, 1=Monday).
|
|
1137
|
+
|
|
1138
|
+
Returns:
|
|
1139
|
+
Updated preferences.
|
|
1140
|
+
"""
|
|
1141
|
+
body: Dict[str, Any] = {}
|
|
1142
|
+
if week_start_day is not None:
|
|
1143
|
+
body["week_start_day"] = week_start_day
|
|
1144
|
+
return self._request("/uploadposts/users/preferences", "POST", json_data=body)
|
|
1145
|
+
|
|
1146
|
+
def get_notification_config(self) -> Dict:
|
|
1147
|
+
"""
|
|
1148
|
+
Get notification configuration (including webhook settings).
|
|
1149
|
+
|
|
1150
|
+
Returns:
|
|
1151
|
+
Notification config including webhook_events and webhook_url.
|
|
1152
|
+
"""
|
|
1153
|
+
return self._request("/uploadposts/notification-config", "GET")
|
|
1154
|
+
|
|
1155
|
+
def update_notification_config(
|
|
1156
|
+
self,
|
|
1157
|
+
webhook_events: Optional[List[str]] = None,
|
|
1158
|
+
webhook_url: Optional[str] = None
|
|
1159
|
+
) -> Dict:
|
|
1160
|
+
"""
|
|
1161
|
+
Update notification configuration (including webhook settings).
|
|
1162
|
+
|
|
1163
|
+
Args:
|
|
1164
|
+
webhook_events: Webhook event types to subscribe to
|
|
1165
|
+
(upload_completed, social_account_connected,
|
|
1166
|
+
social_account_disconnected, social_account_reauth_required).
|
|
1167
|
+
webhook_url: Webhook URL for notifications.
|
|
1168
|
+
|
|
1169
|
+
Returns:
|
|
1170
|
+
Updated notification config.
|
|
1171
|
+
"""
|
|
1172
|
+
body: Dict[str, Any] = {}
|
|
1173
|
+
if webhook_events:
|
|
1174
|
+
body["webhook_events"] = webhook_events
|
|
1175
|
+
if webhook_url:
|
|
1176
|
+
body["webhook_url"] = webhook_url
|
|
1177
|
+
return self._request("/uploadposts/notification-config", "POST", json_data=body)
|
|
1178
|
+
|
|
906
1179
|
# ==================== Helper Endpoints ====================
|
|
907
1180
|
|
|
908
1181
|
def get_facebook_pages(self, profile: Optional[str] = None) -> Dict:
|
|
@@ -943,3 +1216,108 @@ class UploadPostClient:
|
|
|
943
1216
|
"""
|
|
944
1217
|
params = {"profile": profile} if profile else None
|
|
945
1218
|
return self._request("/uploadposts/pinterest/boards", "GET", params=params)
|
|
1219
|
+
|
|
1220
|
+
# ==================== Instagram Comments ====================
|
|
1221
|
+
|
|
1222
|
+
def get_post_comments(
|
|
1223
|
+
self,
|
|
1224
|
+
user: str,
|
|
1225
|
+
post_id: Optional[str] = None,
|
|
1226
|
+
post_url: Optional[str] = None
|
|
1227
|
+
) -> Dict:
|
|
1228
|
+
"""
|
|
1229
|
+
Get comments on an Instagram post.
|
|
1230
|
+
|
|
1231
|
+
Args:
|
|
1232
|
+
user: Profile username.
|
|
1233
|
+
post_id: Numeric media ID (provide post_id or post_url).
|
|
1234
|
+
post_url: Full Instagram post URL (provide post_id or post_url).
|
|
1235
|
+
|
|
1236
|
+
Returns:
|
|
1237
|
+
Comments data including comment IDs, text, timestamps, and user info.
|
|
1238
|
+
"""
|
|
1239
|
+
params = {"platform": "instagram", "user": user}
|
|
1240
|
+
if post_id:
|
|
1241
|
+
params["post_id"] = post_id
|
|
1242
|
+
if post_url:
|
|
1243
|
+
params["post_url"] = post_url
|
|
1244
|
+
return self._request("/uploadposts/comments", "GET", params=params)
|
|
1245
|
+
|
|
1246
|
+
def reply_to_comment(
|
|
1247
|
+
self,
|
|
1248
|
+
user: str,
|
|
1249
|
+
comment_id: str,
|
|
1250
|
+
message: str
|
|
1251
|
+
) -> Dict:
|
|
1252
|
+
"""
|
|
1253
|
+
Send a private reply (DM) to the author of an Instagram comment.
|
|
1254
|
+
|
|
1255
|
+
Args:
|
|
1256
|
+
user: Profile username.
|
|
1257
|
+
comment_id: Comment ID from get_post_comments.
|
|
1258
|
+
message: Reply message text.
|
|
1259
|
+
|
|
1260
|
+
Returns:
|
|
1261
|
+
Reply result with recipient_id and message_id.
|
|
1262
|
+
"""
|
|
1263
|
+
return self._request("/uploadposts/comments/reply", "POST", json_data={
|
|
1264
|
+
"platform": "instagram",
|
|
1265
|
+
"user": user,
|
|
1266
|
+
"comment_id": comment_id,
|
|
1267
|
+
"message": message
|
|
1268
|
+
})
|
|
1269
|
+
|
|
1270
|
+
def public_reply_to_comment(
|
|
1271
|
+
self,
|
|
1272
|
+
user: str,
|
|
1273
|
+
comment_id: str,
|
|
1274
|
+
message: str
|
|
1275
|
+
) -> Dict:
|
|
1276
|
+
"""
|
|
1277
|
+
Post a public reply to an Instagram comment (visible under the original comment).
|
|
1278
|
+
|
|
1279
|
+
Args:
|
|
1280
|
+
user: Profile username.
|
|
1281
|
+
comment_id: Comment ID from get_post_comments.
|
|
1282
|
+
message: Reply message text.
|
|
1283
|
+
|
|
1284
|
+
Returns:
|
|
1285
|
+
Reply result with the new comment ID.
|
|
1286
|
+
"""
|
|
1287
|
+
return self._request("/uploadposts/comments/public-reply", "POST", json_data={
|
|
1288
|
+
"platform": "instagram",
|
|
1289
|
+
"user": user,
|
|
1290
|
+
"comment_id": comment_id,
|
|
1291
|
+
"message": message
|
|
1292
|
+
})
|
|
1293
|
+
|
|
1294
|
+
# ==================== Google Business ====================
|
|
1295
|
+
|
|
1296
|
+
def get_google_business_locations(self, profile: Optional[str] = None) -> Dict:
|
|
1297
|
+
"""
|
|
1298
|
+
Get Google Business Profile locations for a connected account.
|
|
1299
|
+
|
|
1300
|
+
Args:
|
|
1301
|
+
profile: Profile username.
|
|
1302
|
+
|
|
1303
|
+
Returns:
|
|
1304
|
+
List of Google Business locations.
|
|
1305
|
+
"""
|
|
1306
|
+
params = {"profile": profile} if profile else None
|
|
1307
|
+
return self._request("/uploadposts/google-business/locations", "GET", params=params)
|
|
1308
|
+
|
|
1309
|
+
def select_google_business_location(self, location_id: str, profile: Optional[str] = None) -> Dict:
|
|
1310
|
+
"""
|
|
1311
|
+
Select a specific Google Business Profile location for a profile.
|
|
1312
|
+
|
|
1313
|
+
Args:
|
|
1314
|
+
location_id: The location ID to select (e.g. "accounts/123/locations/456").
|
|
1315
|
+
profile: Profile username.
|
|
1316
|
+
|
|
1317
|
+
Returns:
|
|
1318
|
+
Selection result with google_business_id and display_name.
|
|
1319
|
+
"""
|
|
1320
|
+
data = {"location_id": location_id}
|
|
1321
|
+
if profile:
|
|
1322
|
+
data["profile"] = profile
|
|
1323
|
+
return self._request("/uploadposts/google-business/locations/select", "POST", json_data=data)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: upload-post
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: Cross-platform social media upload for TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, and X (Twitter)
|
|
5
5
|
Home-page: https://www.upload-post.com/
|
|
6
6
|
Author: Upload-Post
|
|
@@ -162,6 +162,13 @@ status = client.get_status("request_id_from_upload")
|
|
|
162
162
|
print(status)
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
+
For scheduled or queued posts, check the status using the job_id:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
status = client.get_job_status("job_id_from_scheduled_post")
|
|
169
|
+
print(status)
|
|
170
|
+
```
|
|
171
|
+
|
|
165
172
|
### Get Upload History
|
|
166
173
|
|
|
167
174
|
```python
|
|
@@ -280,7 +287,8 @@ boards = client.get_pinterest_boards("my-profile")
|
|
|
280
287
|
### Facebook
|
|
281
288
|
- `facebook_page_id` - Facebook Page ID (required)
|
|
282
289
|
- `video_state` - PUBLISHED, DRAFT
|
|
283
|
-
- `facebook_media_type` - REELS, STORIES
|
|
290
|
+
- `facebook_media_type` - REELS, STORIES, or VIDEO (normal page video)
|
|
291
|
+
- `thumbnail_url` - Thumbnail URL for normal page videos (only when `facebook_media_type` is VIDEO)
|
|
284
292
|
- `facebook_link_url` - URL for text posts
|
|
285
293
|
|
|
286
294
|
### Pinterest
|
|
@@ -303,9 +311,12 @@ boards = client.get_pinterest_boards("my-profile")
|
|
|
303
311
|
- `share_with_followers` - Share community post with followers
|
|
304
312
|
- `card_uri` - Card URI for Twitter Cards
|
|
305
313
|
- `x_long_text_as_post` - Post long text as single post
|
|
314
|
+
- `x_thread_image_layout` - Comma-separated image layout for thread (e.g. "4,4" or "2,3,1"). Each value 1-4, total must equal image count. Auto-chunks into groups of 4 when >4 images.
|
|
306
315
|
|
|
307
316
|
### Threads
|
|
308
317
|
- `threads_long_text_as_post` - Post long text as single post (vs thread)
|
|
318
|
+
- `threads_thread_media_layout` - Comma-separated list of how many media items to include in each Threads post (e.g. "5,5" or "3,4,3"). Each value 1-10, total must equal media count. Auto-chunks into groups of 10 when >10 items.
|
|
319
|
+
- `threads_topic_tag` - Topic tag for the Threads post (1-50 characters, no periods or ampersands). One tag per post. Helps increase reach.
|
|
309
320
|
|
|
310
321
|
### Reddit
|
|
311
322
|
- `subreddit` - Subreddit name (without r/)
|
|
@@ -325,6 +336,7 @@ These options work across all upload methods:
|
|
|
325
336
|
| `scheduled_date` | ISO date for scheduling |
|
|
326
337
|
| `timezone` | Timezone for scheduled date |
|
|
327
338
|
| `add_to_queue` | Add to posting queue |
|
|
339
|
+
| `max_posts_per_slot` | Max posts per queue slot (overrides profile setting) |
|
|
328
340
|
| `async_upload` | Process asynchronously (default: True) |
|
|
329
341
|
|
|
330
342
|
## Error Handling
|
|
@@ -350,3 +362,5 @@ except UploadPostError as e:
|
|
|
350
362
|
## License
|
|
351
363
|
|
|
352
364
|
MIT
|
|
365
|
+
|
|
366
|
+
<!-- deployed 2026-03-16 17:49 UTC -->
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|