planvortex 0.12.0__tar.gz → 0.13.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.12.0 → planvortex-0.13.0}/CHANGELOG.md +29 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/PKG-INFO +8 -2
- {planvortex-0.12.0 → planvortex-0.13.0}/README.md +7 -1
- {planvortex-0.12.0 → planvortex-0.13.0}/openapi/planvortex.openapi.json +132 -14
- {planvortex-0.12.0 → planvortex-0.13.0}/pyproject.toml +1 -1
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/_generated/models.py +81 -4
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/_version.py +1 -1
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources/ai_plans.py +10 -6
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources_sync/ai_plans.py +6 -4
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/types.py +13 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/webhooks.py +61 -2
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_example_webhooks.py +18 -5
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_types.py +18 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_webhooks.py +90 -4
- {planvortex-0.12.0 → planvortex-0.13.0}/.gitignore +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/LICENSE +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/scripts/build_docs.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/scripts/check_packaging.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/scripts/generate_models.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/scripts/generate_sync.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/scripts/route_coverage.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/__init__.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/_client.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/_client_sync.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/_core/__init__.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/_core/auth.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/_core/auth_sync.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/_core/errors.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/_core/files.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/_core/http.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/_core/http_sync.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/_core/pagination.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/_core/query.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/_core/transport.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/_generated/__init__.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/_shapes.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/py.typed +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources/__init__.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources/accounts.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources/apps.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources/base.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources/catalog.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources/clients.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources/comments.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources/contacts.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources/dashboard.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources/integrations.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources/messages.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources/organizations.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources/products.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources/publications.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources/uploads.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources_sync/__init__.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources_sync/accounts.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources_sync/apps.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources_sync/base.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources_sync/catalog.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources_sync/clients.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources_sync/comments.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources_sync/contacts.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources_sync/dashboard.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources_sync/integrations.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources_sync/messages.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources_sync/organizations.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources_sync/products.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources_sync/publications.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/src/planvortex/resources_sync/uploads.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/__init__.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/conftest.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/contrato.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/live/__init__.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/live/conftest.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/live/test_auth.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/live/test_catalog.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/live/test_connect_flow.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/live/test_errors.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/live/test_publish.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/live/test_read.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_accounts.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_ai_plans.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_apps.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_catalog.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_client.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_clients.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_comments.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_contacts.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_core.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_dashboard.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_example_comments.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_example_connect.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_example_publish.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_example_schedule.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_files.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_generate_models.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_generate_sync.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_integrations.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_messages.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_models.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_openapi_freshness.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_organizations.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_packaging.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_pagination.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_products.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_publications.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_query_parity.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_readme.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_route_coverage.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_shapes_parity.py +0 -0
- {planvortex-0.12.0 → planvortex-0.13.0}/tests/test_uploads.py +0 -0
|
@@ -4,6 +4,35 @@ 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.13.0] - 2026-10-03
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Two webhook events for AI plans: `ai_plan_generated` and `ai_plan_failed`**, typed as
|
|
12
|
+
`AiPlanChange` (with `AiPlanFailure` for its `error`) and narrowed with `is_ai_plan_change` in
|
|
13
|
+
`planvortex.webhooks`. When a plan finishes, your app's `webhook_url` now hears about it, so you
|
|
14
|
+
no longer have to poll `pv.ai_plans.get` every few seconds. The change carries ids and numbers
|
|
15
|
+
only (`id_ai_plan`, `state`, `template`, `total_publications`, `credits_spent`, how many
|
|
16
|
+
`warnings`, and `error` with its `code` and `message` on a failure), never the prompt or the
|
|
17
|
+
generated text: read the plan once when it arrives. Each plan sends one of the two, never a
|
|
18
|
+
`failed` followed by a `generated`. Deliveries are not retried, so keep polling as the fallback
|
|
19
|
+
if your endpoint can be down.
|
|
20
|
+
|
|
21
|
+
- **`pending_publish` on `Publication`**, with the `PublicationPending` type to name it. An
|
|
22
|
+
Instagram video that Meta takes more than ~30 seconds to process now comes back in state
|
|
23
|
+
`publishing` with this object, and PlanVortex keeps asking Instagram in the background for up to
|
|
24
|
+
10 minutes. **Wait and read the publication again after `next_check`; do not retry it or create
|
|
25
|
+
it again**, or the video goes out twice. While it is present the publication cannot be updated
|
|
26
|
+
(error 921). It is read-only: nothing new to send.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- The reference no longer says that an X publication left without credits (940) fires your app's
|
|
31
|
+
webhook. It never did: no webhook announces publications, so read the publication to find out.
|
|
32
|
+
- `publication_errors` documents two new Instagram codes: **998** (Meta rejected the file, so
|
|
33
|
+
retrying it unchanged fails the same way) and **999** (Meta ran out of time, and a retry usually
|
|
34
|
+
works).
|
|
35
|
+
|
|
7
36
|
## [0.12.0] - 2026-10-02
|
|
8
37
|
|
|
9
38
|
**Your app can show its own account picker.** Until now the person connecting a social account
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: planvortex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: Official Python client for the PlanVortex API
|
|
5
5
|
Project-URL: Homepage, https://planvortex.com/en/developers
|
|
6
6
|
Project-URL: Documentation, https://taliasoftworks.github.io/PlanVortexPython/
|
|
@@ -369,13 +369,19 @@ would rather do it in two steps, `verify_webhook_signature(payload, signature, s
|
|
|
369
369
|
plain `True`/`False` and `parse_webhook_body(payload)` gives you the changes.
|
|
370
370
|
|
|
371
371
|
Narrow with the predicates — `is_account_state_change`, `is_message_change`, `is_comment_change`,
|
|
372
|
-
`is_integration_error_change` — and let anything else fall through: **the event list grows**, and a
|
|
372
|
+
`is_integration_error_change`, `is_ai_plan_change` — and let anything else fall through: **the event list grows**, and a
|
|
373
373
|
`field` this release has never heard of is not an error.
|
|
374
374
|
|
|
375
375
|
**PlanVortex does not retry a failed delivery.** A 500 of yours loses the event, so if your work is
|
|
376
376
|
slow, queue it and answer — and use `pv.comments.list` / `pv.messages.list` to catch up on anything
|
|
377
377
|
you missed.
|
|
378
378
|
|
|
379
|
+
**An AI plan tells you when it is done.** `ai_plan_generated` and `ai_plan_failed` arrive when a plan
|
|
380
|
+
finishes, so you do not have to poll `pv.ai_plans.get` every few seconds: read the plan once when
|
|
381
|
+
one of them comes in. They carry ids and numbers only (`id_ai_plan`, `state`, how many `warnings`,
|
|
382
|
+
and `error` on a failure), never the prompt or the generated text. If your endpoint was down,
|
|
383
|
+
`pv.ai_plans.get` still tells you how the plan ended.
|
|
384
|
+
|
|
379
385
|
## The whole API, in fourteen resources
|
|
380
386
|
|
|
381
387
|
`pv.catalog` · `pv.clients` · `pv.organizations` · `pv.accounts` · `pv.uploads` · `pv.publications`
|
|
@@ -339,13 +339,19 @@ would rather do it in two steps, `verify_webhook_signature(payload, signature, s
|
|
|
339
339
|
plain `True`/`False` and `parse_webhook_body(payload)` gives you the changes.
|
|
340
340
|
|
|
341
341
|
Narrow with the predicates — `is_account_state_change`, `is_message_change`, `is_comment_change`,
|
|
342
|
-
`is_integration_error_change` — and let anything else fall through: **the event list grows**, and a
|
|
342
|
+
`is_integration_error_change`, `is_ai_plan_change` — and let anything else fall through: **the event list grows**, and a
|
|
343
343
|
`field` this release has never heard of is not an error.
|
|
344
344
|
|
|
345
345
|
**PlanVortex does not retry a failed delivery.** A 500 of yours loses the event, so if your work is
|
|
346
346
|
slow, queue it and answer — and use `pv.comments.list` / `pv.messages.list` to catch up on anything
|
|
347
347
|
you missed.
|
|
348
348
|
|
|
349
|
+
**An AI plan tells you when it is done.** `ai_plan_generated` and `ai_plan_failed` arrive when a plan
|
|
350
|
+
finishes, so you do not have to poll `pv.ai_plans.get` every few seconds: read the plan once when
|
|
351
|
+
one of them comes in. They carry ids and numbers only (`id_ai_plan`, `state`, how many `warnings`,
|
|
352
|
+
and `error` on a failure), never the prompt or the generated text. If your endpoint was down,
|
|
353
|
+
`pv.ai_plans.get` still tells you how the plan ended.
|
|
354
|
+
|
|
349
355
|
## The whole API, in fourteen resources
|
|
350
356
|
|
|
351
357
|
`pv.catalog` · `pv.clients` · `pv.organizations` · `pv.accounts` · `pv.uploads` · `pv.publications`
|
|
@@ -1359,7 +1359,7 @@
|
|
|
1359
1359
|
"ai_plans"
|
|
1360
1360
|
],
|
|
1361
1361
|
"summary": "Create an AI publication plan",
|
|
1362
|
-
"description": "Create a weekly AI publication plan. The plan is queued in state 'pending' and generated asynchronously by the generate-ai-plans job; poll GET by id while state is pending or generating. Validations at creation: the client plan allows AI (it contracts AI credits; Free does not), the accounts belong to the organization, there are enough AI credits for the deterministic base cost (orchestration + target texts), and there is room in the organization's monthly publication limit. Returns the created plan together with the deterministic cost estimate. Requires the ai_plans:create permission. Since the plan can be generated from a SOURCE (`template` + `source`), part of that validation is the source itself: the article is downloaded, the catalogue is read live and the product pictures are copied — all of it inside this request, so what does not work fails with the user in front of it. A plan whose source did not fit in the week is still created, and says so in ai_plan.warnings (2117). With a Pinterest account in the plan, two more checks run at creation: every Pinterest account needs its board in `destinations` (2118), and a configuration that would leave pins without an image is refused (2119).",
|
|
1362
|
+
"description": "Create a weekly AI publication plan. The plan is queued in state 'pending' and generated asynchronously by the generate-ai-plans job. When it finishes, your app's `webhook_url` receives `ai_plan_generated` or `ai_plan_failed` (the payload is in the comments specification, `AiPlanWebhookChange`); without a webhook, poll GET by id while state is pending or generating. The webhook is best effort and not retried, so the GET is still the source of truth. Validations at creation: the client plan allows AI (it contracts AI credits; Free does not), the accounts belong to the organization, there are enough AI credits for the deterministic base cost (orchestration + target texts), and there is room in the organization's monthly publication limit. Returns the created plan together with the deterministic cost estimate. Requires the ai_plans:create permission. Since the plan can be generated from a SOURCE (`template` + `source`), part of that validation is the source itself: the article is downloaded, the catalogue is read live and the product pictures are copied — all of it inside this request, so what does not work fails with the user in front of it. A plan whose source did not fit in the week is still created, and says so in ai_plan.warnings (2117). With a Pinterest account in the plan, two more checks run at creation: every Pinterest account needs its board in `destinations` (2118), and a configuration that would leave pins without an image is refused (2119).",
|
|
1363
1363
|
"operationId": "addAiPlan",
|
|
1364
1364
|
"x-planvortex-identity": [
|
|
1365
1365
|
"current_user",
|
|
@@ -1674,7 +1674,7 @@
|
|
|
1674
1674
|
"ai_plans"
|
|
1675
1675
|
],
|
|
1676
1676
|
"summary": "Get an AI publication plan by id",
|
|
1677
|
-
"description": "Return a single AI plan with its generated publications populated.
|
|
1677
|
+
"description": "Return a single AI plan with its generated publications populated. Read it when your app receives `ai_plan_generated` or `ai_plan_failed`. Without a webhook, this is the endpoint to poll while state is pending or generating. The webhook is not retried, so if your endpoint was down when the plan finished, this GET still tells you how it ended. Requires the ai_plans:read permission.",
|
|
1678
1678
|
"operationId": "getAiPlan",
|
|
1679
1679
|
"x-planvortex-identity": [
|
|
1680
1680
|
"current_user",
|
|
@@ -1873,7 +1873,7 @@
|
|
|
1873
1873
|
"ai_plans"
|
|
1874
1874
|
],
|
|
1875
1875
|
"summary": "Retry a failed AI publication plan",
|
|
1876
|
-
"description": "Re-queue a failed plan: it goes back to state 'pending' (attempts and error reset) and the background job regenerates it with the SAME data (prompt, accounts, options).
|
|
1876
|
+
"description": "Re-queue a failed plan: it goes back to state 'pending' (attempts and error reset) and the background job regenerates it with the SAME data (prompt, accounts, options). As after creation, the end arrives on your webhook as `ai_plan_generated` or `ai_plan_failed`; without one, poll the plan while state is pending or generating. Only valid from state 'failed'. Already-spent credits are NOT refunded and a new generation spends again; drafts left over from the failed attempt are discarded by the generation itself. If the failure was a business error (no AI credits, no monthly publication slots), retrying will fail the same way. Requires the ai_plans:update permission.",
|
|
1877
1877
|
"operationId": "retryAiPlan",
|
|
1878
1878
|
"x-planvortex-identity": [
|
|
1879
1879
|
"current_user",
|
|
@@ -10503,7 +10503,7 @@
|
|
|
10503
10503
|
"publications"
|
|
10504
10504
|
],
|
|
10505
10505
|
"summary": "Create new publications",
|
|
10506
|
-
"description": "Create new publications. For X (Twitter) accounts, paid actions consume the client's monthly X credit pool (shared across organizations). When a publication is sent immediately, the required credits are checked before publishing and only charged on success. A tweet costs 15 credits, or 200 credits if its text contains a link. Link detection uses the same rule the backend applies: an http(s):// URL, a www. host, or a bare domain with a recognized TLD (regex TWITTER_LINK_REGEX). A scheduled publication does not consume credits when created; it is charged when the background job publishes it. If there are not enough credits at publish time the scheduled publication is left in state 'withErrors' with publication_errors.code = 940 and
|
|
10506
|
+
"description": "Create new publications. For X (Twitter) accounts, paid actions consume the client's monthly X credit pool (shared across organizations). When a publication is sent immediately, the required credits are checked before publishing and only charged on success. A tweet costs 15 credits, or 200 credits if its text contains a link. Link detection uses the same rule the backend applies: an http(s):// URL, a www. host, or a bare domain with a recognized TLD (regex TWITTER_LINK_REGEX). A scheduled publication does not consume credits when created; it is charged when the background job publishes it. If there are not enough credits at publish time the scheduled publication is left in state 'withErrors' with publication_errors.code = 940 (it is never thrown, and no webhook announces it: read the publication to find out). Publishing immediately without enough credits returns error 940.\n\n**An Instagram video can come back in state `publishing`, and that is not an error.** Instagram processes a video before it can be published, and the request waits about 30 seconds for it. If Meta has not finished by then, the response carries `state: \"publishing\"` and a `pending_publish` object, and PlanVortex keeps asking Instagram about once a minute in the background, for up to 10 minutes. Read the publication again (`GET /organizations/{id_organization}/publish/{id_publication}`) after `pending_publish.next_check` until it is `sended` or `withErrors`. **Do not retry it or create it again**: the first one is still on its way, and a second one would publish the video twice.",
|
|
10507
10507
|
"operationId": "addPublication",
|
|
10508
10508
|
"x-planvortex-identity": [
|
|
10509
10509
|
"current_user",
|
|
@@ -10942,7 +10942,7 @@
|
|
|
10942
10942
|
}
|
|
10943
10943
|
},
|
|
10944
10944
|
"400": {
|
|
10945
|
-
"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| `921` | Can't update a publication already sended |\n| `940` | X (Twitter) credits exhausted. Not enough monthly credits to publish this tweet (same link detection and 15/200 tariff as on create). Response data: { used, limit }. |\n| `523` | Invalid application |",
|
|
10945
|
+
"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| `921` | Can't update a publication already sended, **or one the network is still processing** (state `publishing` with `pending_publish`; then `data.state` is `publishing`). Editing it would publish it twice. It can be edited again if it fails. |\n| `940` | X (Twitter) credits exhausted. Not enough monthly credits to publish this tweet (same link detection and 15/200 tariff as on create). Response data: { used, limit }. |\n| `523` | Invalid application |",
|
|
10946
10946
|
"content": {
|
|
10947
10947
|
"application/json": {
|
|
10948
10948
|
"schema": {
|
|
@@ -10978,7 +10978,7 @@
|
|
|
10978
10978
|
"publications"
|
|
10979
10979
|
],
|
|
10980
10980
|
"summary": "Delete publication by identifier",
|
|
10981
|
-
"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.",
|
|
10981
|
+
"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.\n\n**Deleting a publication that is still `publishing` with `pending_publish` stops it**: PlanVortex stops asking Instagram and never publishes it, unless the check running at that very moment has already done so.",
|
|
10982
10982
|
"operationId": "deletePublication",
|
|
10983
10983
|
"x-planvortex-identity": [
|
|
10984
10984
|
"current_user",
|
|
@@ -11054,7 +11054,7 @@
|
|
|
11054
11054
|
"publications"
|
|
11055
11055
|
],
|
|
11056
11056
|
"summary": "Retry a failed publication",
|
|
11057
|
-
"description": "Sends a publication that ended in state `withErrors` to its social network again, WITHOUT changing its content.\n\nThe publication is sent during the request, so the response already carries the outcome: `state: \"sended\"` if it went out, or `state: \"withErrors\"` with the errors of THIS attempt if it failed again. A scheduled publication whose date has not arrived yet goes back to `ready` and the scheduler picks it up at its time.\n\nEvery accepted call spends one retry, even when the publication fails again — what the endpoint promises is an attempt, not a success. The limit is `max_retries`, also served by `GET /publication_limits`. Once it is spent the way to try again is to update the publication (`PUT`), which resets the counter to 0, or to create a new one.",
|
|
11057
|
+
"description": "Sends a publication that ended in state `withErrors` to its social network again, WITHOUT changing its content.\n\nThe publication is sent during the request, so the response already carries the outcome: `state: \"sended\"` if it went out, or `state: \"withErrors\"` with the errors of THIS attempt if it failed again. An Instagram video can also come back in `publishing` with `pending_publish`: the attempt goes on in the background, and the retry is already spent (see `pending_publish` in `Publication`).\n\nOn Instagram, a retry is worth it after a **999** (Meta ran out of time) and pointless after a **998** (Meta rejected the file): change the file with a `PUT` first. A scheduled publication whose date has not arrived yet goes back to `ready` and the scheduler picks it up at its time.\n\nEvery accepted call spends one retry, even when the publication fails again — what the endpoint promises is an attempt, not a success. The limit is `max_retries`, also served by `GET /publication_limits`. Once it is spent the way to try again is to update the publication (`PUT`), which resets the counter to 0, or to create a new one.",
|
|
11058
11058
|
"operationId": "retryPublication",
|
|
11059
11059
|
"x-planvortex-identity": [
|
|
11060
11060
|
"current_user",
|
|
@@ -11893,7 +11893,7 @@
|
|
|
11893
11893
|
}
|
|
11894
11894
|
},
|
|
11895
11895
|
"400": {
|
|
11896
|
-
"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| `921` | Can't update a publication already sended |\n| `940` | X (Twitter) credits exhausted. Not enough monthly credits to publish this tweet (same link detection and 15/200 tariff as on create). Response data: { used, limit }. |\n| `523` | Invalid application |",
|
|
11896
|
+
"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| `921` | Can't update a publication already sended, **or one the network is still processing** (state `publishing` with `pending_publish`; then `data.state` is `publishing`). Editing it would publish it twice. It can be edited again if it fails. |\n| `940` | X (Twitter) credits exhausted. Not enough monthly credits to publish this tweet (same link detection and 15/200 tariff as on create). Response data: { used, limit }. |\n| `523` | Invalid application |",
|
|
11897
11897
|
"content": {
|
|
11898
11898
|
"application/json": {
|
|
11899
11899
|
"schema": {
|
|
@@ -12537,7 +12537,7 @@
|
|
|
12537
12537
|
"webhooks"
|
|
12538
12538
|
],
|
|
12539
12539
|
"summary": "A change happened (notification to your app)",
|
|
12540
|
-
"description": "PlanVortex `POST`s to the `webhook_url` of your client app when something happens. **The body is an array of changes**, and each one carries `field` telling you what it is — switch on it and ignore what you do not handle, because the list grows with the product.\n\
|
|
12540
|
+
"description": "PlanVortex `POST`s to the `webhook_url` of your client app when something happens. **The body is an array of changes**, and each one carries `field` telling you what it is — switch on it and ignore what you do not handle, because the list grows with the product.\n\nThree shapes travel in that array. A change on an **account** (`WebhookChange`) always carries `id_account`, `id_organization` and `social_network`. A change on an **integration** (`IntegrationWebhookChange`) and the end of an **AI plan** (`AiPlanWebhookChange`) carry none of those, because they hang off the organization and not off any account.\n\nThe events delivered today are `new_account`, `change_state_account`, `messages`, `messaging_postbacks`, `messaging_seen`, `messaging_error`, `comments`, `integration_error`, `ai_plan_generated` and `ai_plan_failed`.\n\n### Comments\n\n`field: \"comments\"` is the one for this section, and the comment itself travels in **`commentObj`** — a property of its own, never in `messageObj`. That is deliberate: a comment is not a message, it has no contact and it hangs off a publication. A consumer that only understands messages sees `commentObj` as absent and ignores the change, which is exactly what should happen.\n\n**Which networks arrive this way:** Facebook and Instagram, because Meta pushes them. YouTube, LinkedIn and Google Business have no comment webhook, so those are polled by a background job and appear in the inbox within a few hours rather than instantly. X is polled too, and only when the organization turns it on, because reading replies there is billed per unit.\n\n**Meta repeats deliveries.** The same comment can arrive more than once; the backend is idempotent about it and so should you be — deduplicate on `commentObj.external_id`.\n\n### AI plans\n\n`ai_plan_generated` and `ai_plan_failed` say that a plan has **finished**: it was generated, or it gave up. Instead of polling the plan every few seconds, wait for one of these and then read it once with `GET /clients/{id_client}/organizations/{id_organization}/ai_plans/{id_ai_plan}`. The change carries ids and numbers only, never the prompt or the generated text, so the publications are in that `GET`.\n\nA plan that hits a transient error goes back to the queue and is retried. That is not an end and sends nothing, so each plan sends **one** of these two events, never a `failed` followed by a `generated`. A plan generated with notices (`warnings` above zero) is still `ai_plan_generated`: it is usable, and the notices are in the plan.\n\nThe event goes to **every** app of the client that has a `webhook_url`, not only to the one that created the plan. And it is not retried: if your endpoint was down when the plan finished, the `GET` still tells you how it ended, so on startup read the plans you were waiting for.\n\n### Verifying the signature\n\nEvery delivery carries the body signed with your app's secret, twice:\n\n- `x-hub-signature`: `sha1=<hmac>`\n- `x-hub-signature-256`: `sha256=<hmac>`\n\nCompute the HMAC over the **raw** request body — not over a re-serialized copy of the parsed JSON — and compare in constant time. Prefer the sha256 header.\n\nDelivery is best effort: PlanVortex does not retry a webhook that fails.",
|
|
12541
12541
|
"operationId": "onCommentsWebhook",
|
|
12542
12542
|
"requestBody": {
|
|
12543
12543
|
"required": true,
|
|
@@ -12552,6 +12552,9 @@
|
|
|
12552
12552
|
},
|
|
12553
12553
|
{
|
|
12554
12554
|
"$ref": "#/components/schemas/CommentsIntegrationWebhookChange"
|
|
12555
|
+
},
|
|
12556
|
+
{
|
|
12557
|
+
"$ref": "#/components/schemas/CommentsAiPlanWebhookChange"
|
|
12555
12558
|
}
|
|
12556
12559
|
]
|
|
12557
12560
|
}
|
|
@@ -13372,7 +13375,7 @@
|
|
|
13372
13375
|
"failed",
|
|
13373
13376
|
"cancelled"
|
|
13374
13377
|
],
|
|
13375
|
-
"description": "State machine: pending -> generating -> generated -> validated | failed | cancelled.
|
|
13378
|
+
"description": "State machine: pending -> generating -> generated -> validated | failed | cancelled. Reaching `generated` or `failed` is announced to your app's webhook (`ai_plan_generated`, `ai_plan_failed`); without one, poll the plan while state is pending or generating."
|
|
13376
13379
|
},
|
|
13377
13380
|
"archived_date": {
|
|
13378
13381
|
"type": "string",
|
|
@@ -14040,7 +14043,7 @@
|
|
|
14040
14043
|
},
|
|
14041
14044
|
"webhook_url": {
|
|
14042
14045
|
"type": "string",
|
|
14043
|
-
"description": "Where PlanVortex posts events, when one is configured.\n\n**The body is an array of changes, not a single object**, and it carries two signature headers computed with this app's secret over the **raw** body: `x-hub-signature` (`sha1=<hex>`) and `x-hub-signature-256` (`sha256=<hex>`). Verify against the bytes you received — parsing the JSON and re-serialising it changes them and the signature will not match.\n\nThe events delivered today are `new_account`, `change_state_account`, `messages`, `messaging_postbacks`, `messaging_seen`, `messaging_error`, `comments` and `
|
|
14046
|
+
"description": "Where PlanVortex posts events, when one is configured.\n\n**The body is an array of changes, not a single object**, and it carries two signature headers computed with this app's secret over the **raw** body: `x-hub-signature` (`sha1=<hex>`) and `x-hub-signature-256` (`sha256=<hex>`). Verify against the bytes you received — parsing the JSON and re-serialising it changes them and the signature will not match.\n\nThe events delivered today are `new_account`, `change_state_account`, `messages`, `messaging_postbacks`, `messaging_seen`, `messaging_error`, `comments`, `integration_error`, `ai_plan_generated` and `ai_plan_failed`. The payload is documented in the `comments` specification. **Every** app of the client that has a `webhook_url` receives every event, not only the app whose call caused it: with two apps, each event arrives twice. Delivery is best effort: PlanVortex does not retry a webhook that fails."
|
|
14044
14047
|
},
|
|
14045
14048
|
"creation_date": {
|
|
14046
14049
|
"type": "string",
|
|
@@ -14894,6 +14897,90 @@
|
|
|
14894
14897
|
}
|
|
14895
14898
|
}
|
|
14896
14899
|
},
|
|
14900
|
+
"CommentsAiPlanWebhookChange": {
|
|
14901
|
+
"type": "object",
|
|
14902
|
+
"description": "One change in the array PlanVortex posts to your app's `webhook_url`, when an **AI plan finished**: `ai_plan_generated` when it came out, `ai_plan_failed` when it gave up.\n\nLike `IntegrationWebhookChange`, it carries no `id_account` and no `social_network`: a plan can span several networks, and it hangs off the organization. It carries ids and numbers only. Read the plan itself with `GET /clients/{id_client}/organizations/{id_organization}/ai_plans/{id_ai_plan}`.",
|
|
14903
|
+
"properties": {
|
|
14904
|
+
"field": {
|
|
14905
|
+
"type": "string",
|
|
14906
|
+
"enum": [
|
|
14907
|
+
"ai_plan_generated",
|
|
14908
|
+
"ai_plan_failed"
|
|
14909
|
+
]
|
|
14910
|
+
},
|
|
14911
|
+
"id_ai_plan": {
|
|
14912
|
+
"type": "string"
|
|
14913
|
+
},
|
|
14914
|
+
"id_organization": {
|
|
14915
|
+
"type": "string"
|
|
14916
|
+
},
|
|
14917
|
+
"state": {
|
|
14918
|
+
"type": "string",
|
|
14919
|
+
"enum": [
|
|
14920
|
+
"generated",
|
|
14921
|
+
"failed"
|
|
14922
|
+
],
|
|
14923
|
+
"description": "The state the plan finished in. It always matches `field`: `generated` with `ai_plan_generated`, `failed` with `ai_plan_failed`."
|
|
14924
|
+
},
|
|
14925
|
+
"template": {
|
|
14926
|
+
"type": "string",
|
|
14927
|
+
"enum": [
|
|
14928
|
+
"standard",
|
|
14929
|
+
"from_images",
|
|
14930
|
+
"from_text",
|
|
14931
|
+
"from_catalog",
|
|
14932
|
+
"campaign"
|
|
14933
|
+
],
|
|
14934
|
+
"description": "What the plan was generated from, the same value as `AiPlan.template`."
|
|
14935
|
+
},
|
|
14936
|
+
"total_publications": {
|
|
14937
|
+
"type": "integer",
|
|
14938
|
+
"description": "How many draft publications the plan holds. A failed plan can still hold the drafts of its failed attempt; retrying it discards them."
|
|
14939
|
+
},
|
|
14940
|
+
"credits_spent": {
|
|
14941
|
+
"type": "integer",
|
|
14942
|
+
"description": "AI credits the generation consumed, the same figure as `AiPlan.credits_spent`. A failed plan can have spent credits too, and they are not refunded."
|
|
14943
|
+
},
|
|
14944
|
+
"warnings": {
|
|
14945
|
+
"type": "integer",
|
|
14946
|
+
"description": "**How many** non-blocking notices the plan has, not the notices themselves: those are in `AiPlan.warnings`. Above zero the plan is still generated and usable."
|
|
14947
|
+
},
|
|
14948
|
+
"error": {
|
|
14949
|
+
"allOf": [
|
|
14950
|
+
{
|
|
14951
|
+
"$ref": "#/components/schemas/CommentsAiPlanWebhookError"
|
|
14952
|
+
}
|
|
14953
|
+
],
|
|
14954
|
+
"description": "Only on `ai_plan_failed`: why the plan failed, the same `code` and `message` as `AiPlan.error`. Its `data` does not travel here; read the plan for it."
|
|
14955
|
+
}
|
|
14956
|
+
},
|
|
14957
|
+
"required": [
|
|
14958
|
+
"field",
|
|
14959
|
+
"id_ai_plan",
|
|
14960
|
+
"id_organization",
|
|
14961
|
+
"state",
|
|
14962
|
+
"template",
|
|
14963
|
+
"total_publications",
|
|
14964
|
+
"credits_spent",
|
|
14965
|
+
"warnings"
|
|
14966
|
+
]
|
|
14967
|
+
},
|
|
14968
|
+
"CommentsAiPlanWebhookError": {
|
|
14969
|
+
"type": "object",
|
|
14970
|
+
"description": "Why an AI plan failed, inside `AiPlanWebhookChange.error`: the same `code` and `message` as `AiPlan.error`. Its `data` is left out, because it can carry what the AI provider answered; read the plan for it.",
|
|
14971
|
+
"properties": {
|
|
14972
|
+
"code": {
|
|
14973
|
+
"type": "integer"
|
|
14974
|
+
},
|
|
14975
|
+
"message": {
|
|
14976
|
+
"type": "string"
|
|
14977
|
+
}
|
|
14978
|
+
},
|
|
14979
|
+
"required": [
|
|
14980
|
+
"code",
|
|
14981
|
+
"message"
|
|
14982
|
+
]
|
|
14983
|
+
},
|
|
14897
14984
|
"CommentsComment": {
|
|
14898
14985
|
"type": "object",
|
|
14899
14986
|
"description": "A comment — or a review — as PlanVortex stores it. Remember it is a **snapshot** of what the network said at `collected_date`; the live thread endpoints return the same shape reconciled against the network.",
|
|
@@ -15188,7 +15275,7 @@
|
|
|
15188
15275
|
},
|
|
15189
15276
|
"CommentsWebhookChange": {
|
|
15190
15277
|
"type": "object",
|
|
15191
|
-
"description": "One change in the array PlanVortex posts to your app's `webhook_url`, when the change concerns a social **account**.\n\nAn integration that stopped working has a shape of its own
|
|
15278
|
+
"description": "One change in the array PlanVortex posts to your app's `webhook_url`, when the change concerns a social **account**.\n\nAn integration that stopped working has a shape of its own (`IntegrationWebhookChange`), and so does an AI plan that finished (`AiPlanWebhookChange`). One delivery can mix them. Switch on `field`, and ignore what you do not handle.",
|
|
15192
15279
|
"properties": {
|
|
15193
15280
|
"field": {
|
|
15194
15281
|
"type": "string",
|
|
@@ -17725,7 +17812,10 @@
|
|
|
17725
17812
|
"draft",
|
|
17726
17813
|
"publishing"
|
|
17727
17814
|
],
|
|
17728
|
-
"description": "`draft` is never sent; `ready` is scheduled; `publishing` is in the network's hands right now; `sended` went out; `withErrors` failed and carries the reason in `publication_errors
|
|
17815
|
+
"description": "`draft` is never sent; `ready` is scheduled; `publishing` is in the network's hands right now; `sended` went out; `withErrors` failed and carries the reason in `publication_errors`.\n\n`publishing` usually lasts a few seconds, but it can last **up to 10 minutes** when the network is still processing a video (today, Instagram). Then `pending_publish` is present: wait and read the publication again, do not retry it."
|
|
17816
|
+
},
|
|
17817
|
+
"pending_publish": {
|
|
17818
|
+
"$ref": "#/components/schemas/PublicationPending"
|
|
17729
17819
|
},
|
|
17730
17820
|
"files": {
|
|
17731
17821
|
"type": "array",
|
|
@@ -17744,7 +17834,7 @@
|
|
|
17744
17834
|
},
|
|
17745
17835
|
"publication_errors": {
|
|
17746
17836
|
"type": "array",
|
|
17747
|
-
"description": "Why the publication failed, one entry per problem. **It is an array**, and it is empty on a publication that has not failed.\n\nFor a scheduled X (Twitter) publication that runs out of credits at publish time, `code` is 940 and `data` is `{ used, limit }`; the publication stays in state `withErrors` and the
|
|
17837
|
+
"description": "Why the publication failed, one entry per problem. **It is an array**, and it is empty on a publication that has not failed.\n\nFor a scheduled X (Twitter) publication that runs out of credits at publish time, `code` is 940 and `data` is `{ used, limit }`; the publication stays in state `withErrors`. No webhook announces it: read the publication to find out.\n\nOn Instagram, two codes tell you whether a retry makes sense. **998**: Meta rejected the media while processing it (a codec, a duration, a URL it could not download). `data` carries `container_id`, `status_code` and, when Meta gives one, its reason in `status`. Retrying the same file fails the same way: change it first. **999**: Meta had not finished processing the media 10 minutes after it was sent. `data` carries `container_ids` and `minutes`. The file is not the problem, and a retry usually works.",
|
|
17748
17838
|
"items": {
|
|
17749
17839
|
"type": "object",
|
|
17750
17840
|
"required": [
|
|
@@ -17859,6 +17949,34 @@
|
|
|
17859
17949
|
}
|
|
17860
17950
|
}
|
|
17861
17951
|
},
|
|
17952
|
+
"PublicationPending": {
|
|
17953
|
+
"type": "object",
|
|
17954
|
+
"description": "Present only while the network is still **processing** what was sent, and PlanVortex is waiting to publish it. Today that happens on Instagram alone, when Meta takes longer than the ~30 seconds the request waits to process the media. In practice, that means videos.\n\nThe publication stays in state `publishing`, and the background job asks Instagram again about once a minute until it goes out (`sended`) or fails (`withErrors`). It gives up 10 minutes after the media was sent to Instagram, and then the publication ends in `withErrors` with code 999. The object disappears as soon as the publication is resolved.\n\n**Wait for it. Do not retry it and do not create it again**: it has not failed, and a second publication would put the same video out twice. There is no webhook for the outcome: read the publication again after `next_check`. While this object is present the publication **cannot be updated** (error 921), for the same reason.",
|
|
17955
|
+
"properties": {
|
|
17956
|
+
"next_check": {
|
|
17957
|
+
"type": "string",
|
|
17958
|
+
"format": "date-time",
|
|
17959
|
+
"description": "When PlanVortex will ask the network again. A good moment to read the publication again: asking before it changes nothing."
|
|
17960
|
+
},
|
|
17961
|
+
"deadline": {
|
|
17962
|
+
"type": "string",
|
|
17963
|
+
"format": "date-time",
|
|
17964
|
+
"description": "When PlanVortex stops waiting. If the network has not finished by then, the publication ends in `withErrors` (999 on Instagram)."
|
|
17965
|
+
},
|
|
17966
|
+
"temp_keys": {
|
|
17967
|
+
"type": "array",
|
|
17968
|
+
"items": {
|
|
17969
|
+
"type": "string"
|
|
17970
|
+
},
|
|
17971
|
+
"description": "PlanVortex's own bookkeeping: the temporary cropped files the network may still be downloading. Do not rely on it."
|
|
17972
|
+
},
|
|
17973
|
+
"data": {
|
|
17974
|
+
"type": "object",
|
|
17975
|
+
"additionalProperties": true,
|
|
17976
|
+
"description": "PlanVortex's own bookkeeping to resume the publication (on Instagram, the ids of Meta's containers). Its content may change without notice."
|
|
17977
|
+
}
|
|
17978
|
+
}
|
|
17979
|
+
},
|
|
17862
17980
|
"PublicationStats": {
|
|
17863
17981
|
"type": "object",
|
|
17864
17982
|
"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 `threads` there are six, and the one that matters is `views`: it is the only one of the four newest networks with something like impressions, so its engagement rate is computed over a real base and not over followers. Sharing arrives split in three — `reposts`, `quotes` and `shares` (outside Threads) — and the three are added into one figure, the same criterion as on X; the breakdown stays in `raw`. A reply is what every other network calls a comment.\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.\n\nOn `slack` there is exactly **one**, `reactions`, and the other absences are the informative part: the Web API publishes no impressions, no reach, no views and no clicks for a message, so those keys are missing rather than zero. There is no `comments` either — Slack threads are not read at all (`comments` is `false` in `GET /social_capabilities`). Engagement is computed over the channel's members.\n\nOn `pinterest` there are seven, and three of them exist nowhere else: `saves` — the gesture the whole network is built on —, `outbound_clicks` (clicks that left the pin towards its `link`, which is the one normalised as the common `clicks`) and `pin_clicks` (clicks that opened the pin inside Pinterest, with no common equivalent). `impressions` is real here, so the engagement rate is computed over it and not over followers. `video_views` only exists on a video pin. And `comments` is the odd one: Pinterest **counts** them and offers no way to read them, so the number is reported while `comments` is `false` in `GET /social_capabilities` — this network has no comment inbox, and it is not a gap waiting to be filled.",
|
|
@@ -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.13.0"
|
|
13
13
|
description = "Official Python client for the PlanVortex API"
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
requires-python = ">=3.10"
|
|
@@ -562,7 +562,7 @@ class AppsClientApp(TypedDict):
|
|
|
562
562
|
|
|
563
563
|
**The body is an array of changes, not a single object**, and it carries two signature headers computed with this app's secret over the **raw** body: `x-hub-signature` (`sha1=<hex>`) and `x-hub-signature-256` (`sha256=<hex>`). Verify against the bytes you received — parsing the JSON and re-serialising it changes them and the signature will not match.
|
|
564
564
|
|
|
565
|
-
The events delivered today are `new_account`, `change_state_account`, `messages`, `messaging_postbacks`, `messaging_seen`, `messaging_error`, `comments` and `
|
|
565
|
+
The events delivered today are `new_account`, `change_state_account`, `messages`, `messaging_postbacks`, `messaging_seen`, `messaging_error`, `comments`, `integration_error`, `ai_plan_generated` and `ai_plan_failed`. The payload is documented in the `comments` specification. **Every** app of the client that has a `webhook_url` receives every event, not only the app whose call caused it: with two apps, each event arrives twice. Delivery is best effort: PlanVortex does not retry a webhook that fails.
|
|
566
566
|
"""
|
|
567
567
|
creation_date: str
|
|
568
568
|
deleted: bool
|
|
@@ -897,6 +897,15 @@ class ClientsRolesUserList(TypedDict):
|
|
|
897
897
|
total: NotRequired[int]
|
|
898
898
|
|
|
899
899
|
|
|
900
|
+
class CommentsAiPlanWebhookError(TypedDict):
|
|
901
|
+
"""
|
|
902
|
+
Why an AI plan failed, inside `AiPlanWebhookChange.error`: the same `code` and `message` as `AiPlan.error`. Its `data` is left out, because it can carry what the AI provider answered; read the plan for it.
|
|
903
|
+
"""
|
|
904
|
+
|
|
905
|
+
code: int
|
|
906
|
+
message: str
|
|
907
|
+
|
|
908
|
+
|
|
900
909
|
class CommentsCommentActions(TypedDict):
|
|
901
910
|
"""
|
|
902
911
|
What one network lets you do to a comment. Four booleans and never fewer: an absent key would be indistinguishable from an oversight.
|
|
@@ -1927,6 +1936,33 @@ class PublicationDestination(TypedDict):
|
|
|
1927
1936
|
"""
|
|
1928
1937
|
|
|
1929
1938
|
|
|
1939
|
+
class PublicationPending(TypedDict):
|
|
1940
|
+
"""
|
|
1941
|
+
Present only while the network is still **processing** what was sent, and PlanVortex is waiting to publish it. Today that happens on Instagram alone, when Meta takes longer than the ~30 seconds the request waits to process the media. In practice, that means videos.
|
|
1942
|
+
|
|
1943
|
+
The publication stays in state `publishing`, and the background job asks Instagram again about once a minute until it goes out (`sended`) or fails (`withErrors`). It gives up 10 minutes after the media was sent to Instagram, and then the publication ends in `withErrors` with code 999. The object disappears as soon as the publication is resolved.
|
|
1944
|
+
|
|
1945
|
+
**Wait for it. Do not retry it and do not create it again**: it has not failed, and a second publication would put the same video out twice. There is no webhook for the outcome: read the publication again after `next_check`. While this object is present the publication **cannot be updated** (error 921), for the same reason.
|
|
1946
|
+
"""
|
|
1947
|
+
|
|
1948
|
+
next_check: NotRequired[str]
|
|
1949
|
+
"""
|
|
1950
|
+
When PlanVortex will ask the network again. A good moment to read the publication again: asking before it changes nothing.
|
|
1951
|
+
"""
|
|
1952
|
+
deadline: NotRequired[str]
|
|
1953
|
+
"""
|
|
1954
|
+
When PlanVortex stops waiting. If the network has not finished by then, the publication ends in `withErrors` (999 on Instagram).
|
|
1955
|
+
"""
|
|
1956
|
+
temp_keys: NotRequired[list[str]]
|
|
1957
|
+
"""
|
|
1958
|
+
PlanVortex's own bookkeeping: the temporary cropped files the network may still be downloading. Do not rely on it.
|
|
1959
|
+
"""
|
|
1960
|
+
data: NotRequired[dict[str, Any]]
|
|
1961
|
+
"""
|
|
1962
|
+
PlanVortex's own bookkeeping to resume the publication (on Instagram, the ids of Meta's containers). Its content may change without notice.
|
|
1963
|
+
"""
|
|
1964
|
+
|
|
1965
|
+
|
|
1930
1966
|
class PublicationStats(TypedDict):
|
|
1931
1967
|
"""
|
|
1932
1968
|
Raw, per-network metrics for a publication. Only the fields that belong to the publication's own social network are returned.
|
|
@@ -2773,6 +2809,42 @@ class ClientsPlan(TypedDict):
|
|
|
2773
2809
|
stripe_customer_id: NotRequired[str]
|
|
2774
2810
|
|
|
2775
2811
|
|
|
2812
|
+
class CommentsAiPlanWebhookChange(TypedDict):
|
|
2813
|
+
"""
|
|
2814
|
+
One change in the array PlanVortex posts to your app's `webhook_url`, when an **AI plan finished**: `ai_plan_generated` when it came out, `ai_plan_failed` when it gave up.
|
|
2815
|
+
|
|
2816
|
+
Like `IntegrationWebhookChange`, it carries no `id_account` and no `social_network`: a plan can span several networks, and it hangs off the organization. It carries ids and numbers only. Read the plan itself with `GET /clients/{id_client}/organizations/{id_organization}/ai_plans/{id_ai_plan}`.
|
|
2817
|
+
"""
|
|
2818
|
+
|
|
2819
|
+
field: Literal["ai_plan_generated", "ai_plan_failed"]
|
|
2820
|
+
id_ai_plan: str
|
|
2821
|
+
id_organization: str
|
|
2822
|
+
state: Literal["generated", "failed"]
|
|
2823
|
+
"""
|
|
2824
|
+
The state the plan finished in. It always matches `field`: `generated` with `ai_plan_generated`, `failed` with `ai_plan_failed`.
|
|
2825
|
+
"""
|
|
2826
|
+
template: Literal["standard", "from_images", "from_text", "from_catalog", "campaign"]
|
|
2827
|
+
"""
|
|
2828
|
+
What the plan was generated from, the same value as `AiPlan.template`.
|
|
2829
|
+
"""
|
|
2830
|
+
total_publications: int
|
|
2831
|
+
"""
|
|
2832
|
+
How many draft publications the plan holds. A failed plan can still hold the drafts of its failed attempt; retrying it discards them.
|
|
2833
|
+
"""
|
|
2834
|
+
credits_spent: int
|
|
2835
|
+
"""
|
|
2836
|
+
AI credits the generation consumed, the same figure as `AiPlan.credits_spent`. A failed plan can have spent credits too, and they are not refunded.
|
|
2837
|
+
"""
|
|
2838
|
+
warnings: int
|
|
2839
|
+
"""
|
|
2840
|
+
**How many** non-blocking notices the plan has, not the notices themselves: those are in `AiPlan.warnings`. Above zero the plan is still generated and usable.
|
|
2841
|
+
"""
|
|
2842
|
+
error: NotRequired[CommentsAiPlanWebhookError]
|
|
2843
|
+
"""
|
|
2844
|
+
Only on `ai_plan_failed`: why the plan failed, the same `code` and `message` as `AiPlan.error`. Its `data` does not travel here; read the plan for it.
|
|
2845
|
+
"""
|
|
2846
|
+
|
|
2847
|
+
|
|
2776
2848
|
class Contact(TypedDict):
|
|
2777
2849
|
"""
|
|
2778
2850
|
A person the organization exchanges messages with. The same contact can be reachable on several channels — that is what `social_identifiers` is.
|
|
@@ -3307,7 +3379,10 @@ class Publication(TypedDict):
|
|
|
3307
3379
|
state: Literal["ready", "withErrors", "sended", "draft", "publishing"]
|
|
3308
3380
|
"""
|
|
3309
3381
|
`draft` is never sent; `ready` is scheduled; `publishing` is in the network's hands right now; `sended` went out; `withErrors` failed and carries the reason in `publication_errors`.
|
|
3382
|
+
|
|
3383
|
+
`publishing` usually lasts a few seconds, but it can last **up to 10 minutes** when the network is still processing a video (today, Instagram). Then `pending_publish` is present: wait and read the publication again, do not retry it.
|
|
3310
3384
|
"""
|
|
3385
|
+
pending_publish: NotRequired[PublicationPending]
|
|
3311
3386
|
files: list[Upload]
|
|
3312
3387
|
"""
|
|
3313
3388
|
The attached files, **already resolved**: every read and write path returns full uploads, not identifiers. Identifiers are what you SEND (see `PublicationInput.files`).
|
|
@@ -3318,7 +3393,9 @@ class Publication(TypedDict):
|
|
|
3318
3393
|
"""
|
|
3319
3394
|
Why the publication failed, one entry per problem. **It is an array**, and it is empty on a publication that has not failed.
|
|
3320
3395
|
|
|
3321
|
-
For a scheduled X (Twitter) publication that runs out of credits at publish time, `code` is 940 and `data` is `{ used, limit }`; the publication stays in state `withErrors
|
|
3396
|
+
For a scheduled X (Twitter) publication that runs out of credits at publish time, `code` is 940 and `data` is `{ used, limit }`; the publication stays in state `withErrors`. No webhook announces it: read the publication to find out.
|
|
3397
|
+
|
|
3398
|
+
On Instagram, two codes tell you whether a retry makes sense. **998**: Meta rejected the media while processing it (a codec, a duration, a URL it could not download). `data` carries `container_id`, `status_code` and, when Meta gives one, its reason in `status`. Retrying the same file fails the same way: change it first. **999**: Meta had not finished processing the media 10 minutes after it was sent. `data` carries `container_ids` and `minutes`. The file is not the problem, and a retry usually works.
|
|
3322
3399
|
"""
|
|
3323
3400
|
retries: int
|
|
3324
3401
|
"""
|
|
@@ -3493,7 +3570,7 @@ class AiPlansAiPlan(TypedDict):
|
|
|
3493
3570
|
options: AiPlansAiPlanOptions
|
|
3494
3571
|
state: Literal["pending", "generating", "generated", "validated", "failed", "cancelled"]
|
|
3495
3572
|
"""
|
|
3496
|
-
State machine: pending -> generating -> generated -> validated | failed | cancelled.
|
|
3573
|
+
State machine: pending -> generating -> generated -> validated | failed | cancelled. Reaching `generated` or `failed` is announced to your app's webhook (`ai_plan_generated`, `ai_plan_failed`); without one, poll the plan while state is pending or generating.
|
|
3497
3574
|
"""
|
|
3498
3575
|
archived_date: NotRequired[str]
|
|
3499
3576
|
"""
|
|
@@ -3877,7 +3954,7 @@ class CommentsWebhookChange(TypedDict):
|
|
|
3877
3954
|
"""
|
|
3878
3955
|
One change in the array PlanVortex posts to your app's `webhook_url`, when the change concerns a social **account**.
|
|
3879
3956
|
|
|
3880
|
-
An integration that stopped working has a shape of its own
|
|
3957
|
+
An integration that stopped working has a shape of its own (`IntegrationWebhookChange`), and so does an AI plan that finished (`AiPlanWebhookChange`). One delivery can mix them. Switch on `field`, and ignore what you do not handle.
|
|
3881
3958
|
"""
|
|
3882
3959
|
|
|
3883
3960
|
field: Literal[
|
|
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import platform
|
|
14
14
|
|
|
15
|
-
VERSION = "0.
|
|
15
|
+
VERSION = "0.13.0"
|
|
16
16
|
|
|
17
17
|
# Sin barra final, y con el `/v1.0.0` dentro: la version de la API es parte de la ruta, no una
|
|
18
18
|
# cabecera, asi que una `base_url` sin ella da 404 en todo.
|
|
@@ -11,8 +11,10 @@ THE CYCLE, which is what to have clear before anything else::
|
|
|
11
11
|
AND WHAT SURPRISES PEOPLE:
|
|
12
12
|
|
|
13
13
|
- **``create()`` generates nothing**: it queues the plan and returns the budget. The generating is
|
|
14
|
-
done by a separate job
|
|
15
|
-
or ``
|
|
14
|
+
done by a separate job and can take minutes. When it ends, your app's ``webhook_url`` receives
|
|
15
|
+
``ai_plan_generated`` or ``ai_plan_failed`` (``is_ai_plan_change`` in :mod:`planvortex.webhooks`);
|
|
16
|
+
without a webhook, poll :meth:`AsyncAiPlansResource.get` while the state is ``pending`` or
|
|
17
|
+
``generating``.
|
|
16
18
|
- **What is generated are NORMAL publications in state ``draft``.** They are edited and deleted with
|
|
17
19
|
``pv.publications``, not with anything here. Validating is what moves them to ``ready``.
|
|
18
20
|
- **It is paid for in AI credits and the price is known BEFOREHAND.** The budget is computed by the
|
|
@@ -175,8 +177,10 @@ class AsyncAiPlansResource(AsyncResource):
|
|
|
175
177
|
) -> AiPlan:
|
|
176
178
|
"""One plan, with its publications **already resolved** and with each one's files.
|
|
177
179
|
|
|
178
|
-
|
|
179
|
-
webhook
|
|
180
|
+
Read it when your app receives ``ai_plan_generated`` or ``ai_plan_failed``. Without a
|
|
181
|
+
webhook, it is the endpoint you poll while ``state`` is ``pending`` or ``generating``. The
|
|
182
|
+
webhook is not retried, so if your endpoint was down when the plan finished, this still tells
|
|
183
|
+
you how it ended.
|
|
180
184
|
|
|
181
185
|
Read ``warnings`` on a plan that came out ``generated``, which is the place nobody
|
|
182
186
|
looks: it is not an error of the response, it is a notice about a plan that generated fine.
|
|
@@ -333,8 +337,8 @@ class AsyncAiPlansResource(AsyncResource):
|
|
|
333
337
|
*,
|
|
334
338
|
timeout: float | None = None,
|
|
335
339
|
) -> AiPlan:
|
|
336
|
-
"""Queue a ``failed`` plan again with the same data. The state goes back to ``pending`` and
|
|
337
|
-
|
|
340
|
+
"""Queue a ``failed`` plan again with the same data. The state goes back to ``pending`` and the
|
|
341
|
+
end arrives again as after ``create()``: on your webhook, or by polling.
|
|
338
342
|
|
|
339
343
|
It uses the brand context copied when the plan was created, not today's: a plan is
|
|
340
344
|
reproducible even if somebody edited the configuration in the meantime.
|
|
@@ -114,8 +114,10 @@ class AiPlansResource(Resource):
|
|
|
114
114
|
) -> AiPlan:
|
|
115
115
|
"""One plan, with its publications **already resolved** and with each one's files.
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
webhook
|
|
117
|
+
Read it when your app receives ``ai_plan_generated`` or ``ai_plan_failed``. Without a
|
|
118
|
+
webhook, it is the endpoint you poll while ``state`` is ``pending`` or ``generating``. The
|
|
119
|
+
webhook is not retried, so if your endpoint was down when the plan finished, this still tells
|
|
120
|
+
you how it ended.
|
|
119
121
|
|
|
120
122
|
Read ``warnings`` on a plan that came out ``generated``, which is the place nobody
|
|
121
123
|
looks: it is not an error of the response, it is a notice about a plan that generated fine.
|
|
@@ -272,8 +274,8 @@ class AiPlansResource(Resource):
|
|
|
272
274
|
*,
|
|
273
275
|
timeout: float | None = None,
|
|
274
276
|
) -> AiPlan:
|
|
275
|
-
"""Queue a ``failed`` plan again with the same data. The state goes back to ``pending`` and
|
|
276
|
-
|
|
277
|
+
"""Queue a ``failed`` plan again with the same data. The state goes back to ``pending`` and the
|
|
278
|
+
end arrives again as after ``create()``: on your webhook, or by polling.
|
|
277
279
|
|
|
278
280
|
It uses the brand context copied when the plan was created, not today's: a plan is
|
|
279
281
|
reproducible even if somebody edited the configuration in the meantime.
|