plasgos-crm-sdk 2.0.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.
- plasgos_crm_sdk-2.0.0/.gitignore +28 -0
- plasgos_crm_sdk-2.0.0/CHANGELOG.md +51 -0
- plasgos_crm_sdk-2.0.0/LEGACY_V1.md +137 -0
- plasgos_crm_sdk-2.0.0/LICENSE +21 -0
- plasgos_crm_sdk-2.0.0/PKG-INFO +233 -0
- plasgos_crm_sdk-2.0.0/README.md +210 -0
- plasgos_crm_sdk-2.0.0/pyproject.toml +46 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/__init__.py +100 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/_envelope.py +18 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/_http.py +205 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/_signature.py +36 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/_version.py +3 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/client.py +98 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/credentials_client.py +68 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/errors.py +224 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/py.typed +0 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/__init__.py +13 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/_base.py +22 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/accounts.py +16 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/credentials.py +41 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/messages.py +61 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/official/__init__.py +21 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/official/accounts.py +19 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/official/broadcasts.py +67 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/official/messages.py +31 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/official/shared_waba.py +26 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/official/templates.py +18 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/references.py +25 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/__init__.py +28 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/_api_client.py +459 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/async_client.py +124 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/async_resources.py +214 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/client.py +127 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/content.py +98 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/resources.py +223 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/response.py +112 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/signature.py +51 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/types.py +1071 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/webhook.py +142 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/webhooks/__init__.py +9 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/webhooks/events.py +8 -0
- plasgos_crm_sdk-2.0.0/src/plasgos_crm/webhooks/verify.py +59 -0
- plasgos_crm_sdk-2.0.0/tests/conftest.py +14 -0
- plasgos_crm_sdk-2.0.0/tests/test_client.py +192 -0
- plasgos_crm_sdk-2.0.0/tests/test_signature.py +22 -0
- plasgos_crm_sdk-2.0.0/tests/test_v3_async.py +140 -0
- plasgos_crm_sdk-2.0.0/tests/test_v3_client.py +481 -0
- plasgos_crm_sdk-2.0.0/tests/test_v3_live.py +111 -0
- plasgos_crm_sdk-2.0.0/tests/test_v3_types.py +31 -0
- plasgos_crm_sdk-2.0.0/tests/test_v3_webhook.py +69 -0
- plasgos_crm_sdk-2.0.0/tests/test_webhooks.py +41 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Node
|
|
2
|
+
node_modules/
|
|
3
|
+
packages/node/dist/
|
|
4
|
+
*.tsbuildinfo
|
|
5
|
+
npm-debug.log*
|
|
6
|
+
*.tgz
|
|
7
|
+
|
|
8
|
+
# PHP
|
|
9
|
+
packages/php/vendor/
|
|
10
|
+
packages/php/composer.lock
|
|
11
|
+
.phpunit.result.cache
|
|
12
|
+
.phpunit.cache/
|
|
13
|
+
.phpstan-cache/
|
|
14
|
+
|
|
15
|
+
# Python
|
|
16
|
+
packages/python/dist/
|
|
17
|
+
packages/python/build/
|
|
18
|
+
packages/python/.venv/
|
|
19
|
+
packages/python/*.egg-info/
|
|
20
|
+
__pycache__/
|
|
21
|
+
.pytest_cache/
|
|
22
|
+
.mypy_cache/
|
|
23
|
+
.ruff_cache/
|
|
24
|
+
|
|
25
|
+
# Editors / OS
|
|
26
|
+
.DS_Store
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this package are documented here.
|
|
4
|
+
This project adheres to [Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [2.0.0] - 2026-10-27
|
|
7
|
+
|
|
8
|
+
### API contract 3.0.0-rc.2
|
|
9
|
+
- One `contact` object names "who" in `messages.send`, `leads.create` and `followups.enroll`: `{id}` | `{phone_number, …profile}` | `{channel, external_id}` | `{email}` (replaces `recipient` + `identity` + `create_contact`, and `contact_id` | `contact`). Profile fields only fill a NEW contact. `send-options` / `lookup` queries use the same keys (`send-options` also accepts `email`).
|
|
10
|
+
- `meta.created` everywhere (contact upsert: replaces `meta.operation`; message send: `true`).
|
|
11
|
+
- Malformed `contact` → `422 validation_failed` with field code `invalid_contact`. Error catalog: `components.x-error-codes` in `openapi/openapi-v3.json`.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- `409 idempotency_in_progress` is retried with the same Idempotency-Key (the first request is still running, e.g. after a client-side timeout); other 409s such as `identity_taken` are still raised.
|
|
15
|
+
- Lint rules pinned explicitly (`[tool.ruff.lint] select`): newer ruff versions widened the default rule set and broke CI without any code change.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `AsyncClient` for `asyncio` (`httpx.AsyncClient`): same resources, methods and parameters as `Client` (checked by a parity test), `AsyncPage` with `await next_page()` and `async for … in auto_paging()`, `async with` / `aclose()`. Signing, idempotency, retry policy and logging are shared with the sync client.
|
|
19
|
+
- DEBUG logging on the `plasgos_crm` stdlib logger (docs/SDK_SPEC.md §1.2): one record per attempt with method, path (no query), status, duration, request id, error code and retry wait (also in `record.plasgos_crm`); never headers or bodies.
|
|
20
|
+
- Per-call options (`options=RequestOptions`, docs/SDK_SPEC.md §1.1), keyword-only on every resource method and `client.request()`: `timeout`, `max_retries`, `headers`. Next pages of `auto_paging()` keep them. Headers the SDK owns (signing, `Idempotency-Key`, `Content-Type`, …) are rejected.
|
|
21
|
+
- Types generated from `openapi/openapi-v3.json` (`scripts/generate-types.mjs`, checked in CI): `plasgos_crm.v3.types` (`TypedDict` per schema, plus `…Body`, `…Query`, `…Data`, `…Item`, `…Meta` per operation); `ApiResponse` is generic (`ApiResponse[Contact]`), every resource method is typed, and `Content.text()` / `media()` / `TemplateContent.to_dict()` return the matching `TypedDict`.
|
|
22
|
+
- `Client` for **API Integrasi v3** (`/api/v3`), same contract as the PHP and Node SDK 2.0
|
|
23
|
+
(docs/SDK_SPEC.md):
|
|
24
|
+
- Resources: `me`, `channels`, `endpoints` (including templates), `template_variables`,
|
|
25
|
+
`messages` (omnichannel, `send_options`), `contacts` (including upsert and lookup),
|
|
26
|
+
`contact_groups`, `pipelines`, `leads`, `broadcasts`, `followups` (enrol and unenrol),
|
|
27
|
+
`webhooks` (including `rotate_secret`), plus `client.request()`.
|
|
28
|
+
- Request signing covers the method, path, query and body (`Plasgos-*` headers); exported as
|
|
29
|
+
`sign_v3`, `signature_headers_v3`, `string_to_sign`.
|
|
30
|
+
- An automatic `Idempotency-Key` on POST, reused across retries; retry policy of §6
|
|
31
|
+
(`max_retries`, `max_retry_wait`, injectable `sleeper`/`clock`).
|
|
32
|
+
- `ApiResponse`, `RateLimit` and `Page` (`auto_paging()` generator).
|
|
33
|
+
- `Content` / `TemplateContent` builders (`location()` for LOCATION-header templates).
|
|
34
|
+
- `Webhook.verify()` (HMAC, secret rotation with two signatures; any header mapping including
|
|
35
|
+
WSGI `environ`) and `WebhookEventType`.
|
|
36
|
+
- `ChannelError` (502) and `ServiceUnavailableError` (503), both `ServerError`.
|
|
37
|
+
- v3 error fields on every error: `error_code`, `error_type`, `details`, `fields`, `doc_url`,
|
|
38
|
+
`retry_after`; `error_from_v3()`.
|
|
39
|
+
- Optional live test (`tests/test_v3_live.py`, `PLASGOS_LIVE_*`).
|
|
40
|
+
|
|
41
|
+
### Changed (breaking)
|
|
42
|
+
- The v1 client is now `LegacyClient` (`PlasgosCrmClient` stays as a deprecated alias until 3.0)
|
|
43
|
+
and emits a `DeprecationWarning` when constructed; its documentation moved to `LEGACY_V1.md`.
|
|
44
|
+
- `SignatureExpiredError` now extends `AuthenticationError` (v3 reports clock skew as 401).
|
|
45
|
+
- The legacy `x-hub-token` helper `verify_webhook` is v1-only; v3 deliveries use `Webhook.verify()`.
|
|
46
|
+
|
|
47
|
+
## [1.0.0] - 2026-09-03
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
- Initial release: signed API client, credential-management client, webhook
|
|
51
|
+
verification, typed errors, normalised response envelope.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# plasgos-crm-sdk (Python) — LEGACY v1 client (`/api/v1`)
|
|
2
|
+
|
|
3
|
+
> **Deprecated.** This documents the 1.x API (`PlasgosCrmClient`, now `LegacyClient`). New code should use
|
|
4
|
+
> the API v3 `Client` — see [README.md](README.md). Removed in 3.0.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Official Python SDK for the **Plasgos CRM — Api Integration** API.
|
|
8
|
+
|
|
9
|
+
- One dependency (`httpx`).
|
|
10
|
+
- Automatic HMAC request signing.
|
|
11
|
+
- Normalised response envelope, typed exceptions.
|
|
12
|
+
- Webhook verification helper.
|
|
13
|
+
- Separate client for the credential-management endpoints.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install plasgos-crm-sdk
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start (signed API)
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from plasgos_crm import LegacyClient
|
|
25
|
+
|
|
26
|
+
client = LegacyClient(
|
|
27
|
+
api_key="...",
|
|
28
|
+
secret_key="...",
|
|
29
|
+
base_url="production", # "production" | "sandbox" | absolute URL
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
env = client.messages.send(
|
|
33
|
+
channel="whatsapp",
|
|
34
|
+
account_id="665f1c2e9a1b2c3d4e5f6a7b",
|
|
35
|
+
receiver={"phone_number": "628123456789", "name": "John Doe"},
|
|
36
|
+
content={"type": "text", "data": {"text": "Halo dari SDK"}},
|
|
37
|
+
)
|
|
38
|
+
print(env.metadata["message_id"], env.data)
|
|
39
|
+
|
|
40
|
+
accounts = client.accounts.list()
|
|
41
|
+
|
|
42
|
+
client.official.messages.send_template(
|
|
43
|
+
phone_number_id="123456789012345",
|
|
44
|
+
to="6281234567890",
|
|
45
|
+
template={
|
|
46
|
+
"id": "1234567890123456",
|
|
47
|
+
"name": "order_update_v1",
|
|
48
|
+
"category": "UTILITY",
|
|
49
|
+
"parameters": {"variables": [{"type": "custom", "name": "1", "value": "Budi"}]},
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
client.close() # or: with LegacyClient(...) as client: ...
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Every call returns an `Envelope`:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class Envelope:
|
|
61
|
+
status: int | bool
|
|
62
|
+
message: str | None
|
|
63
|
+
data: Any
|
|
64
|
+
metadata: dict
|
|
65
|
+
raw: Any
|
|
66
|
+
http_status: int
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Errors
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from plasgos_crm import ValidationError, RateLimitError, SignatureExpiredError
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
client.messages.send(...)
|
|
76
|
+
except ValidationError as e:
|
|
77
|
+
print(e.errors)
|
|
78
|
+
except RateLimitError:
|
|
79
|
+
...
|
|
80
|
+
except SignatureExpiredError:
|
|
81
|
+
... # local clock skew vs. server
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Hierarchy: `PlasgosApiError` → `AuthenticationError`, `PermissionError`
|
|
85
|
+
(`PlanError`), `NotFoundError`, `SignatureExpiredError`, `ConflictError`,
|
|
86
|
+
`ValidationError`, `BadRequestError`, `RateLimitError`, `ServerError`,
|
|
87
|
+
`TransportError`.
|
|
88
|
+
|
|
89
|
+
> An un-approved API key can surface as HTTP 500 (`ServerError`) with a message
|
|
90
|
+
> containing `"You don't have permission"` — a known server quirk.
|
|
91
|
+
|
|
92
|
+
## Credential management (`/v2/integration/*`)
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from plasgos_crm import PlasgosCrmCredentialsClient
|
|
96
|
+
|
|
97
|
+
creds = PlasgosCrmCredentialsClient(access_token, fingerprint, base_url="production")
|
|
98
|
+
creds.credentials.request()
|
|
99
|
+
current = creds.credentials.get()
|
|
100
|
+
creds.credentials.regenerate()
|
|
101
|
+
creds.credentials.subscribe_webhook(
|
|
102
|
+
webhook_url="https://your.app/webhooks/plasgos",
|
|
103
|
+
verify_token="a-shared-secret",
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Webhooks
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from plasgos_crm import verify_webhook, MESSAGE_RECEIVED
|
|
111
|
+
|
|
112
|
+
event = verify_webhook(request.headers, raw_body, expected_token="a-shared-secret")
|
|
113
|
+
if event["event"] == MESSAGE_RECEIVED:
|
|
114
|
+
...
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`verify_webhook` compares the `x-hub-token` header (constant-time) and parses
|
|
118
|
+
the JSON body. There is no payload HMAC.
|
|
119
|
+
|
|
120
|
+
## Configuration
|
|
121
|
+
|
|
122
|
+
| Argument | Default | |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `base_url` | `"production"` | `"production"`, `"sandbox"`, or an absolute URL |
|
|
125
|
+
| `timeout` | `15.0` | seconds |
|
|
126
|
+
| `max_retries` | `2` | idempotent GET only; network / 5xx / 429 |
|
|
127
|
+
| `user_agent` | `plasgos-crm-sdk-python/<version>` | |
|
|
128
|
+
| `transport` / `http_client` | — | inject `httpx.MockTransport` / `httpx.Client` for tests |
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
pip install -e ".[dev]"
|
|
134
|
+
pytest
|
|
135
|
+
mypy src
|
|
136
|
+
ruff check .
|
|
137
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 plasgos
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: plasgos-crm-sdk
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Official Python SDK for the Plasgos CRM Api Integration API.
|
|
5
|
+
Project-URL: Homepage, https://github.com/plasgos/plasgos-crm-sdk/tree/main/packages/python
|
|
6
|
+
Project-URL: Source, https://github.com/plasgos/plasgos-crm-sdk
|
|
7
|
+
Author: Plasgos
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: api,crm,plasgos,sdk,waba,whatsapp
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Requires-Dist: httpx<1,>=0.24
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# plasgos-crm-sdk (Python)
|
|
25
|
+
|
|
26
|
+
Official Python SDK for **Plasgos CRM: API Integrasi v3**. The SDK covers:
|
|
27
|
+
|
|
28
|
+
- omnichannel messages (WhatsApp official and unofficial, Instagram, Messenger)
|
|
29
|
+
- contacts and leads
|
|
30
|
+
- broadcasts
|
|
31
|
+
- followups
|
|
32
|
+
- signed webhooks
|
|
33
|
+
|
|
34
|
+
Features:
|
|
35
|
+
|
|
36
|
+
- Python 3.9+, one dependency (`httpx`), fully typed (`py.typed`, mypy strict).
|
|
37
|
+
- Automatic request signing that covers the method, path, query and body.
|
|
38
|
+
- An automatic `Idempotency-Key` on every POST, reused on retries.
|
|
39
|
+
- Typed errors carrying the API `error_code`.
|
|
40
|
+
- A `Page` type with `auto_paging()`.
|
|
41
|
+
- Webhook HMAC verification.
|
|
42
|
+
|
|
43
|
+
> Upgrading from 1.x? See [UPGRADING.md](../../UPGRADING.md). The v1 client lives on as the
|
|
44
|
+
> deprecated `LegacyClient` (`PlasgosCrmClient` is an alias); its docs are in [LEGACY_V1.md](LEGACY_V1.md).
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install plasgos-crm-sdk
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import os
|
|
56
|
+
|
|
57
|
+
from plasgos_crm import Client, Content, PlasgosApiError
|
|
58
|
+
|
|
59
|
+
with Client(
|
|
60
|
+
os.environ["PLASGOS_API_KEY"],
|
|
61
|
+
os.environ["PLASGOS_SECRET_KEY"],
|
|
62
|
+
base_url="sandbox", # "production" by default; or an absolute URL
|
|
63
|
+
) as client:
|
|
64
|
+
me = client.me.get()
|
|
65
|
+
print(me.data["tenant"]["name"])
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
sent = client.messages.send(
|
|
69
|
+
{
|
|
70
|
+
"endpoint_id": "6281100000001",
|
|
71
|
+
"contact": {"phone_number": "081234567890", "name": "Budi Santoso"}, # profile only for a NEW contact
|
|
72
|
+
"content": Content.template("66f1a2b3c4d5e6f708192a3b")
|
|
73
|
+
.dynamic("name", "{{full_name}}")
|
|
74
|
+
.custom("order", "INV-2026-0917"),
|
|
75
|
+
},
|
|
76
|
+
idempotency_key="wc-1234-processing", # one per intent
|
|
77
|
+
)
|
|
78
|
+
print(sent.data["status"]) # pending | sent | failed (202)
|
|
79
|
+
except PlasgosApiError as error:
|
|
80
|
+
# Branch on the stable code; show error.message (Indonesian) to people.
|
|
81
|
+
if error.error_code == "requires_template":
|
|
82
|
+
...
|
|
83
|
+
raise
|
|
84
|
+
|
|
85
|
+
for contact in client.contacts.list(per_page=100).auto_paging():
|
|
86
|
+
... # every page fetched lazily
|
|
87
|
+
|
|
88
|
+
client.contacts.upsert({"phone_number": "081234567890", "name": "Budi", "tags": ["woocommerce"]})
|
|
89
|
+
client.leads.create({"contact": {"phone_number": "081234567890"}, "pipeline_id": "pl_1"})
|
|
90
|
+
client.followups.enroll("fu_1", {"contact": {"id": "ct_1"}})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### asyncio
|
|
94
|
+
|
|
95
|
+
`AsyncClient` has the same resources, methods and parameters (each method is `async`; lists return an
|
|
96
|
+
`AsyncPage`):
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from plasgos_crm import AsyncClient
|
|
100
|
+
|
|
101
|
+
async with AsyncClient(api_key, secret_key, base_url="sandbox") as client:
|
|
102
|
+
me = await client.me.get()
|
|
103
|
+
page = await client.contacts.list(per_page=100)
|
|
104
|
+
async for contact in page.auto_paging():
|
|
105
|
+
...
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
It takes an `httpx.AsyncBaseTransport` / `httpx.AsyncClient` and an async `sleeper`; close it with
|
|
109
|
+
`async with` or `await client.aclose()`.
|
|
110
|
+
|
|
111
|
+
Resources:
|
|
112
|
+
|
|
113
|
+
- `me`, `channels`
|
|
114
|
+
- `endpoints` (including `templates`), `template_variables`
|
|
115
|
+
- `messages` (`send_options`, `send`, `get`)
|
|
116
|
+
- `contacts`, `contact_groups`
|
|
117
|
+
- `pipelines`, `leads`
|
|
118
|
+
- `broadcasts`, `followups`
|
|
119
|
+
- `webhooks` (including `rotate_secret`)
|
|
120
|
+
|
|
121
|
+
Query parameters may be a mapping, keyword arguments, or both:
|
|
122
|
+
`client.contacts.list({"tag": "vip"}, per_page=100)`.
|
|
123
|
+
`client.request(method, path, query=None, body=None, idempotency_key=None, *, options=None)` is an escape hatch for
|
|
124
|
+
endpoints that do not have a method yet. The full surface is in
|
|
125
|
+
[docs/SDK_SPEC.md](../../docs/SDK_SPEC.md) §3. Inputs and outputs use the API wire format
|
|
126
|
+
(snake_case), described by [openapi/openapi-v3.json](../../openapi/openapi-v3.json).
|
|
127
|
+
|
|
128
|
+
## Options
|
|
129
|
+
|
|
130
|
+
| Argument | Default | Meaning |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `base_url` | `"production"` | `"production"`, `"sandbox"`, or an absolute URL (host only). |
|
|
133
|
+
| `timeout` | `15.0` s | Timeout for each attempt. |
|
|
134
|
+
| `max_retries` | `2` | Automatic retries (see below). |
|
|
135
|
+
| `max_retry_wait` | `30.0` s | Longest `Retry-After` honoured automatically. |
|
|
136
|
+
| `user_agent` | `plasgos-crm-sdk-python/<version> python/<version>` | Integrations append themselves. |
|
|
137
|
+
| `transport` / `http_client` | — | Inject an `httpx` transport (e.g. `httpx.MockTransport`) or your own `httpx.Client`. |
|
|
138
|
+
| `sleeper`, `clock` | real | Test hooks. |
|
|
139
|
+
|
|
140
|
+
Logging: enable DEBUG on the `plasgos_crm` logger for one record per attempt (method, path without
|
|
141
|
+
the query, status, duration, request id); headers and bodies are never logged.
|
|
142
|
+
|
|
143
|
+
Every method also takes keyword-only per-call options (docs/SDK_SPEC.md §1.1):
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
client.messages.send(body, "wc-1234-processing", options={"timeout": 5, "max_retries": 0})
|
|
147
|
+
client.contacts.get(contact_id, options={"headers": {"X-Trace-Id": trace_id}})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Responses
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
r.http_status # 200, 201, 202 …
|
|
154
|
+
r.message # user-facing text
|
|
155
|
+
r.data # payload
|
|
156
|
+
r.meta # e.g. {"created": True} (201) / {"created": False} (200)
|
|
157
|
+
r.request_id # quote it when contacting Plasgos
|
|
158
|
+
r.rate_limit # RateLimit(limit=120, remaining=119, reset=42)
|
|
159
|
+
r.replayed # True when the server replayed the first response for this Idempotency-Key
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
List methods return a `Page`: `items`, `pagination`, `has_next_page()`, `next_page()`,
|
|
163
|
+
`auto_paging()` (generator); it is iterable over the current page and supports `len()`.
|
|
164
|
+
|
|
165
|
+
Request bodies, query parameters and `data` are typed with `TypedDict`s generated from the OpenAPI spec
|
|
166
|
+
(checked by mypy/pyright; plain dicts at runtime):
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from plasgos_crm.v3 import types as t
|
|
170
|
+
|
|
171
|
+
sent = client.messages.send({"endpoint_id": "…", "contact": {"phone_number": "0812…"}, "content": Content.text("Halo")})
|
|
172
|
+
status: t.MessageStatus = sent.data["status"]
|
|
173
|
+
page = client.contacts.list({"tag": "vip"}) # Page[t.Contact]
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Errors
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
PlasgosApiError
|
|
180
|
+
├─ AuthenticationError 401
|
|
181
|
+
│ └─ SignatureExpiredError 401 signature_expired (check the server clock)
|
|
182
|
+
├─ PermissionError 403 (scope_missing, api_key_revoked, …)
|
|
183
|
+
│ └─ PlanError 403 not_entitled
|
|
184
|
+
├─ NotFoundError 404
|
|
185
|
+
├─ ConflictError 409 (idempotency_in_progress, identity_taken, …)
|
|
186
|
+
├─ BadRequestError 400
|
|
187
|
+
├─ ValidationError 422 / 400 with fields → .fields
|
|
188
|
+
├─ RateLimitError 429 → .retry_after (rate_limited | daily_quota_exceeded)
|
|
189
|
+
├─ ServerError 5xx
|
|
190
|
+
│ ├─ ChannelError 502 provider error
|
|
191
|
+
│ └─ ServiceUnavailableError 503
|
|
192
|
+
└─ TransportError no response
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Every error exposes `error_code`, `error_type`, `details`, `fields`, `doc_url`, `request_id` and
|
|
196
|
+
`retry_after` (plus the legacy `errors` view `[{message, path}]`). `PermissionError` deliberately
|
|
197
|
+
shadows the builtin inside this package; import it as `from plasgos_crm import PermissionError as
|
|
198
|
+
PlasgosPermissionError` if that bothers you.
|
|
199
|
+
|
|
200
|
+
**Retries.** Transport errors, 502/503 and `rate_limited` (for any method) are retried
|
|
201
|
+
automatically up to `max_retries`, reusing the same idempotency key. A plain 500 is retried only for
|
|
202
|
+
GET. Other 4xx errors and `daily_quota_exceeded` are never retried.
|
|
203
|
+
|
|
204
|
+
## Webhooks
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from plasgos_crm import Webhook, WebhookEventType, WebhookVerificationError
|
|
208
|
+
|
|
209
|
+
# Flask
|
|
210
|
+
@app.post("/webhooks/plasgos")
|
|
211
|
+
def plasgos_webhook():
|
|
212
|
+
try:
|
|
213
|
+
event = Webhook.verify(request.get_data(), request.headers, signing_secret)
|
|
214
|
+
# during secret rotation: Webhook.verify(body, headers, [new_secret, previous_secret])
|
|
215
|
+
except WebhookVerificationError:
|
|
216
|
+
return "", 401
|
|
217
|
+
if event["type"] == WebhookEventType.MESSAGE_RECEIVED:
|
|
218
|
+
... # event["data"] is a Message
|
|
219
|
+
return "", 200
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`headers` may be any mapping, including a WSGI `environ` or Django `request.META` (`HTTP_*` keys).
|
|
223
|
+
Verify the **raw** body. Deliveries can repeat, so drop `event["id"]` values you have already
|
|
224
|
+
processed.
|
|
225
|
+
|
|
226
|
+
## Development
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
pip install -e ".[dev]"
|
|
230
|
+
pytest # tests/test_v3_live.py runs only with PLASGOS_LIVE_BASE_URL/_API_KEY/_SECRET_KEY
|
|
231
|
+
mypy src
|
|
232
|
+
ruff check .
|
|
233
|
+
```
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# plasgos-crm-sdk (Python)
|
|
2
|
+
|
|
3
|
+
Official Python SDK for **Plasgos CRM: API Integrasi v3**. The SDK covers:
|
|
4
|
+
|
|
5
|
+
- omnichannel messages (WhatsApp official and unofficial, Instagram, Messenger)
|
|
6
|
+
- contacts and leads
|
|
7
|
+
- broadcasts
|
|
8
|
+
- followups
|
|
9
|
+
- signed webhooks
|
|
10
|
+
|
|
11
|
+
Features:
|
|
12
|
+
|
|
13
|
+
- Python 3.9+, one dependency (`httpx`), fully typed (`py.typed`, mypy strict).
|
|
14
|
+
- Automatic request signing that covers the method, path, query and body.
|
|
15
|
+
- An automatic `Idempotency-Key` on every POST, reused on retries.
|
|
16
|
+
- Typed errors carrying the API `error_code`.
|
|
17
|
+
- A `Page` type with `auto_paging()`.
|
|
18
|
+
- Webhook HMAC verification.
|
|
19
|
+
|
|
20
|
+
> Upgrading from 1.x? See [UPGRADING.md](../../UPGRADING.md). The v1 client lives on as the
|
|
21
|
+
> deprecated `LegacyClient` (`PlasgosCrmClient` is an alias); its docs are in [LEGACY_V1.md](LEGACY_V1.md).
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install plasgos-crm-sdk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import os
|
|
33
|
+
|
|
34
|
+
from plasgos_crm import Client, Content, PlasgosApiError
|
|
35
|
+
|
|
36
|
+
with Client(
|
|
37
|
+
os.environ["PLASGOS_API_KEY"],
|
|
38
|
+
os.environ["PLASGOS_SECRET_KEY"],
|
|
39
|
+
base_url="sandbox", # "production" by default; or an absolute URL
|
|
40
|
+
) as client:
|
|
41
|
+
me = client.me.get()
|
|
42
|
+
print(me.data["tenant"]["name"])
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
sent = client.messages.send(
|
|
46
|
+
{
|
|
47
|
+
"endpoint_id": "6281100000001",
|
|
48
|
+
"contact": {"phone_number": "081234567890", "name": "Budi Santoso"}, # profile only for a NEW contact
|
|
49
|
+
"content": Content.template("66f1a2b3c4d5e6f708192a3b")
|
|
50
|
+
.dynamic("name", "{{full_name}}")
|
|
51
|
+
.custom("order", "INV-2026-0917"),
|
|
52
|
+
},
|
|
53
|
+
idempotency_key="wc-1234-processing", # one per intent
|
|
54
|
+
)
|
|
55
|
+
print(sent.data["status"]) # pending | sent | failed (202)
|
|
56
|
+
except PlasgosApiError as error:
|
|
57
|
+
# Branch on the stable code; show error.message (Indonesian) to people.
|
|
58
|
+
if error.error_code == "requires_template":
|
|
59
|
+
...
|
|
60
|
+
raise
|
|
61
|
+
|
|
62
|
+
for contact in client.contacts.list(per_page=100).auto_paging():
|
|
63
|
+
... # every page fetched lazily
|
|
64
|
+
|
|
65
|
+
client.contacts.upsert({"phone_number": "081234567890", "name": "Budi", "tags": ["woocommerce"]})
|
|
66
|
+
client.leads.create({"contact": {"phone_number": "081234567890"}, "pipeline_id": "pl_1"})
|
|
67
|
+
client.followups.enroll("fu_1", {"contact": {"id": "ct_1"}})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### asyncio
|
|
71
|
+
|
|
72
|
+
`AsyncClient` has the same resources, methods and parameters (each method is `async`; lists return an
|
|
73
|
+
`AsyncPage`):
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from plasgos_crm import AsyncClient
|
|
77
|
+
|
|
78
|
+
async with AsyncClient(api_key, secret_key, base_url="sandbox") as client:
|
|
79
|
+
me = await client.me.get()
|
|
80
|
+
page = await client.contacts.list(per_page=100)
|
|
81
|
+
async for contact in page.auto_paging():
|
|
82
|
+
...
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
It takes an `httpx.AsyncBaseTransport` / `httpx.AsyncClient` and an async `sleeper`; close it with
|
|
86
|
+
`async with` or `await client.aclose()`.
|
|
87
|
+
|
|
88
|
+
Resources:
|
|
89
|
+
|
|
90
|
+
- `me`, `channels`
|
|
91
|
+
- `endpoints` (including `templates`), `template_variables`
|
|
92
|
+
- `messages` (`send_options`, `send`, `get`)
|
|
93
|
+
- `contacts`, `contact_groups`
|
|
94
|
+
- `pipelines`, `leads`
|
|
95
|
+
- `broadcasts`, `followups`
|
|
96
|
+
- `webhooks` (including `rotate_secret`)
|
|
97
|
+
|
|
98
|
+
Query parameters may be a mapping, keyword arguments, or both:
|
|
99
|
+
`client.contacts.list({"tag": "vip"}, per_page=100)`.
|
|
100
|
+
`client.request(method, path, query=None, body=None, idempotency_key=None, *, options=None)` is an escape hatch for
|
|
101
|
+
endpoints that do not have a method yet. The full surface is in
|
|
102
|
+
[docs/SDK_SPEC.md](../../docs/SDK_SPEC.md) §3. Inputs and outputs use the API wire format
|
|
103
|
+
(snake_case), described by [openapi/openapi-v3.json](../../openapi/openapi-v3.json).
|
|
104
|
+
|
|
105
|
+
## Options
|
|
106
|
+
|
|
107
|
+
| Argument | Default | Meaning |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `base_url` | `"production"` | `"production"`, `"sandbox"`, or an absolute URL (host only). |
|
|
110
|
+
| `timeout` | `15.0` s | Timeout for each attempt. |
|
|
111
|
+
| `max_retries` | `2` | Automatic retries (see below). |
|
|
112
|
+
| `max_retry_wait` | `30.0` s | Longest `Retry-After` honoured automatically. |
|
|
113
|
+
| `user_agent` | `plasgos-crm-sdk-python/<version> python/<version>` | Integrations append themselves. |
|
|
114
|
+
| `transport` / `http_client` | — | Inject an `httpx` transport (e.g. `httpx.MockTransport`) or your own `httpx.Client`. |
|
|
115
|
+
| `sleeper`, `clock` | real | Test hooks. |
|
|
116
|
+
|
|
117
|
+
Logging: enable DEBUG on the `plasgos_crm` logger for one record per attempt (method, path without
|
|
118
|
+
the query, status, duration, request id); headers and bodies are never logged.
|
|
119
|
+
|
|
120
|
+
Every method also takes keyword-only per-call options (docs/SDK_SPEC.md §1.1):
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
client.messages.send(body, "wc-1234-processing", options={"timeout": 5, "max_retries": 0})
|
|
124
|
+
client.contacts.get(contact_id, options={"headers": {"X-Trace-Id": trace_id}})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Responses
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
r.http_status # 200, 201, 202 …
|
|
131
|
+
r.message # user-facing text
|
|
132
|
+
r.data # payload
|
|
133
|
+
r.meta # e.g. {"created": True} (201) / {"created": False} (200)
|
|
134
|
+
r.request_id # quote it when contacting Plasgos
|
|
135
|
+
r.rate_limit # RateLimit(limit=120, remaining=119, reset=42)
|
|
136
|
+
r.replayed # True when the server replayed the first response for this Idempotency-Key
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
List methods return a `Page`: `items`, `pagination`, `has_next_page()`, `next_page()`,
|
|
140
|
+
`auto_paging()` (generator); it is iterable over the current page and supports `len()`.
|
|
141
|
+
|
|
142
|
+
Request bodies, query parameters and `data` are typed with `TypedDict`s generated from the OpenAPI spec
|
|
143
|
+
(checked by mypy/pyright; plain dicts at runtime):
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from plasgos_crm.v3 import types as t
|
|
147
|
+
|
|
148
|
+
sent = client.messages.send({"endpoint_id": "…", "contact": {"phone_number": "0812…"}, "content": Content.text("Halo")})
|
|
149
|
+
status: t.MessageStatus = sent.data["status"]
|
|
150
|
+
page = client.contacts.list({"tag": "vip"}) # Page[t.Contact]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Errors
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
PlasgosApiError
|
|
157
|
+
├─ AuthenticationError 401
|
|
158
|
+
│ └─ SignatureExpiredError 401 signature_expired (check the server clock)
|
|
159
|
+
├─ PermissionError 403 (scope_missing, api_key_revoked, …)
|
|
160
|
+
│ └─ PlanError 403 not_entitled
|
|
161
|
+
├─ NotFoundError 404
|
|
162
|
+
├─ ConflictError 409 (idempotency_in_progress, identity_taken, …)
|
|
163
|
+
├─ BadRequestError 400
|
|
164
|
+
├─ ValidationError 422 / 400 with fields → .fields
|
|
165
|
+
├─ RateLimitError 429 → .retry_after (rate_limited | daily_quota_exceeded)
|
|
166
|
+
├─ ServerError 5xx
|
|
167
|
+
│ ├─ ChannelError 502 provider error
|
|
168
|
+
│ └─ ServiceUnavailableError 503
|
|
169
|
+
└─ TransportError no response
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Every error exposes `error_code`, `error_type`, `details`, `fields`, `doc_url`, `request_id` and
|
|
173
|
+
`retry_after` (plus the legacy `errors` view `[{message, path}]`). `PermissionError` deliberately
|
|
174
|
+
shadows the builtin inside this package; import it as `from plasgos_crm import PermissionError as
|
|
175
|
+
PlasgosPermissionError` if that bothers you.
|
|
176
|
+
|
|
177
|
+
**Retries.** Transport errors, 502/503 and `rate_limited` (for any method) are retried
|
|
178
|
+
automatically up to `max_retries`, reusing the same idempotency key. A plain 500 is retried only for
|
|
179
|
+
GET. Other 4xx errors and `daily_quota_exceeded` are never retried.
|
|
180
|
+
|
|
181
|
+
## Webhooks
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from plasgos_crm import Webhook, WebhookEventType, WebhookVerificationError
|
|
185
|
+
|
|
186
|
+
# Flask
|
|
187
|
+
@app.post("/webhooks/plasgos")
|
|
188
|
+
def plasgos_webhook():
|
|
189
|
+
try:
|
|
190
|
+
event = Webhook.verify(request.get_data(), request.headers, signing_secret)
|
|
191
|
+
# during secret rotation: Webhook.verify(body, headers, [new_secret, previous_secret])
|
|
192
|
+
except WebhookVerificationError:
|
|
193
|
+
return "", 401
|
|
194
|
+
if event["type"] == WebhookEventType.MESSAGE_RECEIVED:
|
|
195
|
+
... # event["data"] is a Message
|
|
196
|
+
return "", 200
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`headers` may be any mapping, including a WSGI `environ` or Django `request.META` (`HTTP_*` keys).
|
|
200
|
+
Verify the **raw** body. Deliveries can repeat, so drop `event["id"]` values you have already
|
|
201
|
+
processed.
|
|
202
|
+
|
|
203
|
+
## Development
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
pip install -e ".[dev]"
|
|
207
|
+
pytest # tests/test_v3_live.py runs only with PLASGOS_LIVE_BASE_URL/_API_KEY/_SECRET_KEY
|
|
208
|
+
mypy src
|
|
209
|
+
ruff check .
|
|
210
|
+
```
|