Skip to main content

uktrade-hmrc-licensing-management-client

API client for HMRC internal licensing management API

Send licence to HMRC example

import requests.exceptions
from django.core.cache import cache

from hmrc_licensing_management import utils
from hmrc_licensing_management.api import Client, Licence, APIResponse, Result


class CachedClient(Client):
    """Example client used that stores the access token for as long as it's valid."""

    session_key = "CDS_CLIENT_GET_ACCESS_TOKEN_KEY"

    def get_access_token(self, *, scope: str | None = None) -> utils.AccessToken:
        # Look for access token in cache
        token = cache.get(self.session_key, None)

        if not token:
            # Fetch a new token
            token = super().get_access_token(scope=scope)

            # Store token in cache for as long as it's valid
            cache.add(self.session_key, token, timeout=token["expires_in"])

        return token


def example_implementation():
    # 1. Create client (use example class above to cache access token)
    client = CachedClient(
        # Production or sandbox hmrc API url
        hmrc_api_base_url="https://test-api.service.hmrc.gov.uk",
        # Client id and secret retrieved from HMRC API application
        hmrc_api_client_id="test-client-id",
        hmrc_api_client_secret="test-client-secret",
        # TOTP secret required for a HMRC "Privileged application"
        hmrc_api_totp_secret="test-totp-secret",
    )

    # 2. Create a licence payload
    # Licence serializer details omitted as it will be specific to your licence type
    licence_reference = "GBSIL123456"
    licence_data = Licence(...)

    # 3. Send a licence to HMRC
    try:
        api_response: APIResponse = client.send_licence_details(
            licence_reference, licence_data
        )

        if api_response.result == Result.accepted:
            print("Handle accepted licence")
        else:
            print("handle rejected licence")

    # The client can raise HTTP errors with attached notes
    except requests.exceptions.HTTPError as e:
        # Handle any http errors
        ...

    # Handle any unknown errors not explicitly raised by the API client
    except Exception as e:
        ...

All available serializers to create a licence payload can be found here.

NOTE: Do not use serializers found in hmrc_licensing_management/api/_serializers.py.

They have been autogenerated from the OpenAPI spec and have been updated with extra validation.

The serializers have been autogenerated from the licensing management Open API specification.

Several useful constants and utility functions can be found here and here:

Usage data callback example

This library provides a class-based view to subclass for processing usage data.

This is the HMRC API it uses: https://developer.service.hmrc.gov.uk/api-documentation/docs/api/service/push-pull-notifications-api/1.0

The licence management api usage data payload is found here:

Shown below is an example implementation with the following:

  • A model to store the incoming data
  • A view that inherits from HMRCPushPullCallbackView and stores the data in HMRCPushPullNotification
import logging

from django.core.serializers.json import DjangoJSONEncoder
from django.db import models, transaction
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt

from hmrc_licensing_management.usage import (
    HMRCPushPullCallbackView,
    HMRCPushPullResponseItem,
    NotifyUsage,
)

logger = logging.getLogger(__name__)


#
# Model used to store the usage data (taken from ECIL)
#
class HMRCPushPullNotification(models.Model):
    """Stores data received from HMRC's push pull notification API.

    Example payload to store in model:
        {
        "notificationId": "1ed5f407-8096-40d1-87ef-9a2a103eeb85",  # /PS-IGNORE
        "boxId": "50dca3fc-c37c-4f03-b719-63571333624c",
        "messageContentType": "application/json",
        "message": "[PAYLOAD]",
        "status": "PENDING",
        "createdDateTime": "2020-06-01T10:20:23.160+0000"
        }
    """

    class MessageContentType(models.TextChoices):
        application_json = "application/json"
        application_xml = "application/xml"

    class Status(models.TextChoices):
        pending = "PENDING"
        failed = "FAILED"
        acknowledged = "ACKNOWLEDGED"

    #
    # Fields containing data from HMRC
    #
    notification_id = models.TextField(
        help_text="Unique identifier for a notification."
    )
    box_id = models.TextField(
        help_text="Unique identifier for a box the notification was sent to."
    )
    message_content_type = models.CharField(
        max_length=20,
        choices=MessageContentType.choices,
        help_text="Content type of the message.",
    )
    message = models.JSONField(
        help_text=(
            "The notification message defined by messageContentType (JSON or XML). "
            "If this is JSON then it will have been escaped. "
            "Details on the structure of this data can be found in the documentation for the HMRC "
            "API that created the notification."
        ),
        encoder=DjangoJSONEncoder,
    )
    status = models.CharField(
        max_length=20, choices=Status.choices, help_text="Status of the notification."
    )
    created_datetime = models.DateTimeField(
        help_text="ISO-8601 UTC date and time the notification was created."
    )

    #
    # Fields added for ECIL
    #
    received_at = models.DateTimeField(
        auto_now_add=True,
        help_text="Date and time the notification was received by ECIL.",
    )
    processed = models.BooleanField(
        default=False, help_text="Indicates if the notification was processed."
    )
    processed_at = models.DateTimeField(
        null=True,
        default=None,
        help_text="Date and time the notification was processed.",
    )


#
# View to store the incoming usage data.
#
@method_decorator(csrf_exempt, name="dispatch")
@method_decorator(transaction.atomic, name="post")
class LicenceDetailsUsageCallbackView(HMRCPushPullCallbackView):
    def process_payload(self, payload: HMRCPushPullResponseItem) -> None:
        """Process the incoming usage data payload from HMRC

        Notes from HMRC:
        Design your application to process duplicate push notifications as a single notification (idempotency)

        The push notification system is designed to send ‘At least once’ to guarantee delivery.
        In most cases, this means notifications will be sent once and successfully received.
        In rare cases of network disruption, messages may be sent more than once.
        You should design your application to process duplicate notifications as a single notification.
        This will prevent errors and provide a consistent outcome for your application and users.
        """

        message = NotifyUsage.model_validate_json(payload.message)

        record, created = (
            HMRCPushPullNotification.objects.select_for_update().get_or_create(
                defaults={
                    "box_id": payload.boxId,
                    "message_content_type": payload.messageContentType.value,
                    "message": message.model_dump(
                        exclude_none=True, exclude_unset=True
                    ),
                    "status": payload.status.value,
                    "created_datetime": payload.createdDateTime,
                },
                notification_id=payload.notificationId,
            )
        )

        if created:
            logger.info(
                "HMRCPushPullNotification record created. notification_id: %s",
                payload.notificationId,
            )
        else:
            logger.info(
                "HMRCPushPullNotification record ignored. notification_id: %s",
                payload.notificationId,
            )

        # Do something with the data
        # e.g. trigger a task to do something with the HMRCPushPullNotification record.
        process_hmrc_push_pull_notifications.delay()

Metadata

Release files for hmrc-licensing-management 1.0.1

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

Source distribution (sdist)

Source distribution for hmrc-licensing-management 1.0.1
File Size Uploaded
hmrc_licensing_management-1.0.1.tar.gz 81.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for hmrc-licensing-management 1.0.1
File Interpreter ABI Platform
hmrc_licensing_management-1.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 122.3 kB

Release files / hmrc_licensing_management-1.0.1.tar.gz

Download URL hmrc_licensing_management-1.0.1.tar.gz
Size 81.7 kB
Tags Source
SHA-256 checksum
How to use checksums
664a5e2b5f902adbbd9964298a13abfb664123cbf5b61a7f6c0e81bfb7fdb288
BLAKE2b-256 checksum
How to use checksums
f48bcdb22f346ca664e9b157c91ee9d55dccc43ab77d9eb7999d39eea16aa2ad
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.6 {"installer":{"name":"uv","version":"0.12.6","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}

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 26, 2026.

Transparency log

Release files / hmrc_licensing_management-1.0.1-py3-none-any.whl

Download URL hmrc_licensing_management-1.0.1-py3-none-any.whl
Size 40.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2c982c5ee21e7513690bc2956254f57c41ea7497728a95c2d755c8301c4361ec
BLAKE2b-256 checksum
How to use checksums
d77c56e9fe6f97af5f34377169c460851e8212b5ec8cce6d77b923214e699e11
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.6 {"installer":{"name":"uv","version":"0.12.6","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}

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 26, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 release files

1.0.0

2 release files

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