Skip to main content

Overview

GOV.UK Notify uses JSON Web Tokens (JWT) for API authentication. Each request must include a JWT signed with your API key’s secret.

API Key Types

Notify provides three types of API keys for different use cases:

Live Key

Normal keys send real notifications to anyone and count toward billing.

Team Key

Team keys send only to team members and the guest list - useful for testing.

Test Key

Test keys simulate sending without actually delivering messages.

Key Type Details

# From app/constants.py
KEY_TYPE_NORMAL = "normal"  # Live API key
KEY_TYPE_TEAM = "team"      # Team/guest list only
KEY_TYPE_TEST = "test"      # Simulated sending
Production use
  • Sends to any recipient
  • Notifications are billable
  • Counts toward daily sending limits
  • Requires service to be live (not restricted)
  • Use for: Production applications
# Key name example:
my-service-live-key

API Key Model

# From app/models.py - ApiKey model
class ApiKey:
    id: UUID                    # Unique key ID
    name: str                   # Descriptive name (e.g., "Production key")
    service_id: UUID            # Parent service
    key_type: str               # "normal", "team", or "test"
    
    secret: str                 # Encrypted secret (for signing JWTs)
    
    created_at: datetime
    created_by_id: UUID         # User who created the key
    
    expiry_date: datetime       # When key expires (null = never)
    updated_at: datetime

Key Naming

Key names must be unique within a service. Choose descriptive names that indicate purpose and environment.
# Good key names:
"production-api-key"
"staging-integration-key"
"ci-automated-tests"
"mobile-app-v2"

# Avoid:
"key1"
"test"
"asdf"

JWT Authentication

How It Works

1

Create JWT Token

Your application creates a JWT containing:
  • iss (issuer): Your service ID
  • iat (issued at): Current timestamp
Sign it with your API key secret using HS256 algorithm.
2

Include in Request

Send the JWT in the Authorization header:
Authorization: Bearer {your-jwt-token}
3

Notify Validates

Notify:
  • Extracts the service ID from iss claim
  • Finds the API key
  • Verifies the JWT signature
  • Checks the key is not expired
  • Validates timestamp is within 30 seconds
4

Request Processed

If valid, your request is processed. If invalid, you receive a 403 error.

Token Requirements

# From app/authentication/auth.py

# JWT must include:
{
  "iss": "service-id-uuid",      # Your service ID (issuer)
  "iat": 1709467800              # Unix timestamp (issued at)
}

# Signed with:
- Algorithm: HS256
- Secret: Your API key secret

# Validation rules:
- Token must be signed with valid API key secret
- iat (issued at) must be within 30 seconds of current time
- Service must exist and be active
- API key must not be expired
Clock Synchronization RequiredYour system clock must be accurate within 30 seconds. Notify rejects tokens with iat timestamps more than 30 seconds in the past or future.
# Error message if clock is wrong:
"Error: Your system clock must be accurate to within 30 seconds"

Creating a JWT

Using the official Python client:
from notifications_python_client.notifications import NotificationsAPIClient

# Client handles JWT creation automatically
notifications_client = NotificationsAPIClient(api_key)

# JWT is created and included in Authorization header for you
notifications_client.send_email_notification(
    email_address="[email protected]",
    template_id="template-id",
)
Manual JWT creation (not recommended):
import jwt
import time
import uuid

# Your API key details
service_id = "your-service-id-uuid"
api_key_secret = "your-api-key-secret"

# Create JWT
token = jwt.encode(
    {
        "iss": service_id,
        "iat": int(time.time())
    },
    api_key_secret,
    algorithm="HS256"
)

# Use in request
headers = {
    "Authorization": f"Bearer {token}",
    "Content-Type": "application/json"
}

Authentication Errors

Common Error Messages

# From app/authentication/auth.py - AuthError responses

# 401 Unauthorized
"Unauthorized: authentication token must be provided"
# Cause: No Authorization header

"Unauthorized: authentication bearer scheme must be used"  
# Cause: Authorization header doesn't start with "Bearer "

# 403 Forbidden
"Invalid token: service id is not the right data type"
# Cause: iss claim is not a valid UUID

"Invalid token: service not found"
# Cause: Service ID in iss claim doesn't exist

"Invalid token: service has no API keys"
# Cause: Service exists but has no API keys

"Invalid token: service is archived"
# Cause: Service has been deactivated

"Invalid token: API key not found"
# Cause: JWT signature doesn't match any API key

"Invalid token: API key revoked"
# Cause: API key has an expiry_date set

"Error: Your system clock must be accurate to within 30 seconds"
# Cause: iat timestamp is too old or too far in future

"Invalid token: algorithm used is not HS256"
# Cause: JWT signed with wrong algorithm

"Invalid token: iss field not provided"
# Cause: JWT missing iss claim

Error Response Format

{
  "status_code": 403,
  "errors": [
    {
      "error": "AuthError",
      "message": "Invalid token: API key not found"
    }
  ]
}

API Key Management

Creating API Keys

API keys are created through the Notify web interface:
  1. Log into your service
  2. Go to Settings → API keys
  3. Click “Create an API key”
  4. Choose key type and name
  5. Copy the key immediately (only shown once)
API key secrets are only displayed once when created. Store them securely - you cannot retrieve them later.

Revoking API Keys

To revoke an API key:
# Set expiry_date to revoke
api_key.expiry_date = datetime.utcnow()

# Authentication will fail:
# "Invalid token: API key revoked"
Revoked keys:
  • Cannot be used for new requests
  • Return 403 Forbidden errors
  • Are preserved for audit trail
  • Cannot be un-revoked

Key Rotation

Best practice for rotating API keys:
1

Create New Key

Generate a new API key with a different name
2

Update Applications

Deploy new key to all applications (gradual rollout recommended)
3

Monitor

Verify new key is working correctly
4

Revoke Old Key

Once migration complete, revoke the old API key

Security Model

API Key Storage

# From app/models.py
class ApiKey:
    _secret: str  # Stored encrypted in database
    
    @property
    def secret(self):
        # Decrypted when accessed
        return signing.decode(self._secret)
    
    @secret.setter
    def secret(self, secret):
        # Encrypted before storage  
        self._secret = signing.encode(str(secret))
API key secrets are:
  • Encrypted at rest in the database
  • Only decrypted when validating JWTs
  • Never returned in API responses
  • Only shown once when created

Request Tracking

Every authenticated request is logged:
# From app/authentication/auth.py
current_app.logger.info(
    "API authorised for service %(service_id)s with api key %(api_key_id)s, "
    "using issuer %(issuer)s for URL: %(url)s",
    extra={
        "service_id": service_id,
        "api_key_id": api_key.id,
        "issuer": request.headers.get("User-Agent"),
        "url": request.base_url,
    }
)
This provides:
  • Audit trail of API usage
  • Which key was used for each request
  • Source of requests (User-Agent)
  • Endpoint accessed

Best Practices

  • Use separate keys for each environment (dev, staging, prod)
  • Name keys descriptively
  • Rotate keys every 90 days
  • Revoke keys immediately if compromised
  • Never commit keys to version control
  • Store keys in environment variables
  • Use secrets management (AWS Secrets Manager, HashiCorp Vault)
  • Encrypt keys at rest in your database
  • Limit access to keys to authorized personnel only
  • Use test keys for automated tests
  • Use team keys for integration testing
  • Never use live keys in development
  • Include key type in key name
  • Monitor for authentication failures
  • Alert on unusual API usage patterns
  • Track which keys are being used
  • Review API key audit logs regularly

Example: Complete Request

import jwt
import time
import requests

# Your credentials
SERVICE_ID = "your-service-id-uuid"
API_KEY_SECRET = "your-api-key-secret"

# Create JWT
def create_jwt_token(service_id, secret):
    headers = {"typ": "JWT", "alg": "HS256"}
    
    claims = {
        "iss": service_id,
        "iat": int(time.time())
    }
    
    return jwt.encode(claims, secret, algorithm="HS256", headers=headers)

# Make authenticated request
def send_notification():
    token = create_jwt_token(SERVICE_ID, API_KEY_SECRET)
    
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }
    
    data = {
        "email_address": "[email protected]",
        "template_id": "template-id-uuid",
        "personalisation": {
            "name": "John"
        }
    }
    
    response = requests.post(
        "https://api.notifications.service.gov.uk/v2/notifications/email",
        json=data,
        headers=headers
    )
    
    return response.json()

# Send
result = send_notification()
print(result)

Build docs developers (and LLMs) love