planvortex 0.1.0__tar.gz → 0.2.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.
- {planvortex-0.1.0 → planvortex-0.2.0}/CHANGELOG.md +63 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/PKG-INFO +1 -1
- {planvortex-0.1.0 → planvortex-0.2.0}/openapi/planvortex.openapi.json +62 -33
- {planvortex-0.1.0 → planvortex-0.2.0}/pyproject.toml +1 -1
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/_generated/models.py +63 -15
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/_shapes.py +41 -8
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/_version.py +2 -2
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources/accounts.py +17 -5
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources/catalog.py +12 -2
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources/comments.py +20 -1
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources/messages.py +2 -2
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources/products.py +2 -2
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources/publications.py +8 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources_sync/accounts.py +17 -5
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources_sync/catalog.py +12 -2
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources_sync/comments.py +14 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources_sync/products.py +2 -2
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources_sync/publications.py +8 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/types.py +47 -10
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/live/conftest.py +1 -1
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/live/test_connect_flow.py +17 -1
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_models.py +14 -2
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_shapes_parity.py +40 -28
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_types.py +40 -7
- {planvortex-0.1.0 → planvortex-0.2.0}/.gitignore +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/LICENSE +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/README.md +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/scripts/build_docs.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/scripts/check_packaging.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/scripts/generate_models.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/scripts/generate_sync.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/scripts/route_coverage.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/__init__.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/_client.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/_client_sync.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/_core/__init__.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/_core/auth.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/_core/auth_sync.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/_core/errors.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/_core/files.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/_core/http.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/_core/http_sync.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/_core/pagination.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/_core/query.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/_core/transport.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/_generated/__init__.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/py.typed +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources/__init__.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources/ai_plans.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources/apps.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources/base.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources/clients.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources/contacts.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources/dashboard.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources/integrations.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources/organizations.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources/uploads.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources_sync/__init__.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources_sync/ai_plans.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources_sync/apps.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources_sync/base.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources_sync/clients.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources_sync/contacts.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources_sync/dashboard.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources_sync/integrations.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources_sync/messages.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources_sync/organizations.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/resources_sync/uploads.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/src/planvortex/webhooks.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/__init__.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/conftest.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/contrato.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/live/__init__.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/live/test_auth.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/live/test_catalog.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/live/test_errors.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/live/test_publish.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/live/test_read.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_accounts.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_ai_plans.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_apps.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_catalog.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_client.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_clients.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_comments.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_contacts.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_core.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_dashboard.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_example_comments.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_example_connect.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_example_publish.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_example_schedule.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_example_webhooks.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_files.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_generate_models.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_generate_sync.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_integrations.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_messages.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_openapi_freshness.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_organizations.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_packaging.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_pagination.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_products.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_publications.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_query_parity.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_readme.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_route_coverage.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_uploads.py +0 -0
- {planvortex-0.1.0 → planvortex-0.2.0}/tests/test_webhooks.py +0 -0
|
@@ -4,6 +4,69 @@ All notable changes to this package are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.2.0] - 2026-08-30
|
|
8
|
+
|
|
9
|
+
Telegram, the eleventh network, reaches the package. No endpoint was added and no signature moved:
|
|
10
|
+
what changes is what the types know — and unlike in the Node library, where an open enumeration
|
|
11
|
+
absorbs an unknown network in silence, here the closed `Literal`s go red until somebody looks. Which
|
|
12
|
+
is the point of them.
|
|
13
|
+
|
|
14
|
+
Upgrading from `0.1.0` needs no changes, and `MIGRATION.md` gains no entry.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **A third authorization shape: `TelegramBotAuthorization`, with its predicate
|
|
19
|
+
`is_telegram_bot_authorization`.** This is the part that is genuinely new contract rather than one
|
|
20
|
+
more name in a list. Telegram is the one entry that carries a `link` and still is not somewhere to
|
|
21
|
+
redirect: it opens a private chat with the PlanVortex bot, with no OAuth behind it — no consent
|
|
22
|
+
screen, no `code`, no `redirect_uri` — and the account is born minutes later, when the person
|
|
23
|
+
drops that bot into their channel. It is announced over the WebSocket and the `new_account`
|
|
24
|
+
webhook, never as the answer to a call of yours, and `accounts.connect` cannot finish it: for
|
|
25
|
+
`telegram` that endpoint always answers 700. The block carries `bot_username` (the bot's `@name`,
|
|
26
|
+
without the at sign) and `add_to_group_link`, the second step, which turns comments on by adding
|
|
27
|
+
the bot to the channel's linked discussion group. **Branch on `authorization["type"]`, never on
|
|
28
|
+
whether `link` is empty** — that test was already wrong for WhatsApp and it is worse here, because
|
|
29
|
+
a filled-in `link` makes it look like it worked.
|
|
30
|
+
- **`"telegram"` in `PublishableNetwork`** — the one hand-written union in `_shapes.py` — and
|
|
31
|
+
therefore in `PUBLISHABLE_NETWORKS`, `COMMENT_NETWORKS` and `SOCIAL_NETWORKS`, which derive from
|
|
32
|
+
their `Literal` and needed no edit. It is deliberately **not** in `CONTACT_CHANNELS`: Telegram has
|
|
33
|
+
no direct messages, and putting it there would promise a chat inbox that does not exist. That the
|
|
34
|
+
contact channels are now a proper subset of the networks, rather than all of them plus `email`, is
|
|
35
|
+
written down in the test that used to assert the equality.
|
|
36
|
+
- **`PublicationStats.reactions` and `reactions_by_emoji`**, which are Telegram's only publication
|
|
37
|
+
metric — and neither of them is asked for. `reactions` is the COMPLETE STATE and not an increment,
|
|
38
|
+
so it goes down when somebody takes theirs back.
|
|
39
|
+
- **`Publication.extra_data`**, for what one network has to remember about one publication and that
|
|
40
|
+
has no common field. Today only `telegram_message_ids` writes there: on Telegram an album is one
|
|
41
|
+
publication that is several messages, `external_identifier` holds the first and the rest live
|
|
42
|
+
here, because deleting the album means deleting all of them.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- The network-counting prose, which had quietly stopped being true and that no test watches:
|
|
47
|
+
`accounts.connect_links`, `SocialAuthorizationMethod`, `ConnectLink`, `PublishableNetwork` and
|
|
48
|
+
`is_publishable_network` all said "nine of the ten" or "one of ten and one of nine".
|
|
49
|
+
- Two warnings about this network are now on the methods that would otherwise surprise you, because
|
|
50
|
+
neither has an error code to announce it. **The comment inbox starts the day the channel was
|
|
51
|
+
connected** — the Bot API cannot read the past, so `comments.list` has nothing earlier and never
|
|
52
|
+
will, and `comments.thread`, which is a live read on every other network, is not live here at all.
|
|
53
|
+
And **there are no impressions and no reach**: engagement is computed over followers, the only
|
|
54
|
+
audience figure the Bot API publishes being the channel's member count.
|
|
55
|
+
- `catalog.social_limits` explains Telegram's **two numbers for the same field** — 4.096 characters
|
|
56
|
+
while the publication is text only and 1.024 the moment it carries an image or a video, because
|
|
57
|
+
then the text is a media caption. The counter switches when the file is attached, not when publish
|
|
58
|
+
is pressed.
|
|
59
|
+
- `publications.remove` documents Telegram's **48-hour window** (error 966, with `published_date`
|
|
60
|
+
and `max_hours` in `data`), which is a button to grey out rather than to offer and fail.
|
|
61
|
+
- `examples/connect.py` grows the third branch, and its `else` — the one that skips an authorization
|
|
62
|
+
method this version does not know — is no longer hypothetical: `telegram_bot` was exactly that.
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- The synchronous twin of `products.get` had been left behind by the previous release: its docstring
|
|
67
|
+
still said the Node library documented that endpoint as broken. Regenerating `resources_sync`
|
|
68
|
+
brings it back in line.
|
|
69
|
+
|
|
7
70
|
## [0.1.0] - 2026-08-28
|
|
8
71
|
|
|
9
72
|
The first release with code in it. Every documented endpoint of the PlanVortex API has a method -
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: planvortex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Official Python client for the PlanVortex API
|
|
5
5
|
Project-URL: Homepage, https://planvortex.com/developers
|
|
6
6
|
Project-URL: Documentation, https://taliasoftworks.github.io/PlanVortexPython/
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"accounts"
|
|
108
108
|
],
|
|
109
109
|
"summary": "Get the connection links of every connectable network",
|
|
110
|
-
"description": "The authorization URL of each network, so the user can connect an account to this organization.\n\n**A network that cannot produce a link simply does not appear.** That is a legitimate answer, not a failure: it is what happens with `discord` in an organization that has not saved its own bot credentials yet (see `PUT /organizations/{id_organization}/social_credentials/{social_network}`).\n\n**An app cannot call this.** Connecting a social account is an OAuth flow with a person in front of it, so this endpoint only accepts a user token or a temporal connect token; with app credentials it answers error 519. The way an integration does it is to issue a temporal connect token with `GET /organizations/{id_organization}/temporal_connect_token` and hand it to its end user.\n\n**Read `authorization`, not `link`.** Nine of the
|
|
110
|
+
"description": "The authorization URL of each network, so the user can connect an account to this organization.\n\n**A network that cannot produce a link simply does not appear.** That is a legitimate answer, not a failure: it is what happens with `discord` in an organization that has not saved its own bot credentials yet (see `PUT /organizations/{id_organization}/social_credentials/{social_network}`).\n\n**An app cannot call this.** Connecting a social account is an OAuth flow with a person in front of it, so this endpoint only accepts a user token or a temporal connect token; with app credentials it answers error 519. The way an integration does it is to issue a temporal connect token with `GET /organizations/{id_organization}/temporal_connect_token` and hand it to its end user.\n\n**Read `authorization`, not `link`.** Nine of the eleven networks are `redirect` and you send the person to `link`. Two are not, and neither of them fails visibly if you treat it as one:\n\n• **WhatsApp is not a URL at all.** Its sign-up is Meta's Embedded Signup, a popup you raise with the Facebook JavaScript SDK, so its `link` is an empty string and everything you need to open that popup travels in `authorization`. A client that loops over the list and redirects to `link` sends its user to its own page.\n• **Telegram has a link and still is not a redirect.** It opens a chat with the PlanVortex bot, and nobody comes back from it: the account is born minutes later, from the bot being added to a channel, and it is announced over the WebSocket. Open it in another tab and keep listening; redirect to it and there is nobody left to tell.",
|
|
111
111
|
"operationId": "getConnectLinks",
|
|
112
112
|
"x-planvortex-identity": [
|
|
113
113
|
"current_user",
|
|
@@ -656,7 +656,7 @@
|
|
|
656
656
|
"accounts"
|
|
657
657
|
],
|
|
658
658
|
"summary": "Complete the connection of a social account",
|
|
659
|
-
"description": "The endpoint the social network sends the user back to after they authorize. It turns the network's callback into one or more PlanVortex accounts.\n\n**You do not build this URL, the network does.** It is the `redirect_uri` inside the link that `GET /organizations/{id_organization}/connect_links` handed out, so the query parameters are whatever the network appends — typically `code` and `state`. Pass them through untouched.\n\n**One authorization can produce several accounts.** A Facebook user with four pages ends up with four; a Discord authorization produces the channel that was picked.\n\n**It answers 200 even when it fails.** The result carries `errorCode` and `errorMsg` instead of an error body, because the browser lands here from a redirect and a raw 400 would be a broken page. Check `errorCode`: empty means everything went well.\n\n**An app cannot call this** — it needs a user token or a temporal connect token (error 519 otherwise). See `GET /organizations/{id_organization}/temporal_connect_token`.\n\n**A temporal connect token is spent here.** Once this call succeeds, that token cannot connect anything else and answers error 543; the `enable` calls that finish the same connection still work until it expires. And if the token was issued for one network, calling this for another answers error 544.",
|
|
659
|
+
"description": "The endpoint the social network sends the user back to after they authorize. It turns the network's callback into one or more PlanVortex accounts.\n\n**You do not build this URL, the network does.** It is the `redirect_uri` inside the link that `GET /organizations/{id_organization}/connect_links` handed out, so the query parameters are whatever the network appends — typically `code` and `state`. Pass them through untouched.\n\n**One authorization can produce several accounts.** A Facebook user with four pages ends up with four; a Discord authorization produces the channel that was picked.\n\n**It answers 200 even when it fails.** The result carries `errorCode` and `errorMsg` instead of an error body, because the browser lands here from a redirect and a raw 400 would be a broken page. Check `errorCode`: empty means everything went well.\n\n**An app cannot call this** — it needs a user token or a temporal connect token (error 519 otherwise). See `GET /organizations/{id_organization}/temporal_connect_token`.\n\n**A temporal connect token is spent here.** Once this call succeeds, that token cannot connect anything else and answers error 543; the `enable` calls that finish the same connection still work until it expires. And if the token was issued for one network, calling this for another answers error 544.\n\n**Telegram does not come through here, and cannot be made to.** That network has no callback: the account is created by PlanVortex when the bot is added to a channel, and what authorizes it is a single-use voucher minted at that moment and spent in the same breath — it never leaves the server, so calling this endpoint for `telegram` answers error 700. It is deliberate: the bot is shared, so without it anyone could hang any channel where that bot is an admin off their own organization by passing a chat id by hand. What an integration listens for instead is the `new_account` webhook notification.",
|
|
660
660
|
"operationId": "connectAccount",
|
|
661
661
|
"x-planvortex-identity": [
|
|
662
662
|
"current_user",
|
|
@@ -715,7 +715,7 @@
|
|
|
715
715
|
}
|
|
716
716
|
},
|
|
717
717
|
"400": {
|
|
718
|
-
"description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `519` | This endpoint does not accept app credentials |\n| `543` | This temporal connect token has already connected an account. Issue a new one |\n| `544` | This temporal connect token was issued for a different social network |\n| `1101` | Invalid organization, or a temporal token for a different one |",
|
|
718
|
+
"description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `519` | This endpoint does not accept app credentials |\n| `543` | This temporal connect token has already connected an account. Issue a new one |\n| `544` | This temporal connect token was issued for a different social network |\n| `700` | The connection cannot be completed through this endpoint. It is what `telegram` always answers here: that network's accounts are created from the bot being added to a channel, never from a call of yours |\n| `1101` | Invalid organization, or a temporal token for a different one |",
|
|
719
719
|
"content": {
|
|
720
720
|
"application/json": {
|
|
721
721
|
"schema": {
|
|
@@ -1761,7 +1761,8 @@
|
|
|
1761
1761
|
"youtube",
|
|
1762
1762
|
"google_business",
|
|
1763
1763
|
"bluesky",
|
|
1764
|
-
"discord"
|
|
1764
|
+
"discord",
|
|
1765
|
+
"telegram"
|
|
1765
1766
|
]
|
|
1766
1767
|
}
|
|
1767
1768
|
}
|
|
@@ -1865,7 +1866,7 @@
|
|
|
1865
1866
|
"catalog"
|
|
1866
1867
|
],
|
|
1867
1868
|
"summary": "Per-network publication limits",
|
|
1868
|
-
"description": "Every limit a publication is validated against, indexed by limit and then by network.\n\nRead it before building a composer. A few traps worth knowing:\n\n• **`characters` is not always the only text limit.** Bluesky counts 300 *graphemes* **and** 3.000 *bytes*; the second one travels in `max_post_bytes`, where `0` means \"this network does not measure text in bytes\". A family emoji is one grapheme and eleven UTF-16 units, so counting with `String.length` is wrong in both directions.\n• **`0` in `title_characters` means the network has no title field**, not a title of zero length.\n• **`comment_characters` is a different limit from `characters`.** Facebook takes 63.206 in a post and 8.000 in a comment.\n• Every network in `/social_networks` appears in every map. A missing key is a bug, and the backend's conformance suite fails on it.",
|
|
1869
|
+
"description": "Every limit a publication is validated against, indexed by limit and then by network.\n\nRead it before building a composer. A few traps worth knowing:\n\n• **`characters` is not always the only text limit.** Bluesky counts 300 *graphemes* **and** 3.000 *bytes*; the second one travels in `max_post_bytes`, where `0` means \"this network does not measure text in bytes\". A family emoji is one grapheme and eleven UTF-16 units, so counting with `String.length` is wrong in both directions.\n• **`0` in `title_characters` means the network has no title field**, not a title of zero length.\n• **`comment_characters` is a different limit from `characters`.** Facebook takes 63.206 in a post and 8.000 in a comment.\n• **`characters` is not one number per network either.** Telegram takes 4.096 in a text post and **1.024** in the caption of a photo or a video, and it is the same composer field: the second number is the key `telegram_media`. Over the limit the publication is created in state `withErrors` with `publication_errors[].code = 967`, which carries `characters`, `max_characters` and `has_media`.\n• Every network in `/social_networks` appears in every map. A missing key is a bug, and the backend's conformance suite fails on it.",
|
|
1869
1870
|
"operationId": "getSocialLimits",
|
|
1870
1871
|
"x-planvortex-identity": [
|
|
1871
1872
|
"current_user",
|
|
@@ -3441,7 +3442,7 @@
|
|
|
3441
3442
|
"comments"
|
|
3442
3443
|
],
|
|
3443
3444
|
"summary": "The inbox: first-level comments across the whole organization",
|
|
3444
|
-
"description": "Served from PlanVortex's database, so it costs nothing and calls no social network. It is a **snapshot**: `collected_date` says when each row was last read. Open a thread to see what the network says right now.\n\nOrdered by `creation_date` descending — the date on the network, not the date we collected it — so rows from
|
|
3445
|
+
"description": "Served from PlanVortex's database, so it costs nothing and calls no social network. It is a **snapshot**: `collected_date` says when each row was last read. Open a thread to see what the network says right now.\n\nOrdered by `creation_date` descending — the date on the network, not the date we collected it — so rows from nine networks interleave correctly.\n\n**On `telegram` the inbox starts the day the channel was connected.** The Bot API has no way to read the past: a bot only learns what happens while it is inside, so nothing written before the connection exists here and never will. Say so in your UI — an inbox that opens empty on a busy channel reads like a failure.\n\n**Your own replies are not in here.** Anything with `author.is_own: true` is filtered out: what you wrote is not incoming mail. They are still stored, and they do show up in the thread.\n\nRequires the `comments:read` permission (`client_organization_comments:read` for apps) and a paid plan.",
|
|
3445
3446
|
"operationId": "getComments",
|
|
3446
3447
|
"x-planvortex-identity": [
|
|
3447
3448
|
"current_user",
|
|
@@ -3615,7 +3616,7 @@
|
|
|
3615
3616
|
"comments"
|
|
3616
3617
|
],
|
|
3617
3618
|
"summary": "The thread of a publication, read live",
|
|
3618
|
-
"description": "Asks the social network and reconciles with what is stored: the network wins on text, counters and existence; the stored copy only contributes `_id`, `read` and `replied`. Comments the network no longer returns are marked deleted and stop appearing in the inbox.\n\n**On X this call costs money.** X bills per unit read, so the response carries `credits_consumed` with what this particular read spent from the client's monthly pool. It is `0` on every other network. Charging happens after the read and by real units: a failed call charges nothing, and a page with three replies is not charged for fifty.\n\nUse this one when the comment has an `id_publication`. When it does not — a review hangs off a listing — use the per-account endpoint instead.\n\nRequires the `comments:read` permission (`client_organization_comments:read` for apps) and a paid plan.",
|
|
3619
|
+
"description": "Asks the social network and reconciles with what is stored: the network wins on text, counters and existence; the stored copy only contributes `_id`, `read` and `replied`. Comments the network no longer returns are marked deleted and stop appearing in the inbox.\n\n**On X this call costs money.** X bills per unit read, so the response carries `credits_consumed` with what this particular read spent from the client's monthly pool. It is `0` on every other network. Charging happens after the read and by real units: a failed call charges nothing, and a page with three replies is not charged for fifty.\n\nUse this one when the comment has an `id_publication`. When it does not — a review hangs off a listing — use the per-account endpoint instead.\n\n**On `telegram` it is not live**, and that is the one exception to everything above: there is no endpoint in the Bot API that lists the replies to a post, so this returns PlanVortex's own inbox — what the bot has seen since the channel was connected. Nothing is reconciled and nothing is swept as deleted, because there is nothing to compare against.\n\nRequires the `comments:read` permission (`client_organization_comments:read` for apps) and a paid plan.",
|
|
3619
3620
|
"operationId": "getPublicationComments",
|
|
3620
3621
|
"x-planvortex-identity": [
|
|
3621
3622
|
"current_user",
|
|
@@ -3744,7 +3745,7 @@
|
|
|
3744
3745
|
"comments"
|
|
3745
3746
|
],
|
|
3746
3747
|
"summary": "Reply in public",
|
|
3747
|
-
"description": "Posts a **public** reply on the social network. This is not the same as replying privately to whoever commented — that is a Messenger feature and lives in the messaging endpoints.\n\nThe reply is stored immediately, with `author.is_own: true`, without waiting for the network to index it, and the comment it answers is left `replied: true` and `read: true` with `our_reply_external_id` pointing at it.\n\n**On Google Business this is an upsert.** A review has at most one reply, so replying again does not add a second one: it replaces the text of the one that is there, and the stored row is updated in place. Label your button accordingly — \"reply\" the first time, \"edit reply\" afterwards.\n\nThe text is validated against that network's limit before anything is sent (`comment_characters` in `GET /social_limits`); an empty or over-long text returns error `948` with the limit in `data.max`.\n\n**On X this costs credits**: 15, or 200 if the text contains a link, charged only on success.\n\nRequires the `comments:create` permission (`client_organization_comments:create` for apps) and a paid plan.",
|
|
3748
|
+
"description": "Posts a **public** reply on the social network. This is not the same as replying privately to whoever commented — that is a Messenger feature and lives in the messaging endpoints.\n\nThe reply is stored immediately, with `author.is_own: true`, without waiting for the network to index it, and the comment it answers is left `replied: true` and `read: true` with `our_reply_external_id` pointing at it.\n\n**On Google Business this is an upsert.** A review has at most one reply, so replying again does not add a second one: it replaces the text of the one that is there, and the stored row is updated in place. Label your button accordingly — \"reply\" the first time, \"edit reply\" afterwards.\n\nThe text is validated against that network's limit before anything is sent (`comment_characters` in `GET /social_limits`); an empty or over-long text returns error `948` with the limit in `data.max`.\n\n**On X this costs credits**: 15, or 200 if the text contains a link, charged only on success.\n\n**On `telegram` the reply is signed by the PlanVortex bot**, not by the channel. It is written in the channel's linked discussion group, which is where Telegram keeps the comments on a channel post — and a channel with no discussion group has no comments at all, which is error 965. Publishing is not affected: a channel post is signed by the channel.\n\nRequires the `comments:create` permission (`client_organization_comments:create` for apps) and a paid plan.",
|
|
3748
3749
|
"operationId": "replyComment",
|
|
3749
3750
|
"x-planvortex-identity": [
|
|
3750
3751
|
"current_user",
|
|
@@ -3885,7 +3886,7 @@
|
|
|
3885
3886
|
"comments"
|
|
3886
3887
|
],
|
|
3887
3888
|
"summary": "Delete the comment on the social network",
|
|
3888
|
-
"description": "Deletes it **on the network** and marks the row `deleted: true` here. The row is kept on purpose rather than removed: if it were removed, the next read of the thread — or the webhook, which repeats — would create it again.\n\nWhich comments you may delete depends on the network and on whose comment it is, and the two cases use different permissions of the network's own: `delete_own` for yours, `delete_others` for somebody else's. Instagram and X refuse the second; **on Google Business the only thing that can be deleted is your own reply**, never a review. Reading `GET /social_comment_actions` first is the difference between a button that works and one that always errors.\n\nOn X this costs credits.\n\nRequires the `comments:delete` permission (`client_organization_comments:delete` for apps) and a paid plan.",
|
|
3889
|
+
"description": "Deletes it **on the network** and marks the row `deleted: true` here. The row is kept on purpose rather than removed: if it were removed, the next read of the thread — or the webhook, which repeats — would create it again.\n\nWhich comments you may delete depends on the network and on whose comment it is, and the two cases use different permissions of the network's own: `delete_own` for yours, `delete_others` for somebody else's. Instagram and X refuse the second; **on Google Business the only thing that can be deleted is your own reply**, never a review. Reading `GET /social_comment_actions` first is the difference between a button that works and one that always errors.\n\nOn X this costs credits.\n\nOn `telegram` both are allowed by the network and both depend on a permission the customer controls: the bot has to be an administrator of the discussion group with the right to delete. When it is not, the answer is error 969 — which is the difference between \"this network cannot\" and \"this particular channel cannot\".\n\nRequires the `comments:delete` permission (`client_organization_comments:delete` for apps) and a paid plan.",
|
|
3889
3890
|
"operationId": "deleteComment",
|
|
3890
3891
|
"x-planvortex-identity": [
|
|
3891
3892
|
"current_user",
|
|
@@ -3930,7 +3931,7 @@
|
|
|
3930
3931
|
"catalog"
|
|
3931
3932
|
],
|
|
3932
3933
|
"summary": "What every network supports",
|
|
3933
|
-
"description": "The full matrix network → capabilities. It is the **single source of truth** about what a network can do: the catalogue publishes it, the backend checks it before acting, and your integration should filter its account pickers with it rather than keeping a list of its own.\n\n`comments` is the coarse gate — whether the network has comments at all. Which *actions* it allows on one is a finer question and lives in `GET /social_comment_actions`, because the shape here is `{[capability]: boolean}` and nesting an object inside would break it.\n\nToday
|
|
3934
|
+
"description": "The full matrix network → capabilities. It is the **single source of truth** about what a network can do: the catalogue publishes it, the backend checks it before acting, and your integration should filter its account pickers with it rather than keeping a list of its own.\n\n`comments` is the coarse gate — whether the network has comments at all. Which *actions* it allows on one is a finer question and lives in `GET /social_comment_actions`, because the shape here is `{[capability]: boolean}` and nesting an object inside would break it.\n\nToday nine networks answer `comments: true`: Facebook, Instagram, LinkedIn, X, YouTube, Google Business, Bluesky, Discord and Telegram. TikTok and WhatsApp answer `false`, for reasons of theirs and not ours.\n\nA `true` here is about the **network**, not about one account of it: a Telegram channel with no linked discussion group answers 965 on its comments even though the network has them.\n\nOnly needs authentication.",
|
|
3934
3935
|
"operationId": "getSocialCapabilities",
|
|
3935
3936
|
"x-planvortex-identity": [
|
|
3936
3937
|
"current_user",
|
|
@@ -3988,7 +3989,7 @@
|
|
|
3988
3989
|
"catalog"
|
|
3989
3990
|
],
|
|
3990
3991
|
"summary": "What each network lets you do to a comment",
|
|
3991
|
-
"description": "The fine-grained matrix: reply, hide, delete your own, delete somebody else's — network by network. Read it before painting controls. Networks without comments answer `false` to all four.\n\nThe differences are real and each has a reason in the network's own API:\n\n| Network | reply | hide | delete_own | delete_others |\n| --- | --- | --- | --- | --- |\n| `facebook` | yes | yes | yes | yes |\n| `instagram` | yes | yes | yes | **no** — Instagram only lets you hide someone else's |\n| `youtube` | yes | yes | yes | yes — the channel owner really does moderate |\n| `linkedin` | yes | **no** — there is no hide endpoint | yes | yes |\n| `twitter` | yes | yes | yes | **no** — you cannot delete another account's post |\n| `google_business` | yes | **no** | yes — **your reply**, never the review | **no** |\n| `bluesky` | yes | yes — through the post's `threadgate` | yes | **no** — the reply lives in somebody else's repository |\n| `discord` | yes | **no** — Discord has no hide, only delete | yes | yes |\n| `tiktok`, `whatsapp` | no | no | no | no |\n\nOnly needs authentication.",
|
|
3992
|
+
"description": "The fine-grained matrix: reply, hide, delete your own, delete somebody else's — network by network. Read it before painting controls. Networks without comments answer `false` to all four.\n\nThe differences are real and each has a reason in the network's own API:\n\n| Network | reply | hide | delete_own | delete_others |\n| --- | --- | --- | --- | --- |\n| `facebook` | yes | yes | yes | yes |\n| `instagram` | yes | yes | yes | **no** — Instagram only lets you hide someone else's |\n| `youtube` | yes | yes | yes | yes — the channel owner really does moderate |\n| `linkedin` | yes | **no** — there is no hide endpoint | yes | yes |\n| `twitter` | yes | yes | yes | **no** — you cannot delete another account's post |\n| `google_business` | yes | **no** | yes — **your reply**, never the review | **no** |\n| `bluesky` | yes | yes — through the post's `threadgate` | yes | **no** — the reply lives in somebody else's repository |\n| `discord` | yes | **no** — Discord has no hide, only delete | yes | yes |\n| `telegram` | yes | **no** — Telegram has no hide either | yes | yes — with the bot as an admin of the discussion group, otherwise error 969 |\n| `tiktok`, `whatsapp` | no | no | no | no |\n\nOnly needs authentication.",
|
|
3992
3993
|
"operationId": "getSocialCommentActions",
|
|
3993
3994
|
"x-planvortex-identity": [
|
|
3994
3995
|
"current_user",
|
|
@@ -7994,7 +7995,7 @@
|
|
|
7994
7995
|
"publications"
|
|
7995
7996
|
],
|
|
7996
7997
|
"summary": "Delete publication by identifier",
|
|
7997
|
-
"description": "Delete publication by identifier. It stops being readable by identifier afterwards, so deleting twice answers error 917. For an already-sent X (Twitter) publication, removing the tweet on X is a paid action that consumes 15 X credits; it is only removed on X when there are enough credits. Returns error 940 when the X credit pool is exhausted.",
|
|
7998
|
+
"description": "Delete publication by identifier. It stops being readable by identifier afterwards, so deleting twice answers error 917. For an already-sent X (Twitter) publication, removing the tweet on X is a paid action that consumes 15 X credits; it is only removed on X when there are enough credits. Returns error 940 when the X credit pool is exhausted.\n\n**On Telegram there is a 48-hour window.** Past it the Bot API refuses to delete a message whatever the bot's role is, and the answer is error 966 with `published_date` and `max_hours` in `data` — so the sensible thing is to grey the button out rather than offer it and fail. Error 969 is the other case: the bot is no longer allowed to delete there. And an album is several messages: all of them go, or the post would be left half-published in the channel.",
|
|
7998
7999
|
"operationId": "deletePublication",
|
|
7999
8000
|
"x-planvortex-identity": [
|
|
8000
8001
|
"current_user",
|
|
@@ -8012,7 +8013,7 @@
|
|
|
8012
8013
|
}
|
|
8013
8014
|
},
|
|
8014
8015
|
"400": {
|
|
8015
|
-
"description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `917` | Publication doesn't exists |\n| `935` | Invalid publication |\n| `940` | X (Twitter) credits exhausted. Deleting the tweet on X could not be charged. Response data: { used, limit }. |\n| `523` | Invalid application |",
|
|
8016
|
+
"description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `917` | Publication doesn't exists |\n| `935` | Invalid publication |\n| `940` | X (Twitter) credits exhausted. Deleting the tweet on X could not be charged. Response data: { used, limit }. |\n| `966` | The Telegram message is older than 48 hours and the Bot API will not delete it any more. Response data: { published_date, max_hours }. |\n| `969` | The PlanVortex bot is not allowed to delete messages in that Telegram chat. Response data: { chat_id }. |\n| `523` | Invalid application |",
|
|
8016
8017
|
"content": {
|
|
8017
8018
|
"application/json": {
|
|
8018
8019
|
"schema": {
|
|
@@ -9102,7 +9103,7 @@
|
|
|
9102
9103
|
"schemas": {
|
|
9103
9104
|
"Account": {
|
|
9104
9105
|
"type": "object",
|
|
9105
|
-
"description": "A social account connected to an organization.\n\nOn `discord` an account is a **channel**, not a profile: publishing to two channels of the same server costs two accounts of the plan.\n\n`error_code` other than `0` means the connection is broken — an expired token, a permission taken away — and the account has to be connected again.",
|
|
9106
|
+
"description": "A social account connected to an organization.\n\nOn `discord` and on `telegram` an account is a **channel**, not a profile: publishing to two Discord channels of the same server — or to two Telegram channels of the same brand — costs two accounts of the plan.\n\n`error_code` other than `0` means the connection is broken — an expired token, a permission taken away — and the account has to be connected again. On `telegram` nothing expires, because there is no account token: what breaks the connection is the bot being removed from the channel or losing its permission to post there (error 968).",
|
|
9106
9107
|
"required": [
|
|
9107
9108
|
"_id",
|
|
9108
9109
|
"id_organization",
|
|
@@ -9129,7 +9130,7 @@
|
|
|
9129
9130
|
},
|
|
9130
9131
|
"username": {
|
|
9131
9132
|
"type": "string",
|
|
9132
|
-
"description": "The handle, when the network has one. Absent on the networks that do not (a Discord channel, a WhatsApp number, a Google Business listing)."
|
|
9133
|
+
"description": "The handle, when the network has one. Absent on the networks that do not (a Discord channel, a WhatsApp number, a Google Business listing) and on a **private** Telegram channel, which has no `@name` at all — only public ones do. That is also why a private channel's publications come back with no `url`."
|
|
9133
9134
|
},
|
|
9134
9135
|
"social_network": {
|
|
9135
9136
|
"allOf": [
|
|
@@ -9153,7 +9154,7 @@
|
|
|
9153
9154
|
},
|
|
9154
9155
|
"followers_count": {
|
|
9155
9156
|
"type": "integer",
|
|
9156
|
-
"description": "Followers the network reports. Absent on an account that has never been measured."
|
|
9157
|
+
"description": "Followers the network reports. Absent on an account that has never been measured. On `telegram` it is the channel's member count, and it is the **only** audience figure that network publishes: there are no views, no impressions and no reach anywhere in the Bot API."
|
|
9157
9158
|
},
|
|
9158
9159
|
"next_stats_update": {
|
|
9159
9160
|
"type": "string",
|
|
@@ -9354,15 +9355,16 @@
|
|
|
9354
9355
|
"required": [
|
|
9355
9356
|
"type"
|
|
9356
9357
|
],
|
|
9357
|
-
"description": "**How** an account of this network is authorized, which is not always \"send the user to this URL\".\n\nNine of the
|
|
9358
|
+
"description": "**How** an account of this network is authorized, which is not always \"send the user to this URL\".\n\nNine of the eleven networks are `redirect`: open `link` and the network sends the person back to PlanVortex with a code. Two are not:\n\n• **WhatsApp.** Its sign-up is Meta's *Embedded Signup*: a popup raised by the Facebook JavaScript SDK from your own page, which returns — over `postMessage` — session data (`waba_id`, `phone_number_id`) that no query string carries. Its `link` is therefore an empty string.\n• **Telegram.** There is no OAuth here: no consent screen, no `code`, no account token. `link` opens a private chat with the PlanVortex bot, the person then adds that bot to their channel, and **the account is created from that event**, not from any request of yours. Which means the connection cannot be finished by calling `GET /organizations/{id_organization}/account-connect/telegram` — see that endpoint.\n\nBranch on `authorization.type`, never on whether `link` is empty.",
|
|
9358
9359
|
"properties": {
|
|
9359
9360
|
"type": {
|
|
9360
9361
|
"type": "string",
|
|
9361
9362
|
"enum": [
|
|
9362
9363
|
"redirect",
|
|
9363
|
-
"meta_embedded_signup"
|
|
9364
|
+
"meta_embedded_signup",
|
|
9365
|
+
"telegram_bot"
|
|
9364
9366
|
],
|
|
9365
|
-
"description": "`redirect`: send the user to `link`. `meta_embedded_signup`: open the Meta popup with the fields below."
|
|
9367
|
+
"description": "`redirect`: send the user to `link`. `meta_embedded_signup`: open the Meta popup with the fields below. `telegram_bot`: open `link` in another tab and wait for the account to show up."
|
|
9366
9368
|
},
|
|
9367
9369
|
"app_id": {
|
|
9368
9370
|
"type": "string",
|
|
@@ -9383,6 +9385,14 @@
|
|
|
9383
9385
|
"session_info_version": {
|
|
9384
9386
|
"type": "string",
|
|
9385
9387
|
"description": "`meta_embedded_signup` only. Goes to `extras.sessionInfoVersion`."
|
|
9388
|
+
},
|
|
9389
|
+
"bot_username": {
|
|
9390
|
+
"type": "string",
|
|
9391
|
+
"description": "`telegram_bot` only. The bot's `@name`, **without the at sign**. Published here so you never have to write it by hand: it is the name the person will see in Telegram, and it changes with the deployment."
|
|
9392
|
+
},
|
|
9393
|
+
"add_to_group_link": {
|
|
9394
|
+
"type": "string",
|
|
9395
|
+
"description": "`telegram_bot` only. The **second** step, and it does not follow from the first: `link` opens the list of channels and this one opens the list of groups. It adds the bot to the channel's linked discussion group, which is what turns comments on — a Telegram channel with no discussion group has no comment inbox at all (error 965). Optional for the user: publishing and statistics work without it."
|
|
9386
9396
|
}
|
|
9387
9397
|
}
|
|
9388
9398
|
},
|
|
@@ -9400,7 +9410,7 @@
|
|
|
9400
9410
|
},
|
|
9401
9411
|
"link": {
|
|
9402
9412
|
"type": "string",
|
|
9403
|
-
"description": "The network's authorization URL. Send the user there. **Empty when `authorization.type` is
|
|
9413
|
+
"description": "The network's authorization URL. Send the user there. **Empty when `authorization.type` is `meta_embedded_signup`** — WhatsApp has no URL to give at all.\n\nOn `telegram_bot` it is filled in, and it still is not somewhere to redirect: it opens a Telegram chat, not an authorization screen. Open it in another tab."
|
|
9404
9414
|
},
|
|
9405
9415
|
"authorization": {
|
|
9406
9416
|
"$ref": "#/components/schemas/AccountsSocialAuthorizationMethod"
|
|
@@ -10039,7 +10049,7 @@
|
|
|
10039
10049
|
"$ref": "#/components/schemas/CatalogSocialLimitsMap"
|
|
10040
10050
|
}
|
|
10041
10051
|
],
|
|
10042
|
-
"description": "Maximum length of a publication's text. Bluesky counts graphemes, everyone else counts characters."
|
|
10052
|
+
"description": "Maximum length of a publication's text. Bluesky counts graphemes, everyone else counts characters — Telegram included, where `String.length` is exactly the right unit.\n\nOn Telegram there are **two numbers for the same field**: `telegram` (4.096) while the publication is text only, and `telegram_media` (1.024) the moment it carries an image or a video, because then the text is a media caption and not a message. Switch the counter when the file is attached, not when publish is pressed."
|
|
10043
10053
|
},
|
|
10044
10054
|
"max_post_bytes": {
|
|
10045
10055
|
"allOf": [
|
|
@@ -10102,7 +10112,7 @@
|
|
|
10102
10112
|
},
|
|
10103
10113
|
"CatalogSocialLimitsMap": {
|
|
10104
10114
|
"type": "object",
|
|
10105
|
-
"description": "One number per network. Every network in `/social_networks` is present.",
|
|
10115
|
+
"description": "One number per network. Every network in `/social_networks` is present.\n\n**And a few keys are not a network.** Some limits depend on the *kind* of publication rather than on the network alone, and those get a compound key next to the plain one: `instagram_story`, `facebook_reel`, `telegram_media`. Read the plain key by default and the compound one when it applies.",
|
|
10106
10116
|
"additionalProperties": {
|
|
10107
10117
|
"type": "integer"
|
|
10108
10118
|
}
|
|
@@ -10637,11 +10647,11 @@
|
|
|
10637
10647
|
},
|
|
10638
10648
|
"publication_external_id": {
|
|
10639
10649
|
"type": "string",
|
|
10640
|
-
"description": "What the comment hangs off, on the network: the post/video id in
|
|
10650
|
+
"description": "What the comment hangs off, on the network: the post/video id in most networks, the **listing** (`locations/{id}`) for a Google Business review, and the published message's id on Telegram — where the thread that holds the comments *is* the forwarded post."
|
|
10641
10651
|
},
|
|
10642
10652
|
"external_id": {
|
|
10643
10653
|
"type": "string",
|
|
10644
|
-
"description": "The comment's id on the network. Unique per account, and what makes repeated webhook deliveries idempotent.\n\
|
|
10654
|
+
"description": "The comment's id on the network. Unique per account, and what makes repeated webhook deliveries idempotent.\n\nTwo exceptions worth knowing, and both are composite ids you should treat as opaque:\n\n• A Google Business reply has no id of its own — it is a *field* of the review — so PlanVortex fabricates a stable one, `{reviewId}/reply`.\n• A Telegram comment lives in a **different chat** from the post it answers (the channel's linked discussion group), so it needs two ids at once and travels as `{thread}/{message}`: replying wants the first, deleting wants the second."
|
|
10645
10655
|
},
|
|
10646
10656
|
"parent_external_id": {
|
|
10647
10657
|
"type": "string",
|
|
@@ -10781,7 +10791,8 @@
|
|
|
10781
10791
|
"youtube",
|
|
10782
10792
|
"google_business",
|
|
10783
10793
|
"bluesky",
|
|
10784
|
-
"discord"
|
|
10794
|
+
"discord",
|
|
10795
|
+
"telegram"
|
|
10785
10796
|
]
|
|
10786
10797
|
},
|
|
10787
10798
|
"CommentsCommentThread": {
|
|
@@ -10884,7 +10895,7 @@
|
|
|
10884
10895
|
"properties": {
|
|
10885
10896
|
"field": {
|
|
10886
10897
|
"type": "string",
|
|
10887
|
-
"description": "What kind of change this is. **Treat it as an open list** and ignore what you do not handle: it grows with the product.\n\n- `new_account` / `change_state_account`: an account was connected, or its state changed — it stopped working, its token was refreshed, it was disconnected.\n- `messages`: a message came in. It travels in `messageObj`.\n- `messaging_postbacks`: the contact pressed a button or a quick reply. Also in `messageObj`.\n- `messaging_seen`: the contact read the conversation. `messageObj` carries the message they read, when we still have it.\n- `messaging_error`: the network refused a message we sent. The reason is in `messageObj.message_errors`.\n- `comments`: a comment came in. It travels in `commentObj`, never in `messageObj`.",
|
|
10898
|
+
"description": "What kind of change this is. **Treat it as an open list** and ignore what you do not handle: it grows with the product.\n\n- `new_account` / `change_state_account`: an account was connected, or its state changed — it stopped working, its token was refreshed, it was disconnected. On `telegram` this is the **only** way to hear about a connection: there is no callback there, so no request of yours ever returns that account.\n- `messages`: a message came in. It travels in `messageObj`.\n- `messaging_postbacks`: the contact pressed a button or a quick reply. Also in `messageObj`.\n- `messaging_seen`: the contact read the conversation. `messageObj` carries the message they read, when we still have it.\n- `messaging_error`: the network refused a message we sent. The reason is in `messageObj.message_errors`.\n- `comments`: a comment came in. It travels in `commentObj`, never in `messageObj`.",
|
|
10888
10899
|
"enum": [
|
|
10889
10900
|
"new_account",
|
|
10890
10901
|
"change_state_account",
|
|
@@ -13200,13 +13211,18 @@
|
|
|
13200
13211
|
"type": "integer",
|
|
13201
13212
|
"description": "Manual retries already spent on a failed publication, against the `max_retries` published by `GET /publication_limits`. Only `POST .../retry` increases it; updating the publication resets it to 0."
|
|
13202
13213
|
},
|
|
13214
|
+
"extra_data": {
|
|
13215
|
+
"type": "object",
|
|
13216
|
+
"additionalProperties": true,
|
|
13217
|
+
"description": "What one network needs to remember about **this** publication and that has no common field. Absent on a publication whose network needs nothing, which is almost all of them.\n\nToday only `telegram` writes here, and only `telegram_message_ids`: the ids of every message an album turned into, because deleting the album means deleting all of them."
|
|
13218
|
+
},
|
|
13203
13219
|
"external_identifier": {
|
|
13204
13220
|
"type": "string",
|
|
13205
|
-
"description": "The network's own identifier, once published."
|
|
13221
|
+
"description": "The network's own identifier, once published. On `telegram` an album is one publication that is **several messages**, and this holds the first one; the rest travel in `extra_data.telegram_message_ids`."
|
|
13206
13222
|
},
|
|
13207
13223
|
"url": {
|
|
13208
13224
|
"type": "string",
|
|
13209
|
-
"description": "Link to the publication on the network, when there is one."
|
|
13225
|
+
"description": "Link to the publication on the network, when there is one. A **private** Telegram channel has no public URL, so it comes back empty even though the post went out."
|
|
13210
13226
|
},
|
|
13211
13227
|
"statistics": {
|
|
13212
13228
|
"$ref": "#/components/schemas/PublicationStats"
|
|
@@ -13244,7 +13260,7 @@
|
|
|
13244
13260
|
},
|
|
13245
13261
|
"PublicationStats": {
|
|
13246
13262
|
"type": "object",
|
|
13247
|
-
"description": "Raw, per-network metrics for a publication. Only the fields that belong to the publication's own social network are returned.\n\n**An absent field is not a zero.** A field is present only when the network actually reported it; `0` means the network measured zero. This matters most on X (Twitter), where metrics are split into groups with different access levels: `public_metrics` (likes, replys, retwets, quotes, bookmarks, impressions) is always available, while clicks, the pre-computed `engagement` and the video playback quartiles come from X's non-public metrics — only for your own posts, within 30 days of publishing, and only if the app is entitled to them. When they are unavailable they are omitted rather than returned as `0`. Do not default missing fields to zero when displaying them.\n\nOn `discord` there are only two: `likes` (the reactions on the message) and `comments` (the messages in its thread). There is no impressions figure anywhere in Discord's API, so engagement is computed over the server's member count.\n\nOn `bluesky` there are no impressions and no reach either — only the public counters — so engagement is computed over followers.",
|
|
13263
|
+
"description": "Raw, per-network metrics for a publication. Only the fields that belong to the publication's own social network are returned.\n\n**An absent field is not a zero.** A field is present only when the network actually reported it; `0` means the network measured zero. This matters most on X (Twitter), where metrics are split into groups with different access levels: `public_metrics` (likes, replys, retwets, quotes, bookmarks, impressions) is always available, while clicks, the pre-computed `engagement` and the video playback quartiles come from X's non-public metrics — only for your own posts, within 30 days of publishing, and only if the app is entitled to them. When they are unavailable they are omitted rather than returned as `0`. Do not default missing fields to zero when displaying them.\n\nOn `discord` there are only two: `likes` (the reactions on the message) and `comments` (the messages in its thread). There is no impressions figure anywhere in Discord's API, so engagement is computed over the server's member count.\n\nOn `bluesky` there are no impressions and no reach either — only the public counters — so engagement is computed over followers.\n\nOn `telegram` there are two as well, and **neither of them is asked for**: the Bot API has no method that returns a message's metrics, so `reactions` arrives on its own through the bot and `comments` is counted in PlanVortex's own inbox. There are no impressions, no reach, no views and no forwards to be had anywhere in it, so engagement is computed over followers.",
|
|
13248
13264
|
"properties": {
|
|
13249
13265
|
"likes": {
|
|
13250
13266
|
"type": "integer"
|
|
@@ -13353,6 +13369,17 @@
|
|
|
13353
13369
|
},
|
|
13354
13370
|
"views": {
|
|
13355
13371
|
"type": "integer"
|
|
13372
|
+
},
|
|
13373
|
+
"reactions": {
|
|
13374
|
+
"type": "integer",
|
|
13375
|
+
"description": "Telegram. Every reaction on the post, all emoji together. It is the **complete state and not an increment**: it goes down when somebody takes theirs back. Normalised as `likes`."
|
|
13376
|
+
},
|
|
13377
|
+
"reactions_by_emoji": {
|
|
13378
|
+
"type": "object",
|
|
13379
|
+
"additionalProperties": {
|
|
13380
|
+
"type": "integer"
|
|
13381
|
+
},
|
|
13382
|
+
"description": "Telegram. The same total broken down by emoji. Reactions with a custom emoji are grouped under a single key: their identifier means nothing outside the server that created it."
|
|
13356
13383
|
}
|
|
13357
13384
|
}
|
|
13358
13385
|
},
|
|
@@ -13371,13 +13398,14 @@
|
|
|
13371
13398
|
"whatsapp",
|
|
13372
13399
|
"youtube",
|
|
13373
13400
|
"bluesky",
|
|
13374
|
-
"discord"
|
|
13401
|
+
"discord",
|
|
13402
|
+
"telegram"
|
|
13375
13403
|
],
|
|
13376
13404
|
"description": "Network the publication targets. **Required when creating**: the request fails with error 702 if it is missing or not one of these values. It must match the network of the account in the path.\n\nNot every connectable network publishes — a local business listing receives reviews, not posts — so this list is shorter than the one in `GET /social_networks`. Ask `GET /allowed_social_publications` rather than hardcoding it, because it grows."
|
|
13377
13405
|
},
|
|
13378
13406
|
"text": {
|
|
13379
13407
|
"type": "string",
|
|
13380
|
-
"description": "Body text of the publication. Either `text` or at least one entry in `files` is required: if both are empty the publication is still created, but in state `withErrors` with `publication_errors[].code = 915`. Maximum length depends on the network. On YouTube this is the video **description** (5,000 characters), and the publication must carry exactly one video file and no images — otherwise it is created in state `withErrors` with `publication_errors[].code = 943`. For X (Twitter), a text containing a link costs 200 credits instead of 15."
|
|
13408
|
+
"description": "Body text of the publication. Either `text` or at least one entry in `files` is required: if both are empty the publication is still created, but in state `withErrors` with `publication_errors[].code = 915`. Maximum length depends on the network. On YouTube this is the video **description** (5,000 characters), and the publication must carry exactly one video file and no images — otherwise it is created in state `withErrors` with `publication_errors[].code = 943`. For X (Twitter), a text containing a link costs 200 credits instead of 15.\n\n**On Telegram the limit depends on what else the publication carries**: 4.096 characters while it is text only, and **1.024** the moment it has an image or a video, because then the text is the caption of a photo, a video or an album and no longer a message. Over the limit it is created in state `withErrors` with `publication_errors[].code = 967`, whose `data` carries `characters`, `max_characters` and `has_media`. Both numbers are published, as `characters.telegram` and `characters.telegram_media` in `GET /social_limits`."
|
|
13381
13409
|
},
|
|
13382
13410
|
"title": {
|
|
13383
13411
|
"type": "string",
|
|
@@ -13713,7 +13741,8 @@
|
|
|
13713
13741
|
"youtube",
|
|
13714
13742
|
"google_business",
|
|
13715
13743
|
"bluesky",
|
|
13716
|
-
"discord"
|
|
13744
|
+
"discord",
|
|
13745
|
+
"telegram"
|
|
13717
13746
|
],
|
|
13718
13747
|
"type": "string",
|
|
13719
13748
|
"description": "A social network supported by PlanVortex.\n\n**This list grows.** Treat it as an open enumeration: a client that rejects an unknown value breaks the day a network is added, which happens several times a year. Not every network does everything — ask `GET /social_capabilities`."
|
|
@@ -14270,7 +14299,7 @@
|
|
|
14270
14299
|
}
|
|
14271
14300
|
},
|
|
14272
14301
|
"CommentsCommentError": {
|
|
14273
|
-
"description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `516` | Comments require a paid plan. Every endpoint here returns this on the free plan. |\n| `700` | The account is not connected — no valid token, or nothing to ask the network about. |\n| `936` | The publication was never sent, so it has no thread to read. |\n| `940` | The client's monthly X credits are exhausted. Only X. |\n| `945` | This network has no comments (TikTok, WhatsApp). Carries `social_network` in `data`. |\n| `946` | The network has comments but does not allow **this action**: hiding on LinkedIn or Google Business, deleting someone else's on Instagram or X, deleting a review. Carries `social_network` and `action` in `data`. See `GET /social_comment_actions`. |\n| `947` | The comment no longer exists on the network. What is stored is a photograph; the network always wins. Carries `external_id` in `data`. |\n| `948` | The reply is empty or longer than the network allows. Carries the limit in `data.max`; the same number is published in `comment_characters` of `GET /social_limits`. |\n| `951` | The Google Business listing is not verified, so it cannot reply to its reviews. It is the state of the customer's profile, not of your token. |\n| `952` | PlanVortex's Google Cloud project has no approved access to the Google Business API yet. Until it does, reviews cannot be read or replied to. |\n| `1101` | Invalid organization. |\n| `1501` | The comment's account could not be resolved. |",
|
|
14302
|
+
"description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `516` | Comments require a paid plan. Every endpoint here returns this on the free plan. |\n| `700` | The account is not connected — no valid token, or nothing to ask the network about. |\n| `936` | The publication was never sent, so it has no thread to read. |\n| `940` | The client's monthly X credits are exhausted. Only X. |\n| `945` | This network has no comments (TikTok, WhatsApp). Carries `social_network` in `data`. |\n| `946` | The network has comments but does not allow **this action**: hiding on LinkedIn or Google Business, deleting someone else's on Instagram or X, deleting a review. Carries `social_network` and `action` in `data`. See `GET /social_comment_actions`. |\n| `947` | The comment no longer exists on the network. What is stored is a photograph; the network always wins. Carries `external_id` in `data`. |\n| `948` | The reply is empty or longer than the network allows. Carries the limit in `data.max`; the same number is published in `comment_characters` of `GET /social_limits`. |\n| `951` | The Google Business listing is not verified, so it cannot reply to its reviews. It is the state of the customer's profile, not of your token. |\n| `952` | PlanVortex's Google Cloud project has no approved access to the Google Business API yet. Until it does, reviews cannot be read or replied to. |\n| `965` | This Telegram channel has no linked discussion group, so it has no comments. The network gate says `true`; what is missing is a setting of **that channel**, which its owner fixes in two taps. Carries `chat_id` in `data`. |\n| `969` | The PlanVortex bot is not allowed to delete messages in this Telegram discussion group. The network allows it; this installation does not. Carries `chat_id` in `data`. |\n| `970` | Telegram is rate limiting the bot and the wait was longer than PlanVortex is willing to hold the request for. Carries `retry_after_seconds` in `data`: retry after it. |\n| `1101` | Invalid organization. |\n| `1501` | The comment's account could not be resolved. |",
|
|
14274
14303
|
"content": {
|
|
14275
14304
|
"application/json": {
|
|
14276
14305
|
"schema": {
|
|
@@ -9,7 +9,7 @@ name = "planvortex"
|
|
|
9
9
|
# entonces `project.version` desapareceria del pyproject y el paso del workflow de release que
|
|
10
10
|
# compara el tag con la version —que lo lee con `tomllib`— dejaria de encontrarla. Se quedan las dos
|
|
11
11
|
# y las ata `tests/test_packaging.py`, que falla si se separan.
|
|
12
|
-
version = "0.
|
|
12
|
+
version = "0.2.0"
|
|
13
13
|
description = "Official Python client for the PlanVortex API"
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
requires-python = ">=3.10"
|