drf-directmessages
A small, lightweight Django REST Framework library that adds direct messaging between your users. Drop it into any DRF project, run migrations, and you have a fully functional private messaging API.
Features
- Send and receive direct messages between users
- List conversation partners with cursor-based pagination
- Retrieve all messages in a conversation (inbound messages are auto-marked as read)
- Total unread message count and per-conversation unread counts
- Soft-delete messages per user (the other participant still sees them)
- Django signals for
message_sentandmessage_readevents - OpenAPI schema support via drf-spectacular
- Configurable recipient restrictions via
DIRECTMESSAGES_ALLOWED_RECIPIENTS - Self-messaging prevention (enforced at both model and service layer)
Requirements
| Package | Version |
|---|---|
| Python | >= 3.10 |
| Django | >= 4.2 |
| Django REST Framework | >= 3.14 |
Installation
Install with pip:
pip install drf-directmessages
Or with uv:
uv add drf-directmessages
Add the app to your Django project's INSTALLED_APPS:
INSTALLED_APPS = [
# ...
"rest_framework",
"directmessages",
]
Run migrations:
python manage.py migrate
Mount the URLs in your project's urls.py:
from django.urls import include, path
urlpatterns = [
# ...
path("api/dm/", include("directmessages.urls")),
]
Configuration
DIRECTMESSAGES_ALLOWED_RECIPIENTS
An optional list of user IDs that act as a messaging whitelist. When set:
- Users in the list can message anyone.
- Users not in the list can only message users in the list.
- If the setting is
Noneor an empty list (the default), there are no restrictions.
# settings.py
DIRECTMESSAGES_ALLOWED_RECIPIENTS = [1, 2, 3] # Only these users can be messaged by anyone
Default: None (no restrictions)
API Endpoints
All endpoints require authentication. The table below assumes you mounted the
URLs at api/dm/.
| Method | Path | Description |
|---|---|---|
| GET | api/dm/unread/ |
Get your user ID and total unread message count |
| GET | api/dm/conversations/ |
List your conversation partners (paginated) |
| GET | api/dm/conversations/unread/ |
Get unread counts per conversation partner |
| GET | api/dm/conversations/<id>/ |
List messages with user <id> (paginated, auto-read) |
| POST | api/dm/conversations/<id>/ |
Send a message to user <id> |
| DELETE | api/dm/messages/<id>/ |
Soft-delete a message for yourself |
| POST | api/dm/send/<id>/ |
Send a message to user <id> |
All list endpoints use cursor-based pagination with a page size of 50.
How-To Guides
How to send a message
You can send a message in two ways: via the dedicated send endpoint or from within a conversation.
Using the send endpoint:
curl -X POST http://localhost:8000/api/dm/send/2/ \
-H "Authorization: Token your-token-here" \
-H "Content-Type: application/json" \
-d '{"content": "Hello, world!"}'
import requests
response = requests.post(
"http://localhost:8000/api/dm/send/2/",
headers={"Authorization": "Token your-token-here"},
json={"content": "Hello, world!"},
)
print(response.status_code) # 201
print(response.json())
Response (201 Created):
{
"id": 1,
"sender": 1,
"recipient": 2,
"direction": "out",
"sent_at": "2025-06-06T12:00:00Z",
"read_at": null,
"content": "Hello, world!"
}
From within a conversation:
curl -X POST http://localhost:8000/api/dm/conversations/2/ \
-H "Authorization: Token your-token-here" \
-H "Content-Type: application/json" \
-d '{"content": "Hello again!"}'
This returns the full updated conversation (201 Created) rather than a single
message.
How to list your conversations
curl http://localhost:8000/api/dm/conversations/ \
-H "Authorization: Token your-token-here"
import requests
response = requests.get(
"http://localhost:8000/api/dm/conversations/",
headers={"Authorization": "Token your-token-here"},
)
print(response.json())
Response:
{
"next": "http://localhost:8000/api/dm/conversations/?cursor=cj0xJnA9Mg%3D%3D",
"previous": null,
"results": [
{
"id": 2,
"username": "jane",
"first_name": "Jane",
"last_name": "Doe"
}
]
}
How to get messages in a conversation
Retrieving messages automatically marks inbound messages as read.
curl http://localhost:8000/api/dm/conversations/2/ \
-H "Authorization: Token your-token-here"
import requests
response = requests.get(
"http://localhost:8000/api/dm/conversations/2/",
headers={"Authorization": "Token your-token-here"},
)
print(response.json())
Response:
{
"next": null,
"previous": null,
"results": [
{
"id": 1,
"sender": 2,
"recipient": 1,
"direction": "in",
"sent_at": "2025-06-06T12:00:00Z",
"read_at": "2025-06-06T12:01:00Z",
"content": "Hey there!"
},
{
"id": 2,
"sender": 1,
"recipient": 2,
"direction": "out",
"sent_at": "2025-06-06T12:02:00Z",
"read_at": null,
"content": "Hello, world!"
}
]
}
The direction field is relative to the authenticated user: "in" for messages
you received, "out" for messages you sent.
How to check unread message counts
Total unread count:
curl http://localhost:8000/api/dm/unread/ \
-H "Authorization: Token your-token-here"
import requests
response = requests.get(
"http://localhost:8000/api/dm/unread/",
headers={"Authorization": "Token your-token-here"},
)
print(response.json())
Response:
{
"id": 1,
"count": 5
}
Per-conversation unread counts:
curl http://localhost:8000/api/dm/conversations/unread/ \
-H "Authorization: Token your-token-here"
import requests
response = requests.get(
"http://localhost:8000/api/dm/conversations/unread/",
headers={"Authorization": "Token your-token-here"},
)
print(response.json())
Response:
[
{
"partner_id": 2,
"partner_username": "jane",
"unread_count": 3
},
{
"partner_id": 5,
"partner_username": "bob",
"unread_count": 1
}
]
How to delete a message
Deleting a message is a soft-delete that only hides it for the requesting user. The other participant still sees the message.
curl -X DELETE http://localhost:8000/api/dm/messages/42/ \
-H "Authorization: Token your-token-here"
import requests
response = requests.delete(
"http://localhost:8000/api/dm/messages/42/",
headers={"Authorization": "Token your-token-here"},
)
print(response.status_code) # 204
A successful deletion returns 204 No Content with an empty body. If the
message doesn't exist or doesn't belong to you, you'll get a 404.
How to use signals
drf-directmessages fires two Django signals that you can connect to for notifications, analytics, real-time updates, or any other side effects.
message_sent — fired when a new message is created.
message_read — fired when an unread message is marked as read.
Both signals provide from_user (the sender of the message) and to (the
recipient of the message). The signal sender is the Message instance.
# your_app/signals.py
from django.dispatch import receiver
from directmessages.signals import message_sent, message_read
@receiver(message_sent)
def on_message_sent(sender, **kwargs):
from_user = kwargs["from_user"]
to = kwargs["to"]
# sender is the Message instance
# e.g. send a push notification, update a feed, log analytics
print(f"Message {sender.id} sent from {from_user} to {to}")
@receiver(message_read)
def on_message_read(sender, **kwargs):
from_user = kwargs["from_user"]
to = kwargs["to"]
# e.g. notify the sender that their message was read
print(f"Message {sender.id} from {from_user} read by {to}")
How to restrict who can message whom
Use DIRECTMESSAGES_ALLOWED_RECIPIENTS in your Django settings to control
which users can receive messages from unrestricted users.
# settings.py
# Only users 1, 2, and 3 can receive messages from anyone.
# All other users can only message users in this list.
DIRECTMESSAGES_ALLOWED_RECIPIENTS = [1, 2, 3]
Common use cases:
- Support staff: list support agent IDs so customers can only message agents.
- Admin-only: restrict to admin users for a moderated messaging system.
- Unset (default): anyone can message anyone.
How to integrate with drf-spectacular
All views include @extend_schema decorators. To serve interactive API docs,
add drf-spectacular to your project:
pip install drf-spectacular
# settings.py
INSTALLED_APPS = [
# ...
"drf_spectacular",
]
REST_FRAMEWORK = {
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
SPECTACULAR_SETTINGS = {
"TITLE": "My API",
"VERSION": "1.0.0",
}
# urls.py
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
urlpatterns = [
# ...
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="docs"),
]
Signals Reference
| Signal | Sender | Keyword Arguments | Fired When |
|---|---|---|---|
message_sent |
Message instance |
from_user (User), to (User) |
A message is created |
message_read |
Message instance |
from_user (User), to (User) |
An unread message is marked as read |
Running Tests
Clone the repository and run the test suite:
git clone https://github.com/garrethcain/drf-directmessages.git
cd drf-directmessages
uv sync --group dev
uv run pytest
License
This project is licensed under the GNU Lesser General Public License v3.0 or later.
Release files for drf-directmessages 0.10.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| drf_directmessages-0.10.0.tar.gz | 59.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| drf_directmessages-0.10.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 75.0 kB
Release files / drf_directmessages-0.10.0.tar.gz
| Download URL | drf_directmessages-0.10.0.tar.gz |
|---|---|
| Size | 59.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
58ac266f133126b9a570f7660beb4f8c23da844632e5b73f715257739d716e08
|
|
BLAKE2b-256 checksum How to use checksums |
38f1b18a225b395e6af4a3084e8e42c4f5aacffe5e559293e835b6f6889068ab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / drf_directmessages-0.10.0-py3-none-any.whl
| Download URL | drf_directmessages-0.10.0-py3-none-any.whl |
|---|---|
| Size | 15.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
44846006c09b5fec4fb1dba296cd093d6c9654ba6ef16bab6f34d4ffa21a949d
|
|
BLAKE2b-256 checksum How to use checksums |
5f2bce6abdde10b368ab281752cb4b4c41ddd89e4d9ebe9bcf466ed6bf021f27
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|