Skip to main content

Overview

A Notification represents a single message sent through GOV.UK Notify. Each notification has a unique ID, tracks delivery status, and maintains an audit trail.

Notification Model

# From app/models.py - Notification model
class Notification:
    id: UUID                    # Unique notification ID
    
    # Recipient
    to: str                     # Email address or phone number
    normalised_to: str          # Normalized for searching
    
    # Template & Content
    template_id: UUID
    template_version: int
    notification_type: str      # "email", "sms", or "letter"
    
    # Sending Context
    service_id: UUID
    api_key_id: UUID           # Which API key was used
    key_type: str              # "normal", "team", or "test"
    
    # Status & Timing
    status: str                # Current delivery status
    created_at: datetime       # When notification was created
    sent_at: datetime          # When sent to provider
    updated_at: datetime       # Last status update
    
    # Billing
    billable_units: int        # SMS fragments or letter pages
    international: bool        # International SMS/letter?
    phone_prefix: str          # Country code for SMS
    rate_multiplier: decimal   # Cost multiplier
    postage: str               # Letter postage class
    
    # Tracking
    reference: str             # Provider reference
    client_reference: str      # Your custom reference
    
    # Content
    personalisation: dict      # Encrypted placeholder values
    
    # Optional
    job_id: UUID               # If sent via bulk job
    created_by_id: UUID        # If sent via UI
    reply_to_text: str         # Reply-to address used

Notification Lifecycle

1

Created

Notification record created in database with status = "created"
2

Sending

Queued for delivery to provider. Status changes to "sending"
3

Provider Processing

Sent to email/SMS provider. Status may be:
  • "sent" - Accepted by provider
  • "pending" - Awaiting delivery
4

Final Status

Terminal status reached:
  • "delivered" - Successfully delivered
  • "permanent-failure" - Cannot be delivered
  • "temporary-failure" - Delivery failed, may retry
  • "technical-failure" - System error

Notification Statuses

All Status Types

# From app/constants.py - All possible statuses
NOTIFICATION_STATUS_TYPES = [
    "cancelled",              # Notification cancelled before sending
    "created",                # Initial state
    "sending",                # Queued for provider
    "sent",                   # Accepted by provider
    "delivered",              # Confirmed delivered
    "pending",                # Awaiting delivery
    "failed",                 # Generic failure (deprecated)
    "technical-failure",      # System/technical error
    "temporary-failure",      # Temporary delivery issue
    "permanent-failure",      # Permanent delivery failure
    "pending-virus-check",    # Letter PDF being scanned
    "validation-failed",      # Invalid recipient/content
    "virus-scan-failed",      # Letter PDF contains virus
    "returned-letter",        # Letter returned by post
]

Status Categories

NOTIFICATION_STATUS_SUCCESS = [
    "sent",      # Sent internationally (SMS) or accepted (letters)
    "delivered"  # Confirmed delivered
]

Channel-Specific Statuses

# Email notification flow:
created → sending → delivered

                  temporary-failure (inbox full, server down)

                  permanent-failure (address doesn't exist)

# Status meanings for email:
{
    "created": "Sending",
    "sending": "Sending",
    "delivered": "Delivered",
    "temporary-failure": "Inbox not accepting messages right now",
    "permanent-failure": "Email address doesn't exist",
    "technical-failure": "Technical failure"
}
# SMS notification flow:
created → sending → sent (international)

                  delivered (UK)

                  temporary-failure (phone off, no signal)

                  permanent-failure (invalid number)

# Status meanings for SMS:
{
    "created": "Sending",
    "sending": "Sending",
    "sent": "Sent internationally",
    "delivered": "Delivered",
    "temporary-failure": "Phone not accepting messages right now",
    "permanent-failure": "Phone number doesn't exist",
    "technical-failure": "Technical failure"
}
# Letter notification flow:
created → pending-virus-check → sending → delivered
          ↓                      ↓
          virus-scan-failed      returned-letter

          validation-failed
          technical-failure

# Status meanings for letters:
{
    "created": "Accepted",
    "sending": "Accepted",
    "delivered": "Received",  # Received by print provider
    "returned-letter": "Returned",
    "technical-failure": "Technical failure",
    "permanent-failure": "Permanent failure"
}

# Letters that were never sent to print:
NOTIFICATION_STATUS_TYPES_LETTERS_NEVER_SENT = [
    "cancelled",
    "technical-failure",
    "validation-failed",
    "virus-scan-failed",
]

Key Types

Notifications track which API key was used:
# From app/constants.py
KEY_TYPE_NORMAL = "normal"    # Live API key - sends real notifications
KEY_TYPE_TEAM = "team"        # Team key - sends only to team/guest list
KEY_TYPE_TEST = "test"        # Test key - doesn't actually send
  • Sends to anyone
  • Notifications are billable
  • Count toward daily limits
  • Require live (non-restricted) service

Billing

Billable Statuses

# From app/constants.py - When notifications count toward billing
NOTIFICATION_STATUS_TYPES_BILLABLE = [
    "sending",
    "sent",
    "delivered",
    "pending",
    "failed",
    "temporary-failure",
    "permanent-failure",
    "returned-letter",
]

# SMS-specific billable statuses (excludes some letter-only statuses)
NOTIFICATION_STATUS_TYPES_BILLABLE_SMS = [
    "sending",
    "sent",
    "delivered",
    "pending",
    "temporary-failure",
    "permanent-failure",
]

# Letter-specific billable statuses
NOTIFICATION_STATUS_TYPES_BILLABLE_FOR_LETTERS = [
    "sending",
    "delivered",
    "returned-letter",
]

Billable Units

Email

Always 0 billable units - emails are free

SMS

1 unit per 160 characters (including personalization)918 chars = 6 units

Letter

1 unit per sheet of paper (double-sided page)Includes postage cost

International Costs

# International notifications have rate multipliers:
international: bool          # Is this international?
phone_prefix: str           # Country code (e.g., "+33", "+1")
rate_multiplier: decimal    # Cost multiplier vs UK rate

# Example: 
# UK SMS: rate_multiplier = 1.0
# France SMS: rate_multiplier = 1.5 (costs 1.5x UK rate)

Client References

Track notifications with your own reference:
# When sending:
client_reference = "ORDER-12345"

# Query notifications by your reference:
GET /v2/notifications?reference=ORDER-12345

# Stored in notification:
notification.client_reference = "ORDER-12345"
Client references are indexed for fast searching. Maximum 255 characters.

Retrieving Notifications

Get Single Notification

GET /v2/notifications/{notification_id}
Response:
{
  "id": "740e5834-3a29-46b4-9a6f-16142fde533a",
  "reference": null,
  "email_address": "[email protected]",
  "phone_number": null,
  "line_1": null,
  "line_2": null,
  "line_3": null,
  "line_4": null,
  "line_5": null,
  "line_6": null,
  "postcode": null,
  "type": "email",
  "status": "delivered",
  "template": {
    "id": "f33517ff-2a88-4f6e-b855-c550268ce08a",
    "version": 1,
    "uri": "https://api.notifications.service.gov.uk/v2/template/f33517ff-2a88-4f6e-b855-c550268ce08a"
  },
  "body": "Hello Sarah,\n\nYour verification code is 123456.",
  "subject": "Your verification code",
  "created_at": "2026-03-03T10:30:00.000000Z",
  "created_by_name": null,
  "sent_at": "2026-03-03T10:30:05.123456Z",
  "completed_at": "2026-03-03T10:30:12.654321Z"
}

List Notifications

GET /v2/notifications?
  template_type=email&
  status=delivered&
  reference=ORDER-12345&
  older_than=notification-id
Use older_than for cursor-based pagination rather than page numbers for better performance.

Notification History

Notifications older than 7 days move to notification_history table:
# From app/models.py - Two tables with same structure:
Notification         # Last 7 days (fast queries)
NotificationHistory  # Archived (slower queries)

# View combines both:
NotificationAllTimeView  # Queries both tables
Querying notifications_all_time_view is slower than querying recent notifications directly.

Best Practices

  • Poll for status updates every 30 seconds minimum
  • Don’t poll faster than every 10 seconds
  • Use callbacks instead of polling when possible
  • Cache status locally; don’t query repeatedly
  • Always include client references for tracking
  • Make references unique and meaningful
  • Use format: {entity}-{id} (e.g., ORDER-12345)
  • Index references in your database
  • permanent-failure: Don’t retry, address is invalid
  • temporary-failure: Safe to retry after delay
  • technical-failure: Contact Notify support
  • validation-failed: Fix your request format
  • Query notifications in date ranges
  • Use pagination for large result sets
  • Filter by status to reduce response size
  • Archive old notifications in your system

Build docs developers (and LLMs) love