Skip to main content

Possinote Python SDK

Official Python SDK for the PossiNote API - Send SMS, emails, and schedule messages with ease.

Installation

From PyPI

pip install possinote

From Source

git clone https://github.com/possitech/possinote-python.git
cd possinote-python
pip install -e .

Quick Start

from possinote import Possinote

# Initialize the client with your API key
client = Possinote(api_key='your_api_key_here')

# Send a single SMS
response = client.sms.send(
    to='+233244123456',
    message='Hello from Possinote!',
    sender_id='YourSenderID'
)

# Send a single email
response = client.email.send(
    recipient='user@example.com',
    subject='Welcome to Possinote',
    content='<h1>Hello!</h1><p>Welcome to our platform.</p>',
    sender_name='Your Company'
)

API Reference

Authentication

All API requests require authentication using your API key:

client = Possinote(api_key='your_api_key_here')

SMS Operations

Send Single SMS

response = client.sms.send(
    to='+233244123456',
    message='Your message here',
    sender_id='YourSenderID'
)

# Response
{
    "success": True,
    "data": {
        "message_id": "msg_123456789",
        "to": "+233244123456",
        "status": "queued",
        "cost": 1.0,
        "created_at": "2025-08-11T11:30:00Z"
    }
}

Send Bulk SMS

response = client.sms.send_bulk(
    sender_id='YourSenderID',
    messages=[
        {'to': '+233244123456', 'message': 'Message 1'},
        {'to': '+233244123457', 'message': 'Message 2'}
    ]
)

# Response
{
    "success": True,
    "data": {
        "batch_id": "batch_123456789",
        "total_messages": 2,
        "successful": 2,
        "failed": 0,
        "total_cost": 2.0,
        "messages": [
            {"message_id": "msg_1", "to": "+233244123456", "status": "queued"},
            {"message_id": "msg_2", "to": "+233244123457", "status": "queued"}
        ]
    }
}

Schedule Single SMS

response = client.sms.schedule(
    recipient='+233244123456',
    message='Scheduled message',
    sender_id='YourSenderID',
    scheduled_at='2025-08-11T12:00:00Z'
)

# Response
{
    "success": True,
    "data": {
        "id": "schedule_123456789",
        "recipient": "+233244123456",
        "message": "Scheduled message",
        "scheduled_at": "2025-08-11T12:00:00Z",
        "status": "pending",
        "cost": "1.0"
    }
}

Schedule Bulk SMS

response = client.sms.schedule_bulk(
    sender_id='YourSenderID',
    messages=[
        {'recipient': '+233244123456', 'message': 'Scheduled message 1'},
        {'recipient': '+233244123457', 'message': 'Scheduled message 2'}
    ],
    scheduled_at='2025-08-11T12:00:00Z'
)

# Response
{
    "success": True,
    "data": {
        "batch_id": "batch_123456789",
        "scheduled_count": 2,
        "total_cost": 2.0,
        "scheduled_at": "2025-08-11T12:00:00Z",
        "messages": [
            {"id": "schedule_1", "recipient": "+233244123456", "status": "pending"},
            {"id": "schedule_2", "recipient": "+233244123457", "status": "pending"}
        ]
    }
}

Email Operations

Send Single Email

response = client.email.send(
    recipient='user@example.com',
    subject='Welcome Email',
    content='<h1>Welcome!</h1><p>Thank you for joining us.</p>',
    sender_name='Your Company'
)

# Response
{
    "success": True,
    "message": "Email queued for delivery",
    "recipient": "user@example.com",
    "message_id": "email_123456789"
}

Send Bulk Email

response = client.email.send_bulk(
    subject='Newsletter',
    content='<h1>Newsletter</h1><p>This is our monthly newsletter.</p>',
    recipients=['user1@example.com', 'user2@example.com'],
    sender_name='Your Company'
)

# Response
{
    "success": True,
    "message": "Bulk emails queued for delivery",
    "queued_count": 2,
    "total_count": 2,
    "batch_id": "batch_123456789",
    "emails": [
        {"message_id": "email_1", "recipient": "user1@example.com", "status": "queued"},
        {"message_id": "email_2", "recipient": "user2@example.com", "status": "queued"}
    ]
}

Scheduling Operations

Schedule Single Email

response = client.scheduling.schedule_email(
    recipient='user@example.com',
    subject='Scheduled Email',
    content='<h1>Scheduled Content</h1>',
    scheduled_at='2025-08-11T12:00:00Z',
    sender_name='Your Company'
)

# Response
{
    "success": True,
    "data": {
        "id": "email_schedule_123456789",
        "recipient": "user@example.com",
        "subject": "Scheduled Email",
        "scheduled_at": "2025-08-11T12:00:00Z",
        "status": "pending",
        "cost": "1.0"
    }
}

Schedule Bulk Individual Emails

response = client.scheduling.schedule_multiple_emails([
    {
        'recipient': 'user1@example.com',
        'subject': 'Personalized Email 1',
        'content': '<h1>Hello User 1!</h1>',
        'scheduled_at': '2025-08-11T12:00:00Z',
        'sender_name': 'Your Company'
    },
    {
        'recipient': 'user2@example.com',
        'subject': 'Personalized Email 2',
        'content': '<h1>Hello User 2!</h1>',
        'scheduled_at': '2025-08-11T12:00:00Z',
        'sender_name': 'Your Company'
    }
])

# Response
{
    "success": True,
    "data": {
        "batch_id": "batch_123456789",
        "total_scheduled": 2,
        "total_cost": 2.0,
        "scheduled_emails": [
            {"id": "email_1", "recipient": "user1@example.com", "status": "pending"},
            {"id": "email_2", "recipient": "user2@example.com", "status": "pending"}
        ]
    }
}

Framework Integration

Django Integration

1. Install the Package

pip install possinote

2. Configure in Settings

# settings.py
import os
from possinote import Possinote

# Initialize the client
POSSINOTE_CLIENT = Possinote(api_key=os.environ.get('POSSINOTE_API_KEY'))

# Or use Django settings
POSSINOTE_CLIENT = Possinote(api_key=os.environ.get('POSSINOTE_API_KEY', 'your_api_key_here'))

3. Use in Views

# views.py
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
import json
from possinote.exceptions import (
    AuthenticationError, PaymentRequiredError, 
    RateLimitError, ValidationError, APIError
)
from .models import User
from django.conf import settings

@csrf_exempt
@require_http_methods(["POST"])
def send_sms(request):
    try:
        data = json.loads(request.body)
        response = settings.POSSINOTE_CLIENT.sms.send(
            to=data['phone_number'],
            message=data['message'],
            sender_id='YourBrand'
        )
        
        if response['success']:
            return JsonResponse({
                'message': 'SMS sent successfully',
                'data': response['data']
            })
        else:
            return JsonResponse({
                'error': 'Failed to send SMS'
            }, status=400)
            
    except AuthenticationError:
        return JsonResponse({'error': 'Authentication failed'}, status=401)
    except PaymentRequiredError:
        return JsonResponse({'error': 'Insufficient credits'}, status=402)
    except ValidationError as e:
        return JsonResponse({'error': str(e)}, status=400)
    except Exception as e:
        return JsonResponse({'error': 'An error occurred'}, status=500)

@csrf_exempt
@require_http_methods(["POST"])
def send_bulk_sms(request):
    try:
        data = json.loads(request.body)
        response = settings.POSSINOTE_CLIENT.sms.send_bulk(
            sender_id='YourBrand',
            messages=data['messages']  # List of {'to': phone, 'message': text}
        )
        
        return JsonResponse({
            'message': 'Bulk SMS sent',
            'data': response['data']
        })
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=500)

@csrf_exempt
@require_http_methods(["POST"])
def send_email(request):
    try:
        data = json.loads(request.body)
        response = settings.POSSINOTE_CLIENT.email.send(
            recipient=data['email'],
            subject=data['subject'],
            content=data['content'],
            sender_name='Your Company'
        )
        
        return JsonResponse({
            'message': 'Email sent successfully',
            'data': response
        })
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=500)

4. Use in Services

# services.py
from django.conf import settings
from django.template.loader import render_to_string
from django.urls import reverse
from .models import User, Appointment
import logging

logger = logging.getLogger(__name__)

class NotificationService:
    def __init__(self):
        self.client = settings.POSSINOTE_CLIENT
    
    def send_welcome_sms(self, user: User):
        """Send welcome SMS to new user"""
        try:
            response = self.client.sms.send(
                to=user.phone_number,
                message=f"Welcome {user.first_name}! Your account has been created successfully.",
                sender_id='YourBrand'
            )
            logger.info(f"Welcome SMS sent to {user.email}: {response}")
            return response
        except Exception as e:
            logger.error(f"Failed to send welcome SMS to {user.email}: {e}")
            raise
    
    def send_password_reset_email(self, user: User, reset_token: str):
        """Send password reset email"""
        try:
            reset_url = f"https://yourdomain.com{reverse('password_reset_confirm', kwargs={'token': reset_token})}"
            
            html_content = render_to_string('emails/password_reset.html', {
                'user': user,
                'reset_url': reset_url
            })
            
            response = self.client.email.send(
                recipient=user.email,
                subject='Password Reset Request',
                content=html_content,
                sender_name='Your Company'
            )
            logger.info(f"Password reset email sent to {user.email}")
            return response
        except Exception as e:
            logger.error(f"Failed to send password reset email to {user.email}: {e}")
            raise
    
    def send_bulk_newsletter(self, users: list, newsletter_content: dict):
        """Send bulk newsletter SMS"""
        try:
            messages = [
                {
                    'to': user.phone_number,
                    'message': f"Newsletter: {newsletter_content['title']}"
                }
                for user in users
            ]
            
            response = self.client.sms.send_bulk(
                sender_id='YourBrand',
                messages=messages
            )
            logger.info(f"Bulk newsletter sent to {len(users)} users")
            return response
        except Exception as e:
            logger.error(f"Failed to send bulk newsletter: {e}")
            raise
    
    def schedule_reminder_email(self, appointment: Appointment):
        """Schedule appointment reminder email"""
        try:
            html_content = render_to_string('emails/appointment_reminder.html', {
                'appointment': appointment,
                'user': appointment.user
            })
            
            response = self.client.scheduling.schedule_email(
                recipient=appointment.user.email,
                subject='Appointment Reminder',
                content=html_content,
                scheduled_at=appointment.datetime.isoformat(),
                sender_name='Your Company'
            )
            logger.info(f"Appointment reminder scheduled for {appointment.user.email}")
            return response
        except Exception as e:
            logger.error(f"Failed to schedule appointment reminder: {e}")
            raise

5. Use in Celery Tasks

# tasks.py
from celery import shared_task
from django.conf import settings
from .models import User, Appointment
from .services import NotificationService

@shared_task
def send_welcome_sms_task(user_id: int):
    """Celery task to send welcome SMS"""
    try:
        user = User.objects.get(id=user_id)
        service = NotificationService()
        service.send_welcome_sms(user)
    except User.DoesNotExist:
        print(f"User with id {user_id} not found")
    except Exception as e:
        print(f"Failed to send welcome SMS: {e}")

@shared_task
def send_password_reset_email_task(user_id: int, reset_token: str):
    """Celery task to send password reset email"""
    try:
        user = User.objects.get(id=user_id)
        service = NotificationService()
        service.send_password_reset_email(user, reset_token)
    except User.DoesNotExist:
        print(f"User with id {user_id} not found")
    except Exception as e:
        print(f"Failed to send password reset email: {e}")

@shared_task
def send_bulk_newsletter_task(user_ids: list, newsletter_content: dict):
    """Celery task to send bulk newsletter"""
    try:
        users = User.objects.filter(id__in=user_ids)
        service = NotificationService()
        service.send_bulk_newsletter(users, newsletter_content)
    except Exception as e:
        print(f"Failed to send bulk newsletter: {e}")

@shared_task
def schedule_appointment_reminder_task(appointment_id: int):
    """Celery task to schedule appointment reminder"""
    try:
        appointment = Appointment.objects.get(id=appointment_id)
        service = NotificationService()
        service.schedule_reminder_email(appointment)
    except Appointment.DoesNotExist:
        print(f"Appointment with id {appointment_id} not found")
    except Exception as e:
        print(f"Failed to schedule appointment reminder: {e}")

# Usage in views
from .tasks import send_welcome_sms_task, send_password_reset_email_task

def register_user(request):
    # ... user registration logic ...
    user = User.objects.create(...)
    
    # Send welcome SMS asynchronously
    send_welcome_sms_task.delay(user.id)
    
    return JsonResponse({'message': 'User registered successfully'})

def request_password_reset(request):
    # ... password reset logic ...
    token = generate_reset_token()
    
    # Send password reset email asynchronously
    send_password_reset_email_task.delay(user.id, token)
    
    return JsonResponse({'message': 'Password reset email sent'})

6. URL Configuration

# urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('api/send-sms/', views.send_sms, name='send_sms'),
    path('api/send-bulk-sms/', views.send_bulk_sms, name='send_bulk_sms'),
    path('api/send-email/', views.send_email, name='send_email'),
]

7. Environment Configuration

# .env
POSSINOTE_API_KEY=your_api_key_here
# settings.py
from decouple import config

POSSINOTE_CLIENT = Possinote(api_key=config('POSSINOTE_API_KEY'))

8. Email Templates

<!-- templates/emails/password_reset.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Password Reset</title>
</head>
<body>
    <h1>Password Reset Request</h1>
    <p>Hello {{ user.first_name }},</p>
    <p>You requested a password reset. Click the link below to reset your password:</p>
    <a href="{{ reset_url }}">Reset Password</a>
    <p>This link will expire in 1 hour.</p>
</body>
</html>
<!-- templates/emails/appointment_reminder.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Appointment Reminder</title>
</head>
<body>
    <h1>Appointment Reminder</h1>
    <p>Hello {{ user.first_name }},</p>
    <p>This is a reminder for your appointment:</p>
    <ul>
        <li><strong>Date:</strong> {{ appointment.datetime|date:"F d, Y" }}</li>
        <li><strong>Time:</strong> {{ appointment.datetime|time:"g:i A" }}</li>
        <li><strong>Location:</strong> {{ appointment.location }}</li>
    </ul>
</body>
</html>

Error Handling

The SDK provides specific exception classes for different error types:

from possinote.exceptions import (
    PossinoteError,
    AuthenticationError,
    PaymentRequiredError,
    RateLimitError,
    ValidationError,
    APIError
)

try:
    response = client.sms.send(
        to='+233244123456',
        message='Hello',
        sender_id='SenderID'
    )
except AuthenticationError as e:
    print(f"Authentication failed: {e}")
except PaymentRequiredError as e:
    print(f"Payment required: {e}")
except RateLimitError as e:
    print(f"Rate limit exceeded: {e}")
except ValidationError as e:
    print(f"Validation error: {e}")
except APIError as e:
    print(f"API error: {e}")

Error Types

  • AuthenticationError - Invalid API key (401)
  • PaymentRequiredError - Insufficient credits (402)
  • RateLimitError - Rate limit exceeded (429)
  • ValidationError - Invalid request data (400)
  • APIError - Other API errors

Configuration

Base URL

The SDK uses the production API by default. For testing, you can modify the base URL:

# In possinote/client.py
BASE_URL = 'https://notifyapi.possitech.net/api/v1'

Timeout Settings

HTTP requests use default timeout settings. You can customize them in the client:

# The SDK uses requests defaults
# You can modify timeout settings in possinote/client.py

Requirements

  • Python >= 3.7
  • requests library
  • typing (included in Python 3.5+)

Development

Setup Development Environment

# Clone the repository
git clone https://github.com/possitech/possinote-python.git
cd possinote-python

# Create virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install dependencies
pip install -r requirements.txt
pip install -r requirements-dev.txt

# Install in development mode
pip install -e .

Running Tests

# Run all tests
pytest

# Run with coverage
pytest --cov=possinote

# Run specific test file
pytest tests/test_sms.py

Code Quality

# Format code
black possinote/

# Lint code
flake8 possinote/

# Type checking
mypy possinote/

Building and Publishing

# Build package
python setup.py sdist bdist_wheel

# Test upload to TestPyPI
twine upload --repository testpypi dist/*

# Upload to PyPI
twine upload dist/*

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/possitech/possinote-python. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The package is available as open source under the terms of the MIT License.

Support

For support, email support@possitech.net or visit our documentation.

Changelog

1.0.0

  • Initial release
  • SMS sending and scheduling
  • Email sending and scheduling
  • Comprehensive error handling
  • Full API coverage

Metadata

Release files for possinote 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for possinote 1.0.0
File Size Uploaded Provenance Actions
possinote-1.0.0.tar.gz 14.1 kB Without provenance Details for possinote-1.0.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for possinote 1.0.0
File Interpreter ABI Platform Actions
possinote-1.0.0-py3-none-any.whl Python 3 none any Details for possinote-1.0.0-py3-none-any.whl

Total release size: 25.1 kB

Release files / possinote-1.0.0.tar.gz

Download URL possinote-1.0.0.tar.gz
Size 14.1 kB
Tags Source
SHA-256 checksum
How to use checksums
14aa6473a1f647f899ba048a0b2fd668c030a81ba1469fa172ed023c8bfefb59
BLAKE2b-256 checksum
How to use checksums
6a66d412403703a24f3c37e9c91681168466c4378d7f516f4ae1cc0008bcd37a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.3

Release files / possinote-1.0.0-py3-none-any.whl

Download URL possinote-1.0.0-py3-none-any.whl
Size 11.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1ac7feb8857dd77a08015e4b7c40ac14e3f56d783e43ebf25931ff1dd002cede
BLAKE2b-256 checksum
How to use checksums
f7882806ce7760194956e1266d0d5ab352728d0eac2a38ac5f60d9e3f8b00800
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.3

Security for possinote 1.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page