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.
Files changed (51) hide show
  1. plasgos_crm_sdk-2.0.0/.gitignore +28 -0
  2. plasgos_crm_sdk-2.0.0/CHANGELOG.md +51 -0
  3. plasgos_crm_sdk-2.0.0/LEGACY_V1.md +137 -0
  4. plasgos_crm_sdk-2.0.0/LICENSE +21 -0
  5. plasgos_crm_sdk-2.0.0/PKG-INFO +233 -0
  6. plasgos_crm_sdk-2.0.0/README.md +210 -0
  7. plasgos_crm_sdk-2.0.0/pyproject.toml +46 -0
  8. plasgos_crm_sdk-2.0.0/src/plasgos_crm/__init__.py +100 -0
  9. plasgos_crm_sdk-2.0.0/src/plasgos_crm/_envelope.py +18 -0
  10. plasgos_crm_sdk-2.0.0/src/plasgos_crm/_http.py +205 -0
  11. plasgos_crm_sdk-2.0.0/src/plasgos_crm/_signature.py +36 -0
  12. plasgos_crm_sdk-2.0.0/src/plasgos_crm/_version.py +3 -0
  13. plasgos_crm_sdk-2.0.0/src/plasgos_crm/client.py +98 -0
  14. plasgos_crm_sdk-2.0.0/src/plasgos_crm/credentials_client.py +68 -0
  15. plasgos_crm_sdk-2.0.0/src/plasgos_crm/errors.py +224 -0
  16. plasgos_crm_sdk-2.0.0/src/plasgos_crm/py.typed +0 -0
  17. plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/__init__.py +13 -0
  18. plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/_base.py +22 -0
  19. plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/accounts.py +16 -0
  20. plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/credentials.py +41 -0
  21. plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/messages.py +61 -0
  22. plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/official/__init__.py +21 -0
  23. plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/official/accounts.py +19 -0
  24. plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/official/broadcasts.py +67 -0
  25. plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/official/messages.py +31 -0
  26. plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/official/shared_waba.py +26 -0
  27. plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/official/templates.py +18 -0
  28. plasgos_crm_sdk-2.0.0/src/plasgos_crm/resources/references.py +25 -0
  29. plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/__init__.py +28 -0
  30. plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/_api_client.py +459 -0
  31. plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/async_client.py +124 -0
  32. plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/async_resources.py +214 -0
  33. plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/client.py +127 -0
  34. plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/content.py +98 -0
  35. plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/resources.py +223 -0
  36. plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/response.py +112 -0
  37. plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/signature.py +51 -0
  38. plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/types.py +1071 -0
  39. plasgos_crm_sdk-2.0.0/src/plasgos_crm/v3/webhook.py +142 -0
  40. plasgos_crm_sdk-2.0.0/src/plasgos_crm/webhooks/__init__.py +9 -0
  41. plasgos_crm_sdk-2.0.0/src/plasgos_crm/webhooks/events.py +8 -0
  42. plasgos_crm_sdk-2.0.0/src/plasgos_crm/webhooks/verify.py +59 -0
  43. plasgos_crm_sdk-2.0.0/tests/conftest.py +14 -0
  44. plasgos_crm_sdk-2.0.0/tests/test_client.py +192 -0
  45. plasgos_crm_sdk-2.0.0/tests/test_signature.py +22 -0
  46. plasgos_crm_sdk-2.0.0/tests/test_v3_async.py +140 -0
  47. plasgos_crm_sdk-2.0.0/tests/test_v3_client.py +481 -0
  48. plasgos_crm_sdk-2.0.0/tests/test_v3_live.py +111 -0
  49. plasgos_crm_sdk-2.0.0/tests/test_v3_types.py +31 -0
  50. plasgos_crm_sdk-2.0.0/tests/test_v3_webhook.py +69 -0
  51. 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
+ ```