Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 73 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
- [Managing chatbot (automated responses) status settings](#bot-on-off)
- [Get chatbot (automated responses) status](#bot-status)

#### Webhooks

- [Subscribe to webhook](#subscribe-webhook)

#### Mics

- [Pagination](#pagination)
Expand Down Expand Up @@ -526,6 +530,59 @@ response.chatbot_active # True/False

```

## Webhooks

### <a href='#subscribe-webhook'>Subscribe to webhook</a>

Register a URL of yours and subscribe it to a webhook event, so we start sending
you requests when that event happens. This does the same thing as activating a
webhook URL in the Sendbee Dashboard.

```python
webhook = api.subscribe_webhook(
url='https://example.com/sendbee/webhook',
event='message.received'
)

webhook.id # UUID of the webhook endpoint
webhook.url # the URL you registered
webhook.active # True/False
webhook.selected_webhooks # list of all events this URL is subscribed to
webhook.secret_key # key used to sign requests we send to your URL
webhook.created_at
```

Both `url` and `event` are required. Calling it again for the same `url` is safe -
it will not create a duplicate, it just adds the new event to that URL (and
re-activates it if it was deactivated), so subscribe one event per call:

```python
for event in ('message.received', 'message.sent'):
api.subscribe_webhook(url='https://example.com/sendbee/webhook', event=event)
```

Available events:

| Event | Description |
| --- | --- |
| `contact.created` | A new contact was created |
| `contact.subscription_created` | A contact subscribed |
| `contact.subscription_updated` | A contact's subscription changed |
| `contact.unsubscribed` | A contact unsubscribed |
| `message.received` | A message was received from a contact |
| `message.sent` | A message was sent to a contact |
| `message.sent_status` | Delivery status of a sent message changed |
| `whatsapp.message.session.sent_status` | Delivery status of a session message changed |
| `whatsapp.message.template.sent_status` | Delivery status of a template message changed |
| `whatsapp.message.template.approval_status` | A message template was approved or rejected |
| `conversation.change_folder` | A conversation was moved to another folder |

An unknown event name returns a `SendbeeRequestApiException`. For the authoritative
and up to date list, see the [documentation](https://developer.ainumber.com/#webhooks).

Once subscribed, see [Authenticate webhook request](#authenticate-webhook-request)
for how to verify the requests we send you.

## Misc

### <a href='#pagination'>Pagination</a>
Expand Down Expand Up @@ -611,7 +668,7 @@ except SendbeeRequestApiException as e:

### <a href='#authenticate-webhook-request'>Authenticate webhook request</a>

After activating your webhook URL in Sendbee Dashboard, we will start sending requests on that URL depending on which webhook type is linked with that webhook URL.
After activating your webhook URL in Sendbee Dashboard or with [Subscribe to webhook](#subscribe-webhook), we will start sending requests on that URL depending on which webhook type is linked with that webhook URL.
Every request that we make will have authorization token in header, like this:

```
Expand All @@ -634,6 +691,21 @@ if not api.auth.check_auth_token(token):
# error! authentication failed!
```

Requests to a webhook URL that has its own secret key also carry a second header,
`X-Auth-Token`, signed with that key instead of your account secret. You get the
key back as `webhook.secret_key` from [Subscribe to webhook](#subscribe-webhook).
Check it the same way, using `SendbeeAuth` directly:

```python
from sendbee_api import SendbeeAuth

secret_key = '...' # webhook.secret_key, store it when you subscribe

token = '...' # taken from the 'X-Auth-Token' request header
if not SendbeeAuth(secret_key).check_auth_token(token):
# error! authentication failed!
```

### <a href='#warnings'>Warnings</a>

Sometimes APi returns a warning so you could be warned about something.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "sendbee_api"
version = "1.7.2"
version = "1.8.0"
description = "Python client SDK for Sendbee Public API"
readme = "README.md"
requires-python = ">=3.6"
Expand Down
3 changes: 2 additions & 1 deletion sendbee_api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
from sendbee_api.auth import SendbeeAuth
from sendbee_api.teams.client import Teams
from sendbee_api.contacts.client import Contacts
from sendbee_api.webhooks.client import Webhooks
from sendbee_api.rate_limit.client import RateLimit
from sendbee_api.automation.client import Automation
from sendbee_api.conversations.client import Messages
from sendbee_api.exceptions import SendbeeRequestApiException


class Client(Contacts, Messages, Automation, Teams, RateLimit):
class Client(Contacts, Messages, Automation, Teams, Webhooks, RateLimit):
"""Main API class. Sets all API calls."""

base_url = 'api-v2.sendbee.io'
Expand Down
Empty file.
16 changes: 16 additions & 0 deletions sendbee_api/webhooks/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from sendbee_api import constants
from sendbee_api.webhooks import models
from sendbee_api.bind import bind_request
from sendbee_api.webhooks import query_params


class Webhooks:
"""Api client for webhooks"""

subscribe_webhook = bind_request(
api_path='/webhooks/subscribe',
model=models.Webhook,
method=constants.RequestConst.POST,
query_parameters=query_params.SubscribeWebhook,
description='Api client for subscribing to a webhook event'
)
22 changes: 22 additions & 0 deletions sendbee_api/webhooks/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from sendbee_api.models import Model
from sendbee_api.fields import (
TextField, BooleanField, DatetimeField, ListField
)


class Webhook(Model):
"""Data model for a webhook endpoint subscription"""

_id = TextField(index='id', desc='UUID')
_url = TextField(index='url', desc='Webhook endpoint URL')
_active = BooleanField(index='active', desc='Is the endpoint active')
_secret_key = TextField(
index='secret_key',
desc='Key that signs the X-Auth-Token header on inbound webhooks'
)
_selected_webhooks = ListField(
index='selected_webhooks', desc='Subscribed webhook event names'
)
_created_at = DatetimeField(
index='created_at', desc='Created at', format='%Y-%m-%d %H:%M:%'
)
9 changes: 9 additions & 0 deletions sendbee_api/webhooks/query_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from sendbee_api.query_params import QueryParams


class SubscribeWebhook(QueryParams):
"""Parameters for subscribing to a webhook event"""

url = 'url', 'Webhook endpoint URL that will receive the events'
event = 'event', 'Webhook event name, e.g. "message.received". ' \
'See https://developer.ainumber.com/#webhooks'
40 changes: 39 additions & 1 deletion tests/test_endpoints_smoke.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""End-to-end smoke tests: one happy-path per resource mixin.

The 24 endpoint methods on `SendbeeApi` are all generated by `bind_request`,
The 25 endpoint methods on `SendbeeApi` are all generated by `bind_request`,
which is covered in detail elsewhere. These tests verify the wiring per mixin
- that each `bind_request(...)` call site declares the right `api_path`,
`method`, `model`, and `QueryParams` - by exercising one representative
Expand All @@ -14,6 +14,7 @@
from sendbee_api.conversations.models import Conversation, SentMessage
from sendbee_api.automation.models import ChatbotActivityStatus
from sendbee_api.teams.models import Team
from sendbee_api.webhooks.models import Webhook
from sendbee_api.rate_limit.models import RateLimitError
from sendbee_api.models import ServerMessage

Expand Down Expand Up @@ -125,6 +126,43 @@ def test_teams_list_endpoint_returns_team_models(client, register, json_body):
assert response.models[0].name == "Support"


@responses.activate
def test_subscribe_webhook_endpoint_returns_webhook_model(client, register, json_body):
"""subscribe_webhook POSTs to /webhooks/subscribe and parses one Webhook.

The body is a bare object rather than a list because the backend's
PaginationJsonRenderer emits {"data": {...}} with no meta for non-GET
requests; Response.models wraps that lone dict before processing it.
"""
register("POST", "/webhooks/subscribe", body=json_body(
data={
"id": "wh1",
"active": True,
"url": "https://example.com/hook",
"secret_key": "s3cr3t",
"selected_webhooks": ["message.received"],
"created_at": "2026-07-27 10:11:12",
},
))

result = client.subscribe_webhook(
url="https://example.com/hook", event="message.received"
)

assert responses.calls[0].request.method == "POST"
assert "/webhooks/subscribe" in responses.calls[0].request.url

body = ujson.loads(responses.calls[0].request.body)
assert body == {"url": "https://example.com/hook", "event": "message.received"}

assert isinstance(result, Webhook)
assert result.id == "wh1"
assert result.active is True
assert result.url == "https://example.com/hook"
assert result.secret_key == "s3cr3t"
assert result.selected_webhooks == ["message.received"]


@responses.activate
def test_rate_limit_error_test_endpoint_returns_rate_limit_error_model(client, register, json_body):
"""rate_limit_error_test hits /rate-limit/error-test on the RateLimit mixin."""
Expand Down
Loading