Skip to content

Commit 6be72e5

Browse files
Domantasclaude
andcommitted
feat(notifications): document the required provider integration id on campaigns and triggers
The gateway now requires the provider integrationId when an Email, Push or SMS campaign is created (Email/Push: campaign.integrationId; SMS: a new top-level integrationId, distinct from databaseIntegrationId) and on every trigger action, which also takes the optional language and initiatorId. The SDK methods pass the request through, so no signature changes; the README and the SMS / Push pages now show the id, and tests pin that it reaches the wire for each case. Breaking: the server now requires the provider integration id. Calls that create a campaign or save a trigger without integrationId are refused; pass the id from get_email_integrations / get_push_integrations / get_sms_integrations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 1499e39 commit 6be72e5

6 files changed

Lines changed: 106 additions & 3 deletions

File tree

‎README.md‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,44 @@ user key: `create_ai_service_user`, `list_ai_service_users`,
169169
Full tables and an MCP example: [docs/hub/account.md](./docs/hub/account.md),
170170
[docs/api/public.md](./docs/api/public.md).
171171

172+
## Campaigns and triggers: the provider integration id
173+
174+
The server now **requires the provider integration id** when you create an
175+
Email, Push or SMS campaign, and on every trigger action. Pick it from
176+
`get_email_integrations` / `get_push_integrations` / `get_sms_integrations`.
177+
Email and Push put it inside `campaign`; SMS sends it at the top level, next to
178+
(and different from) `databaseIntegrationId`:
179+
180+
```python
181+
norbix.hub.notifications.create_email_campaign(
182+
campaign={"templateId": "tpl_123", "integrationId": "int_email", "source": "allUsers"}
183+
)
184+
norbix.hub.notifications.create_push_campaign(
185+
campaign={"source": "allUsers", "templateId": "tpl_456", "integrationId": "int_push"}
186+
)
187+
norbix.hub.notifications.create_sms_campaign(
188+
templateId="tpl_789",
189+
integrationId="int_sms",
190+
deliveryType="PhoneNumbers",
191+
phoneNumbers={"recipientsSourceType": "PhoneNumbers", "phoneNumbers": ["+37060000000"]},
192+
)
193+
```
194+
195+
A trigger action (`save_schema_trigger`, `save_files_trigger`,
196+
`save_membership_trigger`, `save_payments_trigger`) takes the same required
197+
`integrationId`, plus the optional `language` (template language) and
198+
`initiatorId` (who the send is attributed to) on Email, Push and SMS actions.
199+
Two related calls:
200+
201+
- `norbix.hub.triggers.get_triggers_needing_attention(triggerType="Schema")` —
202+
triggers that need a fix (for example, their provider is gone); `{"items": [...]}`.
203+
- `norbix.hub.account.check_project_languages(project_id, languages=["en", "de"])` —
204+
a dry run before `update_project_languages`: which templates miss a language;
205+
`{"templates": [...]}`. Account scope.
206+
207+
Details: [docs/hub/triggers.md](./docs/hub/triggers.md),
208+
[docs/hub/sms.md](./docs/hub/sms.md), [docs/hub/push.md](./docs/hub/push.md).
209+
172210
## Errors
173211

174212
```python
@@ -365,6 +403,10 @@ uv run python scripts/generate_endpoints.py
365403

366404
This refreshes `src/norbix_python/api/`, `hub/`, matching tests under `tests/api` and `tests/hub`, and docs under `docs/`.
367405

406+
Some methods are **hand-written** and marked `HAND-WRITTEN` in the source
407+
(`hub/triggers.py`, its registration in `hub/__init__.py`, and
408+
`account.check_project_languages`). Keep them when you regenerate.
409+
368410
## Development
369411

370412
```bash

‎docs/hub/push.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ field plus the audience's own fields:
2121
| rows of a database collection | `collection` | `schemaName`, `fields` (the record fields that hold the recipient), `fieldType` (`User` or `Email`), optional `roleNames`, `languages` |
2222
| raw device tokens | `devices` | `devices`: a list of `{ token, deliveryFamily }`, `deliveryFamily` one of `Ios`, `Android`, `Chrome`, `Safari`, `Expo` |
2323

24-
Every target also takes `templateId` (required) and the optional `integrationId`,
25-
`language`, `notes`, `campaignTime` (Unix seconds) and `mappedTokens`. Note the
24+
Every target also takes `templateId` and `integrationId` (both required —
25+
`integrationId` is the push provider, from `get_push_integrations`), and the
26+
optional `language`, `notes`, `campaignTime` (Unix seconds) and `mappedTokens`. Note the
2627
spelling: `rolesNames` on `allUsers`, but `roleNames` on `collection` — the
2728
gateway names them differently.
2829

@@ -31,6 +32,7 @@ client.hub.notifications.create_push_campaign(
3132
campaign={
3233
"source": "allUsers",
3334
"templateId": "tpl_123",
35+
"integrationId": "int_push",
3436
"userTags": ["beta"],
3537
}
3638
)

‎docs/hub/sms.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,17 @@ and the settings object repeats the audience name in `recipientsSourceType`:
2525
Every settings object also takes `campaignTime` (Unix seconds, UTC — set a
2626
future time so the campaign can be reviewed in the dashboard first), and the
2727
optional `mappedTokens` and `respectTimeZoneSettings`. The request itself takes
28-
`templateId` (required), and the optional `language` and `databaseIntegrationId`.
28+
`templateId` and `integrationId` (both required — `integrationId` is the SMS
29+
provider, from `get_sms_integrations`), and the optional `language` and
30+
`databaseIntegrationId` (the database behind a `Collection` audience — a
31+
different id from `integrationId`).
2932
Note the spelling: `rolesNames` on `allUsers`, but `roleNames` on `collection` —
3033
the gateway names them differently.
3134

3235
```python
3336
client.hub.notifications.create_sms_campaign(
3437
templateId="tpl_123",
38+
integrationId="int_sms",
3539
deliveryType="PhoneNumbers",
3640
phoneNumbers={
3741
"recipientsSourceType": "PhoneNumbers",

‎tests/hub/test_email_routes.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,3 +478,20 @@ async def run() -> None:
478478

479479
assert len(seen) == 1
480480
assert seen[0].headers.get("Authorization") is None
481+
482+
483+
def test_create_email_campaign_sends_the_provider_integration_id_nested() -> None:
484+
# The gateway requires the email provider id inside `campaign`.
485+
import json
486+
487+
client, transport = make_client()
488+
client.hub.notifications.create_email_campaign(
489+
campaign={"templateId": "tpl_1", "integrationId": "int_1", "source": "allUsers"}
490+
)
491+
492+
assert transport.last_request is not None
493+
assert transport.last_request["method"] == "POST"
494+
assert urlparse(transport.last_request["url"]).path == "/v2/notifications/email/campaigns"
495+
assert json.loads(transport.last_request["body"]) == {
496+
"campaign": {"templateId": "tpl_1", "integrationId": "int_1", "source": "allUsers"}
497+
}

‎tests/hub/test_push.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,18 @@ def test_create_push_campaign_sends_the_target_shape(source: str, fields: dict[s
270270
assert campaign == {"source": source, "templateId": TEMPLATE_ID, **fields}
271271

272272

273+
def test_create_push_campaign_sends_the_provider_integration_id_nested() -> None:
274+
# The gateway requires the push provider id inside `campaign`.
275+
client, transport = make_client()
276+
client.hub.notifications.create_push_campaign(
277+
campaign={"source": "allUsers", "templateId": TEMPLATE_ID, "integrationId": "int_1"}
278+
)
279+
280+
assert transport.last_request is not None
281+
body = json.loads(transport.last_request["body"])
282+
assert body == {"campaign": {"source": "allUsers", "templateId": TEMPLATE_ID, "integrationId": "int_1"}}
283+
284+
273285
def test_campaign_targets_match_the_gateway() -> None:
274286
assert {source.lower() for source, _ in CAMPAIGN_TARGETS} == GATEWAY_TARGETS
275287
assert len(CAMPAIGN_TARGETS) == len(GATEWAY_TARGETS)

‎tests/hub/test_sms.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,32 @@ def test_campaign_audiences_match_the_gateway() -> None:
318318
assert len(CAMPAIGN_AUDIENCES) == len(GATEWAY_AUDIENCES)
319319

320320

321+
def test_create_sms_campaign_sends_the_provider_integration_id_top_level() -> None:
322+
# The gateway requires the SMS provider id as a top-level `integrationId`.
323+
# It is a different field from `databaseIntegrationId` (the database that
324+
# holds a `Collection` audience), so both must reach the wire side by side.
325+
client, transport = make_client()
326+
client.hub.notifications.create_sms_campaign(
327+
templateId=TEMPLATE_ID,
328+
integrationId=INTEGRATION_ID,
329+
databaseIntegrationId="db_1",
330+
deliveryType="Collection",
331+
collectionSettings={"schemaName": "subscribers", "fields": ["phone"], "campaignTime": CAMPAIGN_TIME},
332+
)
333+
334+
assert transport.last_request is not None
335+
assert transport.last_request["method"] == "POST"
336+
assert urlparse(transport.last_request["url"]).path == f"{BASE}/campaigns"
337+
body = json.loads(transport.last_request["body"])
338+
assert body == {
339+
"templateId": TEMPLATE_ID,
340+
"integrationId": INTEGRATION_ID,
341+
"databaseIntegrationId": "db_1",
342+
"deliveryType": "Collection",
343+
"collectionSettings": {"schemaName": "subscribers", "fields": ["phone"], "campaignTime": CAMPAIGN_TIME},
344+
}
345+
346+
321347
# The eight providers the gateway's save converter accepts, with the fields
322348
# each one validates. Source of truth: gateway Hub.Sms/Integrations/Save_.cs
323349
# (the switch arms of the converter, read from `provider`) and

0 commit comments

Comments
 (0)