Documentation Index
Fetch the complete documentation index at: https://mintlify.com/alphagov/notifications-api/llms.txt
Use this file to discover all available pages before exploring further.
Most of the API endpoints in this repo are for internal use. These are all defined within top-level folders under app/ and tend to have the structure app/<feature>/rest.py.
Overview
Public APIs are intended for use by services and are all located under app/v2/ to distinguish them from internal endpoints. Originally we did have a “v1” public API, where we tried to reuse/expose existing internal endpoints. The needs for public APIs are sufficiently different that we decided to separate them out. Any “v1” endpoints that remain are now purely internal and no longer exposed to services.
New APIs
Here are some pointers for how we write public API endpoints.
Each endpoint should be in its own file in a feature folder
Example: app/v2/inbound_sms/get_inbound_sms.py
This helps keep the file size manageable but does mean a bit more work to register each endpoint if we have many that are related. Note that internal endpoints are grouped differently: in large rest.py files.
Each group of endpoints should have an __init__.py file
Example:
from flask import Blueprint
from app.v2.errors import register_errors
v2_notification_blueprint = Blueprint(
"v2_notifications",
__name__,
url_prefix='/v2/notifications'
)
register_errors(v2_notification_blueprint)
Note that the error handling setup by register_errors (defined in app/v2/errors.py) for public API endpoints is different to that for internal endpoints (defined in app/errors.py).
Each endpoint should have an adapter in each API client
Example: Ruby Client adapter to get template by ID.
All our clients should fully support all of our public APIs.
Each adapter should be documented in each client (example). We should also document each public API endpoint in our generic API docs (example). Note that internal endpoints are not documented anywhere.
Each endpoint should specify the authentication it requires
This is done as part of registering the blueprint in app/__init__.py e.g.
v2_notification_blueprint.before_request(requires_auth)
application.register_blueprint(v2_notification_blueprint)
Public API Structure
Directory Organization
app/v2/
├── __init__.py
├── errors.py # Public API error handlers
├── notifications/
│ ├── __init__.py # Blueprint registration
│ ├── post_notifications.py # Send notification
│ ├── get_notifications.py # Get notification status
│ └── get_notification_by_id.py
├── template/
│ ├── __init__.py
│ ├── get_template.py
│ └── get_templates.py
└── inbound_sms/
├── __init__.py
└── get_inbound_sms.py
Authentication
Public API endpoints require JWT authentication:
from app.authentication.auth import requires_auth
v2_notification_blueprint.before_request(requires_auth)
The requires_auth decorator:
- Extracts JWT from Authorization header
- Validates signature against service API key
- Checks key is not expired
- Loads service and attaches to
g.service_id
Error Handling
Public APIs use consistent error responses:
from app.v2.errors import register_errors
register_errors(v2_notification_blueprint)
Error format:
{
"errors": [
{
"error": "ValidationError",
"message": "phone_number is a required property"
}
],
"status_code": 400
}
Vs internal API errors which may have different structure.
Best Practices
Use JSON schemas for request validation:
from app.schema_validation import validate
from app.v2.notifications.notification_schemas import post_sms_request_schema
@v2_notification_blueprint.route('/sms', methods=['POST'])
def post_sms_notification():
request_json = request.get_json()
validate(request_json, post_sms_request_schema)
# ...
Response Serialization
Return consistent JSON responses:
from flask import jsonify
def get_notification_by_id(notification_id):
notification = notifications_dao.get_notification_with_personalisation(
str(g.service_id),
notification_id,
key_type=None,
)
return jsonify(notification.serialize_for_v2()), 200
Versioning Strategy
- All public APIs under
/v2/ prefix
- Breaking changes require new version (e.g.,
/v3/)
- Non-breaking changes can be added to existing version
- Maintain backwards compatibility within version
Rate Limiting
Public APIs enforce rate limits:
API_RATE_LIMIT_ENABLED = True
DEFAULT_LIVE_SERVICE_RATE_LIMITS = {
EMAIL_TYPE: 250_000,
SMS_TYPE: 250_000,
LETTER_TYPE: 20_000,
INTERNATIONAL_SMS_TYPE: 100,
}
Rate limits checked in app/notifications/validators.py.
List endpoints support pagination:
@v2_notification_blueprint.route('', methods=['GET'])
def get_notifications():
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', API_PAGE_SIZE, type=int)
# API_PAGE_SIZE = 250
return jsonify(
notifications=[n.serialize_for_v2() for n in notifications],
links={
"current": url_for('.get_notifications', page=page, _external=True),
"next": url_for('.get_notifications', page=page+1, _external=True) if has_next else None,
}
)
Example Endpoint
Complete example of a public API endpoint:
# app/v2/notifications/post_notifications.py
from flask import request, jsonify
from app.v2.notifications import v2_notification_blueprint
from app.schema_validation import validate
from app.v2.notifications.notification_schemas import post_sms_request_schema
from app.notifications.process_notifications import (
persist_notification,
send_notification_to_queue,
)
@v2_notification_blueprint.route('/sms', methods=['POST'])
def post_sms_notification():
"""
Send an SMS notification
POST /v2/notifications/sms
{
"phone_number": "+447700900123",
"template_id": "...",
"personalisation": {...},
"reference": "..."
}
"""
request_json = request.get_json()
# 1. Validate request
validate(request_json, post_sms_request_schema)
# 2. Load service from auth
service = services_dao.dao_fetch_service_by_id(g.service_id)
# 3. Load template
template = templates_dao.dao_get_template_by_id_and_service_id(
template_id=request_json['template_id'],
service_id=service.id,
)
# 4. Validate and format recipient
recipient_data = validate_and_format_recipient(
send_to=request_json['phone_number'],
key_type=g.api_user.key_type,
service=service,
notification_type=SMS_TYPE,
)
# 5. Check rate limits
check_service_over_daily_message_limit(
service,
g.api_user.key_type,
notification_type=SMS_TYPE,
)
# 6. Persist notification
notification = persist_notification(
template_id=template.id,
template_version=template.version,
recipient=recipient_data,
service=service,
personalisation=request_json.get('personalisation'),
notification_type=SMS_TYPE,
api_key_id=g.api_user.id,
key_type=g.api_user.key_type,
client_reference=request_json.get('reference'),
)
# 7. Queue for delivery
send_notification_to_queue(notification, research_mode=False)
# 8. Return response
return jsonify(
id=str(notification.id),
reference=notification.client_reference,
uri=url_for(
'v2_notifications.get_notification_by_id',
notification_id=notification.id,
_external=True
),
template={
"id": str(template.id),
"version": template.version,
"uri": template.get_link(),
},
content={
"body": str(notification.content),
},
), 201
Testing Public APIs
Unit Tests
Test each endpoint in isolation:
def test_post_sms_notification_returns_201(client, sample_template):
data = {
'phone_number': '+447700900123',
'template_id': str(sample_template.id),
}
auth_header = create_authorization_header(
service_id=sample_template.service_id
)
response = client.post(
'/v2/notifications/sms',
data=json.dumps(data),
headers=[('Content-Type', 'application/json'), auth_header]
)
assert response.status_code == 201
json_resp = json.loads(response.get_data(as_text=True))
assert json_resp['id']
Integration Tests
Functional tests using API clients:
from notifications_python_client import NotificationsAPIClient
def test_send_sms_integration():
client = NotificationsAPIClient(
base_url=API_HOST_NAME,
api_key=test_api_key
)
response = client.send_sms_notification(
phone_number='+447700900123',
template_id=template_id,
)
assert response['id']
Client Libraries
Official clients that must support all public APIs:
Documentation
Public APIs must be documented in:
- Client README - Usage examples
- Client DOCUMENTATION.md - Full method reference
- Tech Docs - notifications-tech-docs API reference
- Admin UI - API documentation page
app/v2/ - Public API endpoints
app/v2/errors.py - Public API error handlers
app/authentication/auth.py - JWT authentication
app/schema_validation/ - Request validation schemas
app/notifications/process_notifications.py - Notification processing