# Laws.Africa Developer Guide

Use the Laws.Africa Legal Knowledge Platform to build products grounded in authoritative African legal information.

The platform has two main APIs:

* **Knowledge Bases** help you retrieve relevant legislation and judgment context for AI grounding, RAG, legal agents, search and workflow tools.
* **Content API** gives you full legislation collections, structured formats and metadata for deeper integrations inside your own systems.

Start with Knowledge Bases if you want the fastest path to a working integration. Use the Content API when your product needs full local copies of legislation, point-in-time versions, XML, HTML, PDF, lifecycle metadata or webhooks.

{% content-ref url="/pages/-LrrnkUvDEilSJSH4StB" %}
[Platform overview](/get-started/overview)
{% endcontent-ref %}

{% content-ref url="/pages/Ve6UZgqLViJNXjOUepzQ" %}
[Quick start](/knowledge-bases/quick-start)
{% endcontent-ref %}

{% content-ref url="/pages/RUCR0zpulO2PMXZbn5wT" %}
[Choose Knowledge Bases or the Content API](/get-started/choose-an-api)
{% endcontent-ref %}

{% content-ref url="/pages/D9LEe4mbSHxclVPFCZns" %}
[Pricing and plans](/get-started/pricing)
{% endcontent-ref %}

## Start building

{% content-ref url="/pages/Eejrvs58dxOBVkclQXlk" %}
[Knowledge Bases](/knowledge-bases/knowledge-bases)
{% endcontent-ref %}

{% content-ref url="/pages/aNtIan2DRLMtVOB7dlnt" %}
[Content API](/content-api/content-api)
{% endcontent-ref %}

## Still not sure?

* [Pricing and plans](/get-started/pricing)
* [Contact us](https://laws.africa/contact)


# Platform overview

Start building with the Laws.Africa Legal Knowledge Platform.

The Laws.Africa Legal Knowledge Platform helps developers build products that use authoritative African legal information.

The platform has two complementary APIs:

* **Knowledge Bases** retrieve relevant legislation and judgment context for AI grounding, RAG, legal agents, search and workflow tools.
* **Content API** provides full legislation content and metadata in structured formats for deeper integrations.

Most new integrations should start with Knowledge Bases. They are easier to prototype with because Laws.Africa handles ingestion, indexing, embeddings and updates. Your application sends a query and receives relevant legal context with metadata and public source links.

Use the Content API when you need direct access to full legislation collections inside your own infrastructure.

## How the products fit together

Knowledge Bases are for retrieval. They answer the question: "What legal material is relevant to this query?"

The Content API is for content access. It answers the question: "Give me this legislation, in this format, so I can store, render or process it myself."

## How to get started

### Prototype with Knowledge Bases

1. Create a free platform account.
2. Get an API token.
3. Query a Knowledge Base with a legal question or search phrase.
4. Use the returned text, metadata and public URLs in your app, search interface or AI workflow.

{% content-ref url="/pages/Ve6UZgqLViJNXjOUepzQ" %}
[Quick start](/knowledge-bases/quick-start)
{% endcontent-ref %}

### Build a deeper Content API integration

1. Choose the country or locality you need.
2. Fetch works and expressions.
3. Store the legislation and metadata you need.
4. Use webhooks or polling to keep your copy up to date.

{% content-ref url="/pages/xYokJYvpv0F9xDIBpI7g" %}
[Quick start](/content-api/quick-start)
{% endcontent-ref %}

## Plans and access

The membership plans support a progression from experimentation to production:

* **Sandbox** for free experimentation with one country and limited daily usage.
* **Build** for production Knowledge Base workflows in one country.
* **Scale** for broader coverage, higher usage and full Content API access.

See [pricing and plans](/get-started/pricing) for developer-facing plan details, limits and current pricing guidance.

Use [Manage your plan and subscription](/get-started/manage-your-plan) to learn how to check active services, change your primary API country, request plan changes and monitor usage in the platform.


# Choose Knowledge Bases or the Content API

Choose the Laws.Africa API that matches your product.

Start with the API that matches what your product needs to do.

## When to use Knowledge Bases

Use Knowledge Bases when your product needs to find relevant legal context.

Knowledge Bases are best for:

* legal AI assistants;
* RAG systems;
* legal agents and tool calls;
* semantic legal search;
* workflow tools that need relevant legislation or case law;
* prototypes where you do not want to build ingestion and indexing pipelines.

With Knowledge Bases, Laws.Africa maintains the source collections, indexes and retrieval layer. Your application sends a search query and gets back matching legal content with metadata and public source URLs.

{% content-ref url="/pages/Ve6UZgqLViJNXjOUepzQ" %}
[Quick start](/knowledge-bases/quick-start)
{% endcontent-ref %}

## When to use the Content API

Use the Content API when your product needs full legislation content inside your own systems.

The Content API is best for:

* legal publishing systems;
* compliance infrastructure;
* internal legal databases;
* offline processing;
* analytics and classification pipelines;
* products that need XML, HTML, PDF or point-in-time legislation versions.

With the Content API, your system controls storage, processing and presentation. You can use webhooks to receive update notifications for subscribed legislation content.

{% content-ref url="/pages/xYokJYvpv0F9xDIBpI7g" %}
[Quick start](/content-api/quick-start)
{% endcontent-ref %}

## When to use both

Some products use both APIs:

1. Use Knowledge Bases to find relevant legislation or judgments for a user's query.
2. Use the Content API to fetch full legislation content when the product needs to render, store or process the complete document.

This is useful when a product starts as search or AI grounding, but later needs deeper content control.

## How pricing affects the choice

Knowledge Bases are available from the free Sandbox plan, which makes them the lowest-friction starting point for evaluation and prototypes.

Full Content API access is available on Scale and Enterprise plans. Use it when your product needs complete legislation collections, historical versions, structured formats or update webhooks.

{% content-ref url="/pages/D9LEe4mbSHxclVPFCZns" %}
[Pricing and plans](/get-started/pricing)
{% endcontent-ref %}


# Pricing and plans

Laws.Africa Legal Knowledge Platform plans, limits and pricing.

Laws.Africa Legal Knowledge Platform plans are designed to support a path from free experimentation to production and multi-country integrations.

{% hint style="info" %}
Prices and limits can change. Use this page for developer guidance, and check <https://laws.africa/platform/> for current pricing before making a purchasing decision.
{% endhint %}

## Plan summary

### Sandbox

Sandbox is for prototyping and experimentation. It is free and includes one country, legislation and judgments Knowledge Bases, free access to the Cape Town by-laws Content API, and 100 calls per day.

### Build

Build is for production Knowledge Base workflows in one country. It starts at ZAR 4,200 per Knowledge Base per month and includes legislation and judgments Knowledge Bases, free access to the Cape Town by-laws Content API, and 3,000 calls per day.

### Scale

Scale is for multiple countries, higher usage and deeper integrations. It starts at ZAR 6,700 per Knowledge Base per country per month and includes legislation and judgments Knowledge Bases, 10,000 calls per day, and optional full Legislation Content API access starting at ZAR 42,000 per country per month, billed annually.

### Enterprise

Enterprise is for custom requirements, custom support and custom call limits. It supports multiple countries and content types, legislation and judgments Knowledge Bases, and optional full Legislation Content API access. Pricing is custom.

Prices exclude VAT.

## Knowledge Bases

Knowledge Bases are available on all plans.

Use Knowledge Bases for AI grounding, RAG, legal agents, semantic search and workflow tools.

## Content API

Content API access depends on the plan.

All plans include free access to the Cape Town by-laws Content API, so that you can experiment, discover how the API works, and determine if it will be useful for your application.

Use the Content API when your product needs full legislation collections inside your own systems, structured formats, historical versions, lifecycle metadata or content update webhooks.

## Choosing a plan

Start with **Sandbox** when you are evaluating results, testing prompts or building a prototype.

Move to **Build** when you need production Knowledge Base access for one country.

Move to **Scale** when you need multiple countries, higher call limits or full Content API access.

Use **Enterprise** when you need custom countries, custom call limits, support or commercial terms.

## What happens when you hit limits?

Limits protect shared infrastructure. If an application reaches its minute or daily call limit, upgrade to a plan with higher usage or contact Laws.Africa for custom limits.

Monitor current usage at <https://platform.laws.africa/usage/> and see [manage your plan and subscription](/get-started/manage-your-plan) for rate-limit handling guidance.

## Where to sign up

* Start or manage access at <https://platform.laws.africa/>.
* Manage your plan and subscription at <https://platform.laws.africa/plan-billing/>.
* Review current pricing at <https://laws.africa/platform/>.


# Manage your plan and subscription

Manage your Laws.Africa platform plan, subscription, usage and service access.

Use [platform.laws.africa](https://platform.laws.africa/) to manage the account settings that control what your API token can access.

The developer guide explains how to integrate with the APIs. The platform is where you manage your API token, active services, selected country, usage, plan and subscription requests.

## Check your current plan

Open [Plan & billing](https://platform.laws.africa/plan-billing/) to see:

* your current plan;
* your primary API country;
* your active services;
* your API rate limits;
* pending upgrade, downgrade or cancellation requests.

Use [Usage](https://platform.laws.africa/usage/) to monitor how many calls your account has made against your current limits.

## Primary API country

Your primary API country controls the default country-scoped services on self-service plans, including country-specific Knowledge Bases.

If your plan allows country changes, you can change your primary API country from [Plan & billing](https://platform.laws.africa/plan-billing/). Changing the country can change which country-specific services are active for your account.

## Change your plan

Plan changes are managed from [Plan & billing](https://platform.laws.africa/plan-billing/).

Depending on your current plan, you may be able to:

* request an upgrade to Build;
* contact Laws.Africa about Scale;
* request a downgrade;
* request cancellation.

Some changes are handled as requests rather than instant changes, especially where they affect countries, products, billing or commercial terms. Pending requests are shown on the Plan & billing page.

For current prices and commercial plan details, use [laws.africa/platform](https://laws.africa/platform/).

## Monitor usage and handle rate limits

API calls are counted against your account's active plan limits. Limits may apply per minute and per day, depending on the product family and your plan.

Open [Usage](https://platform.laws.africa/usage/) to check current usage.

Your application should handle `429 Too Many Requests` responses gracefully. For production integrations:

* log API response status codes;
* use retry and backoff for rate-limit responses;
* avoid tight retry loops;
* cache stable Content API responses where appropriate;
* monitor daily usage trends before launch;
* request an upgrade before sustained usage reaches your plan limits.

If usage looks unexpectedly high, check for repeated polling, unbounded loops, duplicate background jobs or repeated uncached requests.

## Related account tasks

* Manage API keys at <https://platform.laws.africa/api-keys/>.
* Review [developer entry points](https://platform.laws.africa/developer-resources/).
* When logged in, manage Content API webhooks at <https://platform.laws.africa/webhooks/>.


# Authentication and API keys

Authenticate Laws.Africa API requests with an API token.

Calls to the Laws.Africa APIs must be authenticated with an API token.

1. Sign up for a Laws.Africa platform account at <https://platform.laws.africa/>.
2. Create or copy your API token from <https://platform.laws.africa/api-keys/>.
3. Include the token in the `Authorization` header for API requests.

```http
Authorization: Bearer <YOUR_AUTH_TOKEN>
```

For example:

```bash
curl -H "Authorization: Bearer <YOUR_AUTH_TOKEN>" \
  https://api.laws.africa/ai/v1/knowledge-bases
```

{% hint style="info" %}
Keep your API token private. Do not commit it to source control or expose it in browser-side code.
{% endhint %}

Your API token is tied to your platform account. Your active plan and services control which Knowledge Bases, countries, localities and Content API endpoints the token can access. See [manage your plan and subscription](/get-started/manage-your-plan) for plan, country and usage management.

If you are logged into your platform account, you can also browse some API endpoints directly in your web browser.


# Knowledge Bases

Retrieve authoritative African legal context for AI, search and workflow tools.

A Knowledge Base is a searchable legal collection for a place and content type, such as national legislation, municipal by-laws or court judgments.

Use Knowledge Bases when your application needs relevant legal context but does not need to store and maintain full legal collections itself. They are the fastest way to build legal AI assistants, RAG systems, legal agents, semantic search and workflow tools grounded in African legal information.

When you query a Knowledge Base, Laws.Africa runs text and semantic search over maintained legal collections and returns the best matching results with:

* legal text or summaries;
* source metadata;
* public URLs for inspection and citation;
* a match score.

## What you can build

Knowledge Bases are useful for:

* grounding legal AI answers in maintained sources;
* retrieving legal context for RAG pipelines;
* powering semantic legislation or judgment search;
* giving agents a legal retrieval tool;
* triaging legal research questions before deeper review.

## Start here

{% content-ref url="/pages/Ve6UZgqLViJNXjOUepzQ" %}
[Quick start](/knowledge-bases/quick-start)
{% endcontent-ref %}

{% content-ref url="/pages/6dd6jVz8PAtbwflboS9p" %}
[Concepts](/knowledge-bases/concepts)
{% endcontent-ref %}

{% content-ref url="/pages/DXCgAZHCYu0EFLvHNfbv" %}
[Use results in your app](/knowledge-bases/use-in-apps)
{% endcontent-ref %}

## API endpoint

Knowledge Bases are available at:

```
https://api.laws.africa/ai/v1/knowledge-bases
```

The Knowledge Base retrieve endpoint is:

```
POST https://api.laws.africa/ai/v1/knowledge-bases/{code}/retrieve
```

See the [reference](/knowledge-bases/reference) for endpoint details.


# Quick start

Make your first Laws.Africa Knowledge Base query.

This guide shows you how to query a Knowledge Base and use the result in an application.

You will:

1. create a platform account and API token;
2. list available Knowledge Bases;
3. query a Knowledge Base;
4. use the returned legal context in your application.

## Create an API token

1. Sign up at <https://platform.laws.africa/>.
2. Get your API token from <https://platform.laws.africa/api-keys/>.

In the examples below, replace `<YOUR_AUTH_TOKEN>` with your token.

Knowledge Base retrieve calls count toward your account's Knowledge Base usage limits. You can monitor usage in the platform and read rate-limit guidance in [manage your plan and subscription](/get-started/manage-your-plan).

## List available Knowledge Bases

```bash
curl -H "Authorization: Bearer <YOUR_AUTH_TOKEN>" \
  https://api.laws.africa/ai/v1/knowledge-bases
```

The response includes Knowledge Base codes. Use a code in the retrieve endpoint.

## Query a legislation Knowledge Base

This example queries the South African municipal legislation Knowledge Base for Cape Town dog ownership rules.

```bash
curl -X POST \
  -H "Authorization: Bearer <YOUR_AUTH_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "dog ownership cape town",
    "top_k": 5,
    "filters": {
      "principal": true,
      "repealed": false,
      "frbr_place": "za-cpt"
    }
  }' \
  https://api.laws.africa/ai/v1/knowledge-bases/legislation-za-municipal/retrieve
```

The response contains matching legal portions:

```json
{
  "results": [
    {
      "content": {
        "text": "..."
      },
      "metadata": {
        "title": "Animal By-law, 2011",
        "work_frbr_uri": "/akn/za-cpt/act/by-law/2011/animal",
        "public_url": "https://lawlibrary.org.za/...",
        "portion_title": "Chapter 7 - Miscellaneous",
        "portion_public_url": "https://lawlibrary.org.za/...#chp_7"
      },
      "score": 0.16151309999999997
    }
  ]
}
```

{% hint style="info" %}
For legislation queries, start with `principal: true` and `repealed: false` so results prefer current principal legislation rather than amendment notices or repealed works.
{% endhint %}

## Use Result

The examples below make the same Knowledge Base request and format each result as source-linked context.

{% tabs %}
{% tab title="JavaScript" %}

```javascript
// Node.js 18+
const TOKEN = "<YOUR_AUTH_TOKEN>";
const KB_CODE = "legislation-za-municipal";

const response = await fetch(
  `https://api.laws.africa/ai/v1/knowledge-bases/${KB_CODE}/retrieve`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      text: "dog ownership cape town",
      top_k: 5,
      filters: {
        principal: true,
        repealed: false,
        frbr_place: "za-cpt",
      },
    }),
    signal: AbortSignal.timeout(30_000),
  },
);

if (!response.ok) {
  throw new Error(`Request failed: ${response.status} ${response.statusText}`);
}

const { results } = await response.json();

const context = results.map((result) => {
  const metadata = result.metadata;
  const source = metadata.portion_public_url || metadata.public_url;

  return [
    `Title: ${metadata.title}`,
    `Source: ${source}`,
    `Text: ${result.content.text}`,
  ].join("\n");
});

console.log(context.join("\n\n"));
```

{% endtab %}

{% tab title="Python" %}

```python
import requests

TOKEN = "<YOUR_AUTH_TOKEN>"
KB_CODE = "legislation-za-municipal"

response = requests.post(
    f"https://api.laws.africa/ai/v1/knowledge-bases/{KB_CODE}/retrieve",
    headers={
        "Authorization": f"Bearer {TOKEN}",
        "Content-Type": "application/json",
    },
    json={
        "text": "dog ownership cape town",
        "top_k": 5,
        "filters": {
            "principal": True,
            "repealed": False,
            "frbr_place": "za-cpt",
        },
    },
    timeout=30,
)
response.raise_for_status()

results = response.json()["results"]

context = []
for result in results:
    metadata = result["metadata"]
    context.append(
        "\n".join(
            [
                f"Title: {metadata.get('title')}",
                f"Source: {metadata.get('portion_public_url') or metadata.get('public_url')}",
                f"Text: {result['content']['text']}",
            ]
        )
    )

print("\n\n".join(context))
```

{% endtab %}

{% tab title="Go" %}

```go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
	"time"
)

const (
	token  = "<YOUR_AUTH_TOKEN>"
	kbCode = "legislation-za-municipal"
)

type retrieveResponse struct {
	Results []struct {
		Content struct {
			Text string `json:"text"`
		} `json:"content"`
		Metadata struct {
			Title            string `json:"title"`
			PortionPublicURL string `json:"portion_public_url"`
			PublicURL        string `json:"public_url"`
		} `json:"metadata"`
	} `json:"results"`
}

func main() {
	payload := map[string]any{
		"text":  "dog ownership cape town",
		"top_k": 5,
		"filters": map[string]any{
			"principal": true,
			"repealed":  false,
			"frbr_place": "za-cpt",
		},
	}

	body, err := json.Marshal(payload)
	if err != nil {
		log.Fatal(err)
	}

	url := fmt.Sprintf(
		"https://api.laws.africa/ai/v1/knowledge-bases/%s/retrieve",
		kbCode,
	)
	request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}
	request.Header.Set("Authorization", "Bearer "+token)
	request.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 30 * time.Second}
	response, err := client.Do(request)
	if err != nil {
		log.Fatal(err)
	}
	defer response.Body.Close()

	if response.StatusCode < 200 || response.StatusCode >= 300 {
		message, _ := io.ReadAll(response.Body)
		log.Fatalf("request failed: %s: %s", response.Status, message)
	}

	var data retrieveResponse
	if err := json.NewDecoder(response.Body).Decode(&data); err != nil {
		log.Fatal(err)
	}

	context := make([]string, 0, len(data.Results))
	for _, result := range data.Results {
		source := result.Metadata.PortionPublicURL
		if source == "" {
			source = result.Metadata.PublicURL
		}

		context = append(context, fmt.Sprintf(
			"Title: %s\nSource: %s\nText: %s",
			result.Metadata.Title,
			source,
			result.Content.Text,
		))
	}

	fmt.Println(strings.Join(context, "\n\n"))
}
```

{% endtab %}

{% tab title="C#" %}

```csharp
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

const string token = "<YOUR_AUTH_TOKEN>";
const string kbCode = "legislation-za-municipal";

using var client = new HttpClient
{
    Timeout = TimeSpan.FromSeconds(30),
};
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", token);

var payload = new
{
    text = "dog ownership cape town",
    top_k = 5,
    filters = new
    {
        principal = true,
        repealed = false,
        frbr_place = "za-cpt",
    },
};

using var response = await client.PostAsJsonAsync(
    $"https://api.laws.africa/ai/v1/knowledge-bases/{kbCode}/retrieve",
    payload
);
response.EnsureSuccessStatusCode();

using var document = JsonDocument.Parse(
    await response.Content.ReadAsStringAsync()
);

var context = new List<string>();
foreach (var result in document.RootElement.GetProperty("results").EnumerateArray())
{
    var metadata = result.GetProperty("metadata");

    string? source = null;
    if (
        metadata.TryGetProperty("portion_public_url", out var portionUrl)
        && portionUrl.ValueKind == JsonValueKind.String
    )
    {
        source = portionUrl.GetString();
    }

    if (string.IsNullOrEmpty(source))
    {
        source = metadata.GetProperty("public_url").GetString();
    }

    context.Add(
        $"Title: {metadata.GetProperty("title").GetString()}\n"
        + $"Source: {source}\n"
        + $"Text: {result.GetProperty("content").GetProperty("text").GetString()}"
    );
}

Console.WriteLine(string.Join("\n\n", context));
```

{% endtab %}
{% endtabs %}

Pass this context into your search interface, RAG prompt or agent response with the user's question. Always include source URLs so users can inspect the legal material.

## Next steps

* Learn the main [Knowledge Base concepts](/knowledge-bases/concepts).
* Apply [filters](/knowledge-bases/filters) for more precise legislation results.
* Use results in [RAG, search and agent workflows](/knowledge-bases/use-in-apps).


# Concepts

Core concepts for working with Laws.Africa Knowledge Bases.

Knowledge Bases retrieve legal context from maintained Laws.Africa collections. They are designed for applications that need search and grounding, not full local copies of legal content.

## Knowledge Base codes

Each Knowledge Base has a unique `code`. You use the code in API URLs:

```
POST /ai/v1/knowledge-bases/{code}/retrieve
```

List the Knowledge Bases available to your account with:

```
GET /ai/v1/knowledge-bases
```

## Places and content types

Knowledge Bases are scoped by place and content type. A place can be a country, province, municipality or other locality.

The current content types are:

* legislation;
* judgments.

For legislation, place identifiers follow the FRBR place codes used by Akoma Ntoso and the Content API. For example, `za` is South Africa and `za-cpt` is the City of Cape Town.

## Retrieve requests

A retrieve request includes:

* `text`: the query text to match;
* `top_k`: the maximum number of results to return;
* `filters`: optional metadata filters.

```json
{
  "text": "delict slip and trip",
  "top_k": 5,
  "filters": {}
}
```

`top_k` defaults to `10`. The maximum is `100`.

## Retrieve results

Each result includes:

* `content.text`: the matched legal text or summary;
* `metadata`: source information such as title, FRBR URI, dates and public URLs;
* `score`: an opaque relevance score.

Results are returned in descending relevance order. Higher scores indicate stronger matches. Treat scores as opaque non-negative ranking values; compare them only within the same response.

Use public URLs from result metadata when showing results to users or grounding AI responses. They let users inspect the source material.

## Query text

You can send a user's question directly as `text`, but AI workflows often get better matches when a model first rewrites the question as a focused legal search query.

For example, a user may ask:

```
Can my landlord lock me out without a court order?
```

Your application might query:

```
eviction lockout without court order residential tenant
```

## Plans and limits

Knowledge Bases are available on the platform's Sandbox, Build and Scale plans. Plans differ by country coverage, content availability and rate limits. See [Pricing and plans](/get-started/pricing) for developer-facing details.


# Use results in your app

Use Knowledge Base results in RAG, search and legal agent workflows.

Knowledge Base results are designed to be passed into product workflows as legal context. They work well in RAG systems, legal search interfaces and agent tool calls.

## Legal search

For a search interface:

1. send the user's search text to the retrieve endpoint;
2. show result titles, excerpts and source URLs;
3. group legislation results by `work_frbr_uri` when several portions come from the same work;
4. let users open `portion_public_url` or `public_url` to inspect the source.

## RAG and AI answers

For a RAG workflow:

1. retrieve the most relevant results;
2. build a context block from `content.text`, `title`, `portion_title` and source URLs;
3. ask the model to answer only from the provided context;
4. include citations or source links in the final answer.

Example context format:

```
Source: Animal By-law, 2011
URL: https://lawlibrary.org.za/akn/za-cpt/act/by-law/2011/animal/eng@2011-08-05#chp_7
Portion: Chapter 7 - Miscellaneous
Text: ...
```

{% hint style="warning" %}
Knowledge Base retrieval provides legal context. It does not replace legal review, and AI-generated answers should make their sources clear.
{% endhint %}

## Agent tools

For a legal agent, expose retrieval as a tool with inputs such as:

* `query`: focused legal search text;
* `knowledge_base`: the KB code to query;
* `place`: optional place filter;
* `top_k`: result count.

The agent should use the returned legal text and source URLs before drafting an answer or deciding whether it needs more information.

## Improve retrieval quality

If results are too broad:

* add place filters such as `frbr_place`;
* apply `principal: true` and `repealed: false` for legislation;
* reduce `top_k`;
* rewrite the user question into more specific legal search terms.

If results are too narrow:

* remove filters;
* increase `top_k`;
* query a broader Knowledge Base;
* try alternative legal terms.

## More examples

See the example repository for runnable integrations:

<https://github.com/laws-africa/knowledge-base-examples>


# Filters

Filter Knowledge Base retrieve requests.

Use filters to restrict which documents a Knowledge Base searches. Filters are most useful for legislation Knowledge Bases, especially provincial and municipal collections.

## Recommended legislation filters

For most legislation queries, start with:

```json
{
  "filters": {
    "principal": true,
    "repealed": false
  }
}
```

These filters exclude amendment or commencement works and repealed legislation.

## Place filters

Use `frbr_place` or `frbr_place__in` when you know the country, province, municipality or other locality you want to search.

```json
{
  "filters": {
    "frbr_place": "za-cpt"
  }
}
```

```json
{
  "filters": {
    "frbr_place__in": ["za-cpt", "za-jhb"]
  }
}
```

`frbr_place` is usually the safest place filter because it includes both the country and locality code.

## Other filters

The retrieve API also supports:

* `work_frbr_uri` and `work_frbr_uri__in`;
* `expression_frbr_uri` and `expression_frbr_uri__in`;
* `frbr_country`;
* `frbr_doctype` and `frbr_doctype__in`;
* `frbr_subtype` and `frbr_subtype__in`;
* `repealed`;
* `commenced`;
* `principal`.

Use exact filters only when your application already knows the relevant value. For exploratory search, start broad and then narrow the query.

## Example

```json
{
  "text": "informal trading permit",
  "top_k": 5,
  "filters": {
    "principal": true,
    "repealed": false,
    "frbr_place": "za-cpt"
  }
}
```

See the [retrieve endpoint reference](broken://pages/GxCVmalmkkGrVfvlFuXp) for the full request schema.


# Legislation Knowledge Bases

Work with legislation Knowledge Bases.

Legislation Knowledge Bases retrieve relevant portions of legislation, such as chapters, sections or schedules.

Use them when your product needs current legislative context for:

* AI grounding;
* legal search;
* legal agents;
* workflow triage;
* lightweight product integrations.

## What they return

Legislation results include:

* the matched legal text in `content.text`;
* work metadata such as `title`, `work_frbr_uri`, `frbr_place` and `expression_date`;
* portion metadata such as `portion_type`, `portion_id`, `portion_title` and `portion_public_url`;
* a public URL for the source work.

In some cases, a result may represent a page of a PDF when the legislation has not yet been converted into Akoma Ntoso format.

## Recommended filters

For most legislation retrieval, use:

```json
{
  "principal": true,
  "repealed": false
}
```

Add a place filter when the user or workflow is about a specific jurisdiction:

```json
{
  "frbr_place": "za-cpt"
}
```

See [Filters](/knowledge-bases/filters) for details.

## Versions

Knowledge Bases focus on the latest available legislation. Use the [Content API](/content-api/content-api) when your product needs point-in-time versions or full local copies of legislation.


# Judgment Knowledge Bases

Work with judgment Knowledge Bases.

Judgment Knowledge Bases retrieve relevant case law context.

Use them when your product needs:

* case law discovery;
* legal research triage;
* judgment context for AI answers;
* semantic search over case law summaries.

## What they return

Judgment results include:

* a judgment summary in `content.text`;
* judgment metadata such as `title`, `work_frbr_uri`, `expression_date` and `public_url`;
* summary fields such as `blurb` and `flynote` when available;
* a match score.

{% hint style="warning" %}
Judgment Knowledge Bases return summaries and metadata. They do not return the full original judgment text through the retrieve response.
{% endhint %}

## Example query

```json
{
  "text": "delict slip and trip",
  "top_k": 5
}
```

Use the returned `public_url` so users can inspect the source judgment.


# API Reference

Knowledge Base API reference.

Knowledge Base endpoints are available under:

{% code collapsedlinecount="10" %}

```
https://api.laws.africa/ai/v1/knowledge-bases
```

{% endcode %}

Use these endpoints to list available Knowledge Bases, inspect a single Knowledge Base and retrieve matching legal context.

## List Knowledge Bases

Call this endpoint to list the Knowledge Bases available to your account.

## Explore legal data Knowledge Bases for use with Generative AI (PREVIEW).

> Get details of Laws.Africa's Knowledge Bases.\
> \
> Note: this API is a preview and may change in future.

```json
{"openapi":"3.0.3","info":{"title":"Laws.Africa Content API","version":"20.0.0 (v1)"},"security":[{"cookieAuth":[]},{"tokenAuth":[]},{"tokenAuth":[]}],"components":{"securitySchemes":{"cookieAuth":{"type":"apiKey","in":"cookie","name":"sessionid"},"tokenAuth":{"type":"http","scheme":"bearer"}},"schemas":{"PaginatedKnowledgeBaseList":{"type":"object","required":["count","results"],"properties":{"count":{"type":"integer"},"next":{"type":"string","nullable":true,"format":"uri"},"previous":{"type":"string","nullable":true,"format":"uri"},"results":{"type":"array","items":{"$ref":"#/components/schemas/KnowledgeBase"}}}},"KnowledgeBase":{"type":"object","properties":{"code":{"type":"string","description":"Unique code identifying this knowledge base","maxLength":50,"pattern":"^[-a-zA-Z0-9_]+$"},"name":{"type":"string","description":"Name of the knowledge base","maxLength":255},"description":{"type":"string","description":"Description of the knowledge base"},"url":{"type":"string","format":"uri","readOnly":true}},"required":["code","description","name","url"]}}},"paths":{"/ai/v1/knowledge-bases":{"get":{"operationId":"knowledge_bases_list","description":"Get details of Laws.Africa's Knowledge Bases.\n\nNote: this API is a preview and may change in future.","summary":"Explore legal data Knowledge Bases for use with Generative AI (PREVIEW).","parameters":[{"name":"page","required":false,"in":"query","description":"A page number within the paginated result set.","schema":{"type":"integer"}},{"name":"page_size","required":false,"in":"query","description":"Number of results to return per page.","schema":{"type":"integer"}}],"tags":["knowledge-bases"],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedKnowledgeBaseList"}}},"description":""}}}}}}
```

## Get a Knowledge Base

Get details for a Knowledge Base using its unique code.

## Explore legal data Knowledge Bases for use with Generative AI (PREVIEW).

> Get details of Laws.Africa's Knowledge Bases.\
> \
> Note: this API is a preview and may change in future.

```json
{"openapi":"3.0.3","info":{"title":"Laws.Africa Content API","version":"20.0.0 (v1)"},"security":[{"cookieAuth":[]},{"tokenAuth":[]},{"tokenAuth":[]}],"components":{"securitySchemes":{"cookieAuth":{"type":"apiKey","in":"cookie","name":"sessionid"},"tokenAuth":{"type":"http","scheme":"bearer"}},"schemas":{"KnowledgeBase":{"type":"object","properties":{"code":{"type":"string","description":"Unique code identifying this knowledge base","maxLength":50,"pattern":"^[-a-zA-Z0-9_]+$"},"name":{"type":"string","description":"Name of the knowledge base","maxLength":255},"description":{"type":"string","description":"Description of the knowledge base"},"url":{"type":"string","format":"uri","readOnly":true}},"required":["code","description","name","url"]}}},"paths":{"/ai/v1/knowledge-bases/{code}":{"get":{"operationId":"knowledge_bases_retrieve","description":"Get details of Laws.Africa's Knowledge Bases.\n\nNote: this API is a preview and may change in future.","summary":"Explore legal data Knowledge Bases for use with Generative AI (PREVIEW).","parameters":[{"in":"path","name":"code","schema":{"type":"string","description":"Unique code identifying this knowledge base"},"required":true}],"tags":["knowledge-bases"],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeBase"}}},"description":""}}}}}}
```

## Query a Knowledge Base

Use the retrieve endpoint to query a Knowledge Base for legal information that matches keywords, phrases or AI-generated search text.

The request must:

1. identify the Knowledge Base by `code` in the URL;
2. include the `text` to search for;
3. optionally include `top_k` and `filters`.

The response returns matching items with text, metadata and a score.

## Query the knowledge base for matching items.

> Retrieve items from the knowledge base that match your query.

```json
{"openapi":"3.0.3","info":{"title":"Laws.Africa Content API","version":"20.0.0 (v1)"},"security":[{"cookieAuth":[]},{"tokenAuth":[]},{"tokenAuth":[]}],"components":{"securitySchemes":{"cookieAuth":{"type":"apiKey","in":"cookie","name":"sessionid"},"tokenAuth":{"type":"http","scheme":"bearer"}},"schemas":{"KnowledgeBaseRetrieveRequest":{"type":"object","description":"Details to retrieve items from a knowledge base.","properties":{"text":{"type":"string","description":"The text to find matching items for"},"top_k":{"type":"integer","maximum":100,"minimum":1,"default":10,"description":"Number of results to return"},"filters":{"properties":{"work_frbr_uri":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Work Frbr Uri"},"work_frbr_uri__in":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"default":null,"title":"Work Frbr Uri  In"},"expression_frbr_uri":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Expression Frbr Uri"},"expression_frbr_uri__in":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"default":null,"title":"Expression Frbr Uri  In"},"frbr_place":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Frbr Place"},"frbr_place__in":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"default":null,"title":"Frbr Place  In"},"frbr_doctype":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Frbr Doctype"},"frbr_doctype__in":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"default":null,"title":"Frbr Doctype  In"},"frbr_subtype":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Frbr Subtype"},"frbr_subtype__in":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"default":null,"title":"Frbr Subtype  In"},"repealed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"title":"Repealed"},"commenced":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"title":"Commenced"},"principal":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"title":"Principal"}},"title":"KnowledgeBaseFilters","type":"object"}},"required":["text"]},"KnowledgeBaseRetrieveResponse":{"type":"object","description":"Items retrieved from a knowledge base.","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/KnowledgeBaseItem"}}},"required":["results"]},"KnowledgeBaseItem":{"type":"object","description":"A knowledge base item.","properties":{"content":{"$ref":"#/components/schemas/KnowledgeBaseItemContent"},"metadata":{"$ref":"#/components/schemas/KnowledgeBaseItemMetadata"},"score":{"type":"number","format":"double","minimum":0,"description":"The similarity score of the item (higher is better)"}},"required":["content","metadata","score"]},"KnowledgeBaseItemContent":{"type":"object","description":"The content of a knowledge base item.","properties":{"text":{"type":"string","description":"The text content of the item"}},"required":["text"]},"KnowledgeBaseItemMetadata":{"type":"object","description":"Metadata about a knowledge base item.","properties":{"work_frbr_uri":{"type":"string","description":"Work FRBR URI for the work the item belongs to"},"frbr_place":{"type":"string","description":"FRBR place code"},"frbr_country":{"type":"string","description":"FRBR country code"},"frbr_doctype":{"type":"string","description":"FRBR document type"},"frbr_subtype":{"type":"string","description":"FRBR document subtype"},"title":{"type":"string","description":"Title of the document the item belongs to"},"repealed":{"type":"boolean","description":"Is the work repealed? Legislation only."},"commenced":{"type":"boolean","description":"Is the work commenced? Legislation only."},"principal":{"type":"boolean","description":"Is the work a principal work? Legislation only."},"blurb":{"type":"string","description":"A short one-sentence summary of the document (Judgments only)."},"flynote":{"type":"string","description":"A flynote (key phrases) for the document (Judgments only)."},"expression_date":{"type":"string","format":"date","description":"Expression date of the document"},"expression_frbr_uri":{"type":"string","description":"Expression FRBR URI for the document the item belongs to"},"public_url":{"type":"string","description":"Public URL to access the document"},"portion_type":{"allOf":[{"$ref":"#/components/schemas/PortionTypeEnum"}],"description":"Type of the portion\n\n* `page` - page\n* `provision` - provision\n* `text` - text\n* `summary` - summary"},"portion_id":{"type":"string","description":"Identifier of the portion. A page number when type=page, an eid when type=portion."},"portion_title":{"type":"string","description":"Title of the portion"},"portion_parent_ids":{"type":"array","items":{"type":"string"},"description":"List of parent portion IDs, from top downwards"},"portion_parent_titles":{"type":"array","items":{"type":"string"},"description":"List of parent portion titles, from top downwards"},"portion_public_url":{"type":"string","description":"Public URL to access the portion"}},"required":["expression_date","expression_frbr_uri","frbr_country","frbr_doctype","frbr_place","frbr_subtype","portion_type","public_url","title","work_frbr_uri"]},"PortionTypeEnum":{"enum":["page","provision","text","summary"],"type":"string","description":"* `page` - page\n* `provision` - provision\n* `text` - text\n* `summary` - summary"}}},"paths":{"/ai/v1/knowledge-bases/{code}/retrieve":{"post":{"operationId":"knowledge_bases_retrieve_create","description":"Retrieve items from the knowledge base that match your query.","summary":"Query the knowledge base for matching items.","parameters":[{"in":"path","name":"code","schema":{"type":"string","description":"Unique code identifying this knowledge base"},"required":true}],"tags":["knowledge-bases"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeBaseRetrieveRequest"}},"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/KnowledgeBaseRetrieveRequest"}},"multipart/form-data":{"schema":{"$ref":"#/components/schemas/KnowledgeBaseRetrieveRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgeBaseRetrieveResponse"}}},"description":""}}}}}}
```


# Content API

Fetch full legislation content and metadata from Laws.Africa.

The Content API is a read-only API for listing and fetching published versions of legislative works.

Use it when your product needs full legislation collections inside your own systems, including:

* Akoma Ntoso XML;
* HTML;
* PDF where available;
* table of contents JSON;
* publication, commencement, amendment and repeal metadata;
* point-in-time legislation versions;
* webhooks for content updates.

For AI grounding, search, RAG and legal agents, start with [Knowledge Bases](/knowledge-bases/knowledge-bases). Use the Content API when retrieval is not enough and your product needs deeper control over storage, processing or presentation.

## Endpoint

The latest version of the Content API is version 3:

```
https://api.laws.africa/v3/
```

## What you can build

The Content API is best for:

* legal publishing systems;
* compliance infrastructure;
* internal legal databases;
* offline processing;
* analytics and classification pipelines;
* advanced legislation rendering.

## Content formats

Some API calls can return content in multiple formats. Specify the format by placing `.format` at the end of the URL or by using the `Accept` header.

* `.json` or `Accept: application/json`: JSON
* `.xml` or `Accept: application/xml`: Akoma Ntoso XML
* `.html` or `Accept: text/html`: HTML
* `.epub` or `Accept: application/epub+zip`: ePUB
* `.pdf` or `Accept: application/pdf`: PDF
* `.zip` or `Accept: application/zip`: ZIP file with XML and media attachments

{% hint style="info" %}
Not all responses support all formats. Endpoint reference pages describe the formats available for each response.
{% endhint %}

## OpenAPI schema

* Download the OpenAPI schema from <https://api.laws.africa/v3/schema>
* Swagger UI: <https://api.laws.africa/v3/schema/swagger-ui>
* Redoc: <https://api.laws.africa/v3/schema/redoc>


# Quick start

Fetch legislation from the Laws.Africa Content API.

This quick start shows you how to fetch legislation from the Content API. Use this path when your application needs full legislation content or metadata.

You will:

1. create a Laws.Africa account and API token;
2. list available legislation;
3. fetch metadata for a work;
4. fetch HTML for a legislation portion;
5. display the HTML.

{% hint style="info" %}
If you want search, RAG, legal agents or AI grounding, start with the [Knowledge Base quick start](/knowledge-bases/quick-start) instead.
{% endhint %}

## Create an API token

1. Sign up at <https://platform.laws.africa/>.
2. Get your API token from <https://platform.laws.africa/api-keys/>.

In the examples below, replace `<YOUR_AUTH_TOKEN>` with your token.

Content API requests count toward your account's plan limits. Listing and crawling paginated collections can use many calls, so monitor usage in the platform and read the guidance in [manage your plan and subscription](/get-started/manage-your-plan).

## Get a list of by-laws

Fetch a list of municipal by-laws for the City of Cape Town.

```bash
curl -H "Authorization: Bearer <YOUR_AUTH_TOKEN>" \
  https://api.laws.africa/v3/akn/za-cpt/.json
```

The `za-cpt` part of the URL identifies the City of Cape Town in South Africa.

## Fetch the Animal By-law

Fetch metadata for Cape Town's Animal By-law in JSON format.

```bash
curl -H "Authorization: Bearer <YOUR_AUTH_TOKEN>" \
  https://api.laws.africa/v3/akn/za-cpt/act/by-law/2011/animal.json
```

The response includes the title, publication details and links to additional API calls.

Fetch the table of contents:

```bash
curl -H "Authorization: Bearer <YOUR_AUTH_TOKEN>" \
  https://api.laws.africa/v3/akn/za-cpt/act/by-law/2011/animal/toc.json
```

Fetch the HTML content for section 3:

```bash
curl -H "Authorization: Bearer <YOUR_AUTH_TOKEN>" \
  https://api.laws.africa/v3/akn/za-cpt/act/by-law/2011/animal/eng/!main~chp_2__sec_3
```

## Display the by-law

Use [Laws.Africa Law Widgets](https://github.com/laws-africa/law-widgets/blob/main/core/README.md) to style Akoma Ntoso HTML.

```markup
<script type="module" src="https://cdn.jsdelivr.net/npm/@lawsafrica/law-widgets@latest/dist/lawwidgets/lawwidgets.esm.js"></script>

<la-akoma-ntoso>
  <section class="akn-section" id="section-3" data-id="section-3">
    <h3>3. Dog registration and licensing</h3>
    <section class="akn-subsection" id="section-3.1" data-id="section-3.1">
      <span class="akn-num">(1)</span>
      <span class="akn-content"><span class="akn-p">The owner of a property where one or more dogs are kept must register the dog or dogs with the Council.</span></span>
    </section>
  </section>
</la-akoma-ntoso>
```

![](/files/-Lrrp-s3JG1aQukyz40M)

## Next steps

* Learn about [works and expressions](/content-api/works-and-expressions).
* Browse the [Content API reference](/content-api/reference).
* Follow the [advanced Content API tutorial](/tutorials/about-the-tutorial).


# Works and expressions

Understand these two important concepts.

<figure><img src="/files/2KaJg66d7G0h4RoTNRgC" alt=""><figcaption></figcaption></figure>

Two important concepts that are an essential part of the API are **works** and **expressions**.

* A **Work** is a piece of legislation, such as an act, regulation or by-law. A work may be amended over time and may even have its title changed. A work is uniquely identified by a *work FRBR URI* which never changes.
* An **Expression** is a version of a Work in a specific language at a particular point in time. A work can have many expressions, usually one for each official language and amendment. An expression is uniquely identified by its own *expression FRBR URI*, which is derived from the work's FRBR URI.

An example of a work is the South African *Employment Equity Amendment Act, 2013 (Act 55 of 1998)* with unique work FRBR URI `/akn/za/act/1998/55`. This act has been amended a number of times since it was first passed. Each amended version (also called a *point in time*) is a unique expression of the work.

The English expression of the work, as it was amended on 17 January 2014, is uniquely identified by the expression FRBR URI `/akn/za/act/1998/55/eng@2014-01-17`. You can see that this is built from the work's URI, with a language code `eng` and the expression date `2014-01-17` included.

{% hint style="info" %}
When fetching details from the API, you are always fetching details for a particular expression of the work. The expression will also include information related to the expression's work, such as the work's FRBR URI and publication information. Even if you don't specify a particular date for the expression, the API will return the latest expression applicable at the time of the request.
{% endhint %}

{% hint style="info" %}
Read more about the terminology used by Laws.Africa in our [Terminology Guide](https://docs.laws.africa/getting-started/terminology-guide).
{% endhint %}

## Akoma Ntoso FRBR URIs

The API relies heavily on Akoma Ntoso FRBR URIs, which are described in the [Akoma Ntoso naming convention standard](http://docs.oasis-open.org/legaldocml/akn-nc/v1.0/akn-nc-v1.0.html).

When we use a URL such as `/v3/frbr-uri/` in this guide, the `frbr-uri` part is a full FRBR URI, such as `/akn/za/act/1998/84/eng`.


# Webhooks

Webhooks are push notifications when a work is created, updated or deleted.

When you are logged into the platform, you can add a webhook URL from your Laws.Africa platform account at <https://platform.laws.africa/webhooks/>.

When a Work Expression is created, updated or deleted, [Laws.Africa](http://laws.africa) will send a POST request to the webhook URL with details of the action in JSON in the body of the request.

## Add a webhook

1. Visit <https://platform.laws.africa/webhooks/>
2. Under **Add a new webhook** fill in the URL field with your webhook URL.
3. Click **Add webhook**

<figure><img src="/files/CL9vtqbeON1RGKmO4QY4" alt=""><figcaption></figcaption></figure>

## Delete a webhook

1. Visit <https://platform.laws.africa/webhooks/>
2. Click the **Delete** button alongside the webhook you wish to delete.

## Webhook invocation details

The body of a webhook invocation request contains the following information:

```jsx
{
  "invocation_id": "abc-123", // unique ID for this invocation; re-used if the invocation fails and is retried
  "request_id": "def-456",    // unique request ID for this HTTP request, never re-used
  "action": "updated",        // or "created" or "deleted"
  "object": "work",
  "work": {
    "frbr_uri": "/akn/za/act/2009/1",
    "url": "https://api.laws.africa/v3/akn/za/act/2009/1"
  }
}
```

## Handling a webhook invocation

A webhook invocation should be treated as an indication that the work has been changed or deleted. It should do minimal work so that it can respond within the 10 second timeout. A common approach is to trigger an asynchronous (background) task to fetch/refresh the data for all expressions of the work, and return a 200-success response.

### Retries

[Laws.Africa](http://laws.africa) expects the webhook endpoint to return a 200-level response code within 10 seconds. A response that takes longer than that is considered a failure.

Laws.Africa will retry a failed (non-200 response) webhook invocation up to 10 times. Laws.Africa will initially retry after just a few seconds. If more attempts fail, it will use exponential backoff to delay retrying longer and longer (up to a few hours between attempts).

### Duplicate invocations

It is possible for a work to be updated rapidly in quick succession. [Laws.Africa](http://laws.africa) will group multiple updates to the same work within a short period (30 seconds) into a single invocation. This avoids overloading the webhook target with repeated invocations in quick succession.

Because a webhook invocation can be retried, it is important that your webhook endpoint can safely process a duplicate invocation. [Laws.Africa](http://laws.africa) cannot guarantee exactly-once delivery of a webhook invocation.

### Handling deletions

In rare cases a work may be deleted and then re-created a few minutes later. The order of the webhook invocations is not guaranteed. This means that you may receive the “deleted” webhook invocation after the “created” invocation.

Therefore, rather than immediately deleting a work when getting a “deleted” webhook, first call the [Laws.Africa](http://laws.africa) API to see if that work still exists. If the API returns a 404, then it is safe to delete that work. If the work does exist, then consider your copy out of date and treat the invocation as an “updated” invocation.


# How to use the Table of Contents API

Using the Laws.Africa Table of Contents API in your application.

This guide will take you through how to use the Table of Contents (TOC) API, part of [Laws.Africa's Content API](/content-api/content-api). After reading this guide you will know:

* what the Table of Contents API is and why it's useful
* how to call the Table of Contents API
* how to integrate the Table of Contents into your website or application
* how to display the Table of Contents and link it to the text of a work
* how to use the Table of Contents to fetch a section, chapter or part of a work

## What is the Table of Contents API?

The Table of Contents (TOC) API is a JSON description of the hierarchy and structure of a work. It's useful for including a Table of Contents in your website or application, or for helping your users navigate or explore the structure of a work.

For example, here's a screenshot of the Table of Contents for [Cape Town's Animal By-law](https://openbylaws.org.za/za-cpt/act/by-law/2011/animal/eng/) generated using the TOC API:

![](/files/-Lrrt_qgiiTpxTGW_auR)

Here's an extract of the corresponding JSON from the Table of Contents API:

```javascript
"toc": [
  {
    "type": "preamble",
    "subcomponent": "preamble",
    "url": "https://api.laws.africa/v3/akn/za-cpt/act/by-law/2011/animal/eng/!main~preamble",
    "title": "Preamble",
    "component": "main"
  },
  {
    "heading": "Interpretation",
    "type": "chapter",
    "id": "chapter-1",
    "component": "main",
    "url": "https://api.laws.africa/v3/akn/za-cpt/act/by-law/2011/animal/eng/!main~chp_1",
    "title": "Chapter 1 – Interpretation",
    "num": "1",
    "subcomponent": "chapter/1"
    "children": [
      {
        "heading": "Definitions",
        "type": "section",
        "id": "section-1",
        "component": "main",
        "url": "https://api.laws.africa/v3/akn/za-cpt/act/by-law/2011/animal/eng/!main~chp_1__sec_1",
        "title": "1. Definitions",
        "num": "1.",
        "subcomponent": "section/1"
      }
    ],
  },
]
```

There is rich information about each element in the Table of Contents, including a type, number, heading, well-formatted title and URL. Each element may also contain nested children. For example, a part or chapter may contain sections.

## Why the Table of Contents API is useful

The Table of Contents API simplifies the task of navigating the structure of a work, both for the user and programatically. It takes care of the complications of dealing with Akoma Ntoso, such as handling different legislative traditions, doing translations and formatting titles, so that you don't have to.

Use the Table of Contents API to:

* put a clickable Table of Contents alongside a work on a webpage
* help the user navigate the structure of a work
* fetch certain parts, chapters and sections of a work directly from the API

## How to fetch the TOC for a work

Fetch the Table of Contents for a work using the `/frbr-uri/toc.json` URL for the work, such as <https://api.laws.africa/v3/akn/za-cpt/act/by-law/2011/animal/toc.json>.

```bash
curl -H "Authorization: Bearer <YOUR_AUTH_TOKEN>" \
  https://api.laws.africa/v3/akn/za-cpt/act/by-law/2011/animal/toc.json
```

{% hint style="info" %}
The Table of Contents will change depending on the language and date of the work expression.
{% endhint %}

You can also find this URL in the work's `links` array:

```javascript
"links": [
  {
    "title": "Table of Contents",
    "rel": "toc",
    "mediaType": "application/json",
    "href": "https://api.laws.africa/v3/akn/za-cpt/act/by-law/2011/animal/toc.json"
  },
]
```

[Table of Contents API reference](/content-api/reference/works-and-expressions/table-of-contents)

### Legislative traditions

The TOC API automatically adapts to the local legislative tradition of a country. For example, in South African legislation the Section is the basic unit. In other traditions, the Paragraph or Article is the basic unit. Laws.Africa handles this for you and generates a Table of Contents that takes local tradition into account.

## Showing a Table of Contents alongside a work

The most common use of the Table of Contents API is to put a clickable Table of Contents alongside a work in a website. This helps the user understand the structure of the text and navigate to the sections that are most important to them.

We're going to add the Table of Contents as a series of nested `ul` and `li` elements, and each entry will be a clickable `a` element with a link to the matching portion of the text.

We're going to use three key attributes of a Table of Contents entry:

* `title` – a formatted title for this entry, combining `num` and `heading`;
* `id` – the ID of this element in the HTML body of the work returned by the Content API; and
* `children` – a list of sub-entries underneath this entry.

First, let's assume you have fetched the work's HTML content from the `/frbr-uri.html` Content API and it's already on your webpage along with an empty element for the TOC:

```markup
<aside id="toc"></aside>
<article class="akoma-ntoso">(HTML from Content API goes here)</article>
```

Let's also assume we have fetched the Table of Contents JSON from the API, and stored it the variable `toc`.

Now we're ready to add the Table of Contents as HTML to the page:

```javascript
function makeToc(entries) {
  // the container for these entries
  var ul = document.createElement('ul');

  entries.forEach(function(entry) {
    var li = document.createElement('li'),
        a = document.createElement('a');

    // make a link such as <a href="#chapter-1">Chapter 1</a>
    a.innerText = entry.title;
    // the entry.id matches the corresponding HTML body element's ID attribute
    if (entry.id) a.setAttribute('href', '#' + entry.id);
    li.appendChild(a);
    ul.appendChild(li);

    // make nested lists for this entry's children
    if (entry.children) {
      li.appendChild(makeToc(entry.children));
    }
  });

  return ul;
}

// transform the table of contents into a UL element and add it to the page
var ulToc = makeToc(toc);
document.getElementById('toc').appendChild(ulToc);
```

Now you have a clickable Table of Contents alongside your HTML:

```markup
<aside id="toc">
  <ul>
    <li>
      <a>Preamble</a>
    </li>
    <li>
      <a href="#chapter-1">Chapter 1 – Interpretation</a>
      <ul>
        <li>
          <a href="#section-1">1. Definitions</a>
        </li>
      </ul>
    </li>
    <li>
      <a href="#chapter-2">Chapter 2 – Dogs</a>
      <ul>
        <li>
          <a href="#section-2">2. Restriction on number of dogs</a>
        </li>
        <li>
          <a href="#section-3">3. Dog registration and licensing</a>
        </li>
        ...
      </ul>
    </li>
  </ul>
</aside>
```

## Fetching only a section, chapter or part of a work

The Table of Contents API also makes it easier to fetch portions of a work, such as a particular chapter, part or section. You can do this by using the `url` attribute of the TOC entry. Not all entries have a URL, so be sure to check first.

In this example we'll fetch the portion of the [Cape Town's Animal By-law](https://openbylaws.org.za/za-cpt/act/by-law/2011/animal/eng/) chosen by the user. Again, we'll assume that the Table of Contents JSON has been fetched and stored in the `toc` variable.

We'll use the TOC to populate a `select` element and then respond to a change by loading the HTML into a `div`.

```markup
<label>Choose a section:</label> <select id="toc-selector"></select>
<div id="section-content" class="akoma-ntoso"></div>
```

```javascript
var select = document.getElementById('toc-selector');

// transform the TOC into options in a select
function makeTocSelect(entries) {
  entries.forEach(function(entry) {
    if (entry.url) {
      var option = document.createElement('option');
      option.innerText = entry.title;
      option.value = entry.url;
      select.appendChild(option);
    }

    // include entries for this entry's children
    if (entry.children) {
      makeTocSelect(entry.children);
    }
  });
}

// selection changed, load the HTML
function selectionChanged(e) {
  var xhr = new XMLHttpRequest(),
      container = document.getElementById('section-content'),
      url = e.target.value + '.html',
      // get your API token from https://platform.laws.africa/api-keys/
      apiToken = '<YOUR_AUTH_TOKEN>';

  xhr.open('GET', url);
  xhr.setRequestHeader('Authorization', 'Bearer ' + apiToken);
  xhr.onload = function() {
    if (xhr.status === 200) {
      container.innerHTML = xhr.responseText;
    } else {
      container.innerText = 'Request failed. Returned status of ' + xhr.status;
    }
  };
  xhr.send();
}

// fetch content when the selection changes
select.addEventListener('change', selectionChanged);

// build the select options
makeTocSelect(toc);

```


# How to download images

Downloading embedded images from the Laws.Africa Content API.

This guide will take you through how to work with images and other media embedded in expressions in Laws.Africa’s Content API. After reading this guide you will know:

* how to list all the images and media included with an expression
* how to download and save images and other media from the Content API
* how to ensure that images are loaded correctly in your HTML content

## What are embedded images?

Some legislation includes embedded images. You will need to download and store these images to ensure they are displayed when you show the legislation to your users.

{% hint style="info" %}
You must download these images to include them in your HTML. The images will not be shown from the Laws.Africa servers.
{% endhint %}

## Including images in your application or website

There are two steps to include embedded images in the legislation used in your app or website.

1. Download the images from the Laws.Africa Content API and save them to your server or app.
2. Ensure the HTML you fetch from the Laws.Africa Content API references the images correctly.

We'll go through each of these steps below.

## How to list images using the Content API

The Laws.Africa Content API makes it easy to list and download embedded images for a particular work. Fetch the list of images using the `/frbr-uri/media.json` URL for the work. You can find this URL in the work's `links` array:

```javascript
"links": [
  {
    "mediaType": "application/json",
    "href": "https://api.laws.africa/v2/akn/za-jhb/act/by-law/2004/public-road-electronic-communications-networks-and-miscellaneous/eng/media.json",
    "title": "Media",
    "rel": "media"
  },
]
```

For example, here is the API call to list the images embedded with Johannesburg's [Public Road and Miscellaneous By-laws by-law](https://openbylaws.org.za/za-jhb/act/by-law/2004/public-road-electronic-communications-networks-and-miscellaneous/eng/):

```bash
$ curl -H "Authorization: Bearer <YOUR_AUTH_TOKEN>" \
  https://api.laws.africa/v2/akn/za-jhb/act/by-law/2004/public-road-electronic-communications-networks-and-miscellaneous/media.json
```

```javascript
{
  "count": 10,
  "next": null,
  "previous": null,
  "results": [
    {
      "url": "https://api.laws.africa/v2/akn/za-jhb/act/by-law/2004/public-road-electronic-communications-networks-and-miscellaneous/eng@2011-08-10/media/certificate.png",
      "filename": "certificate.png",
      "mime_type": "image/png",
      "size": 253224
    },
    {
      "url": "https://api.laws.africa/v2/akn/za-jhb/act/by-law/2004/public-road-electronic-communications-networks-and-miscellaneous/eng@2011-08-10/media/indemnity.png",
      "filename": "indemnity.png",
      "mime_type": "image/png",
      "size": 210645
    },
  ]
}
```

Each item in the list describes a single image or media type and includes the filename, [mime type](https://en.wikipedia.org/wiki/Media_type), filesize in bytes, and the URL to use to download the file.

## Downloading and saving images

In order for the embedded images to show up when your users view the content you have fetched from the Laws.Africa API, you must first download and store the images locally.

{% hint style="info" %}
You must save the images in a folder called `media`, next to your legislation HTML file. The `<img>` tags in the HTML will try to load the images from this path.
{% endhint %}

For example, suppose you have downloaded the English HTML content of the 2011-08-10 point in time for the by-law and saved it as:

* `public-road-electronic-communications-networks-and-miscellaneous/eng/2011-08-10/index.html`

Then you should download and save the media files to `media` folder in the same directory:

* `public-road-electronic-communications-networks-and-miscellaneous/eng/2011-08-10/media/`

This ensures that you'll keep the different images for different works, languages and points in time separate.

{% hint style="info" %}
You must save the images separately for each separate point in time and language that you download.
{% endhint %}

## Controlling where images are loaded from

By default, HTML from Laws.Africa will try to load images from the `media` directory relative to the current URL, using an image tag such as `<img src="media/wayleave-application.png">`.

If you need to load images from a different location you can tell Laws.Africa to use apply a prefix to the `src` attribute of image tags by using the `?media-url` query parameter.

For example, using `?media-url=/static/assets/za/act/1995/2/eng/2018-11-10/` will produce image tags such as `<img src="/static/assets/za/act/1995/2/eng/2018-11-10/media/wayleave-application.png">`.

{% hint style="info" %}
You should encode the FRBR URI into the `media-url` parameter to ensure that you show images for the correct work, point in time and language.
{% endhint %}


# API Reference

Content API reference.

The Content API is available under:

```
https://api.laws.africa/v3/
```

Use these reference pages when your product needs full legislation content, metadata, formats or update workflows.

{% content-ref url="/pages/KKgsm6edNwCb20bQZzec" %}
[Authentication](/content-api/reference/authentication)
{% endcontent-ref %}

{% content-ref url="/pages/I80b8edHcJXNrkmg3cfB" %}
[Pagination](/content-api/reference/pagination)
{% endcontent-ref %}

{% content-ref url="/pages/-LuBmqjMGU4cxRpMTWx9" %}
[Places](/content-api/reference/countries-and-localities)
{% endcontent-ref %}

{% content-ref url="/pages/-LuBr6Gv8a8hSMTKOk0y" %}
[Single work expression](/content-api/reference/works-and-expressions)
{% endcontent-ref %}


# Authentication

The Content API uses the shared Laws.Africa API token authentication scheme.

{% content-ref url="/pages/2mveWei9sQIkok9cag4Y" %}
[Authentication and API keys](/get-started/authentication)
{% endcontent-ref %}


# Pagination

API calls that return lists will be paginated and return a limited number of items per page. The response includes information on the total number of items and the URLs to use to fetch the next and previous pages of items:

<table><thead><tr><th width="141">Field</th><th width="487">Meaning</th><th>Type</th></tr></thead><tbody><tr><td><code>count</code></td><td>Total number of items in the entire response, across all pages.</td><td>number</td></tr><tr><td><code>next</code></td><td>URL for the next page of results, if any.</td><td>string or null</td></tr><tr><td><code>previous</code></td><td>URL for the previous page of results, if any.</td><td>string or null</td></tr></tbody></table>

Here's an example of the first page of a paginated response with 250 total items and two pages:

```javascript
{
  "count": 250,
  "next": "https://api.laws.africa/v3/akn/za.json?page=2",
  "previous": null,
  "results": [ "..." ]
}
```

In this case, fetching the `next` URL will return the second (and final) page.

Our recommended way of walking through all paginated results is using a `while` loop like the following in Python:

```python
def fetch(url):
  while url:
    response = get(url)
    results = response["results"]
    # TODO: do something with the results
    url = response["next"]
```


# Places

List the places - countries and localities (sub-country regions) - that are available from the Content API.

All Laws.Africa content belongs to a country or a locality within a country. A locality is a jurisdiction within a country, such as a province or municipality. Together, they form a two-level hierarchy.

## Place codes

**Countries** are identified using two-letter [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country codes, such as `na` or `za`.

**Localities** are identified using a combination of the country code and a locality code specific to the country, such as `za-cpt`.

Locality codes are not well standardised and may vary between different countries. In South Africa, for example, municipalities are identified by the codes determined by the [South African Municipal Demarcation Board](http://www.demarcation.org.za/).

## Get places

{% openapi src="/files/4kNXPoVcclCo2986qOST" path="/v3/places" method="get" %}
[Laws.Africa Content API 2024-04-23.yaml](https://4163728571-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LrrJ5L0RJ3goKWzqKVC%2Fuploads%2FYkGoMScj3txq3sbsbbwo%2FLaws.Africa%20Content%20API%20\(v3\)%20\(2\).yaml?alt=media\&token=7476d4cb-8aff-4c63-8ec8-44782e977024)
{% endopenapi %}

## Get a place

{% openapi src="/files/4kNXPoVcclCo2986qOST" path="/v3/places/{frbr\_uri\_code}" method="get" %}
[Laws.Africa Content API 2024-04-23.yaml](https://4163728571-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LrrJ5L0RJ3goKWzqKVC%2Fuploads%2FYkGoMScj3txq3sbsbbwo%2FLaws.Africa%20Content%20API%20\(v3\)%20\(2\).yaml?alt=media\&token=7476d4cb-8aff-4c63-8ec8-44782e977024)
{% endopenapi %}

## Get work expressions for a place

Use the `frbr_uri_code` for the place to fetch work expressions in that place.

{% openapi src="/files/4kNXPoVcclCo2986qOST" path="/v3/places/{frbr\_uri\_code}/work-expressions" method="get" %}
[Laws.Africa Content API 2024-04-23.yaml](https://4163728571-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LrrJ5L0RJ3goKWzqKVC%2Fuploads%2FYkGoMScj3txq3sbsbbwo%2FLaws.Africa%20Content%20API%20\(v3\)%20\(2\).yaml?alt=media\&token=7476d4cb-8aff-4c63-8ec8-44782e977024)
{% endopenapi %}


# All work expressions

Fetch all work expressions.

Use the global work expressions API to fetch all work expressions your subscription has access to.

{% hint style="info" %}
You can use the [place-specific work expressions API](/content-api/reference/countries-and-localities) to fetch work expressions for a particular place.

You can use the [taxonomies work expressions API](/content-api/reference/taxonomy-topics) to fetch work expressions for a particular taxonomy topic.
{% endhint %}

## Fetch work expressions for all places

{% openapi src="/files/4kNXPoVcclCo2986qOST" path="/v3/work-expressions" method="get" %}
[Laws.Africa Content API 2024-04-23.yaml](https://4163728571-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LrrJ5L0RJ3goKWzqKVC%2Fuploads%2FYkGoMScj3txq3sbsbbwo%2FLaws.Africa%20Content%20API%20\(v3\)%20\(2\).yaml?alt=media\&token=7476d4cb-8aff-4c63-8ec8-44782e977024)
{% endopenapi %}


# Single work expression

Fetch a single work expression using an FRBR URI.

## Fetch a single work expression

{% openapi src="/files/4kNXPoVcclCo2986qOST" path="/v3/{frbr\_uri}" method="get" %}
[Laws.Africa Content API 2024-04-23.yaml](https://4163728571-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LrrJ5L0RJ3goKWzqKVC%2Fuploads%2FYkGoMScj3txq3sbsbbwo%2FLaws.Africa%20Content%20API%20\(v3\)%20\(2\).yaml?alt=media\&token=7476d4cb-8aff-4c63-8ec8-44782e977024)
{% endopenapi %}

Supported content types: JSON, HTML, PDF, ePub, zip.

### Query Parameters for HTML Content

When fetching HTML content (using the `.html` endpoint), you can control some aspects of how the HTML is generated using query parameters.

For example, usually you will want to make links to other legislation relative to your website. You can do this by using `?resolver=none` when fetching the HTML.

| Name       | Type   | Description                                                                                                                                                                 |
| ---------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| coverpage  | string | Should the response include a generated coverpage? Use 1 for true, anything else for false. Default: 1. HTML-only.                                                          |
| media-url  | string | The fully-qualified URL prefix to use when generating links to embedded media, such as images.                                                                              |
| resolver   | string | The fully-qualified URL to use when resolving references to other Akoma Ntoso documents. Use `no` or `none` to disable. Defaults to using the Laws.Africa resolver.         |
| standalone | number | If this is `1` , the response will be a complete HTML document, including CSS, that can stand on its own. Otherwise it will be an HTML fragment. Default: false. HTML-only. |

## Expressions at specific points in time

Works may be amended and change over time. You can fetch different amended versions of a work by specifying the language and date in the FRBR URI of the request.

The available points in time of a work are listed in the `points_in_time` field of the JSON description of the work. Each point in time includes a date and a list of expressions available at that date, one for each available language.

To fetch the very first expression of a work, use `frbr-uri/:language@`, for example: `/akn/za/act/1998/5/eng@`.

To fetch a specific point in time, use `frbr-uri/:language@:date`, for example: `/akn/za/act/1998/5/eng@2014-01-17`.

To fetch the most recent point in time at or before a specific date, use `frbr-uri/:language::date`, for example `/akn/za/act/1998/5/eng:2014-01-17`.

{% hint style="info" %}
The `.format` part of the FRBR URI is placed after the `@YYYY-MM-DD` part.
{% endhint %}

{% hint style="info" %}
If you use `@` to specify a particular date and the API doesn't have a version at exactly that date, it will return a 404 response. If you need the expression of the work closest to a particular date, use `:` instead.
{% endhint %}

### Date formats for specific points in time

| Date Format   | Meaning                                                                                          | Example Expression FRBR URI          |
| ------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------ |
| `@`           | Very first expression of a work.                                                                 | `/akn/za/act/1998/55/eng@`           |
| `@YYYY-MM-DD` | Expression at the specific date.                                                                 | `/akn/za/act/1998/55/eng@2014-01-17` |
| `:YYYY-MM-DD` | Most recent expression at or before a date.                                                      | `/akn/za/act/1998/55/eng:2015-01-01` |
| (none)        | The most recent expression at or before today's date. Equivalent to using `:` with today's date. | `/akn/za/act/1998/55/eng`            |


# Commencements

Get the details of commencement events for an expression.

The commencement events for a work expression detail how different parts of the work expression have commenced over time.

## Get the commencements for a work expression

{% openapi src="/files/4kNXPoVcclCo2986qOST" path="/v3/{frbr\_uri}/commencements" method="get" %}
[Laws.Africa Content API 2024-04-23.yaml](https://4163728571-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LrrJ5L0RJ3goKWzqKVC%2Fuploads%2FYkGoMScj3txq3sbsbbwo%2FLaws.Africa%20Content%20API%20\(v3\)%20\(2\).yaml?alt=media\&token=7476d4cb-8aff-4c63-8ec8-44782e977024)
{% endopenapi %}


# Embedded images

Fetch metadata and files for images embedded in the content of a work expression.

Some documents include images. The media API includes metadata for these images, and provides a mechanism to fetch the images from the API so that you can store them and serve them to your users when they view the content.

## List media files for a work expression

{% openapi src="/files/4kNXPoVcclCo2986qOST" path="/v3/{frbr\_uri}/media" method="get" %}
[Laws.Africa Content API 2024-04-23.yaml](https://4163728571-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LrrJ5L0RJ3goKWzqKVC%2Fuploads%2FYkGoMScj3txq3sbsbbwo%2FLaws.Africa%20Content%20API%20\(v3\)%20\(2\).yaml?alt=media\&token=7476d4cb-8aff-4c63-8ec8-44782e977024)
{% endopenapi %}

## Download a media file

{% openapi src="/files/4kNXPoVcclCo2986qOST" path="/v3/{frbr\_uri}/media/{filename}" method="get" %}
[Laws.Africa Content API 2024-04-23.yaml](https://4163728571-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LrrJ5L0RJ3goKWzqKVC%2Fuploads%2FYkGoMScj3txq3sbsbbwo%2FLaws.Africa%20Content%20API%20\(v3\)%20\(2\).yaml?alt=media\&token=7476d4cb-8aff-4c63-8ec8-44782e977024)
{% endopenapi %}


# Publication document

Fetch the details of the original publication document for a work.

The original publication document is available for most works. This is the primary source document for the work, such as the official government gazette. It is always a PDF.

{% hint style="info" %}
The original publication document is different to the PDF version of a work expression.

* The **PDF version** is a PDF version of the XML content of the document that is generated automatically.
* The **publication document PDF** is a copy of the original publication document, which may be a scanned PDF. It is usually only the original version of the document and does not contain amendment information.
  {% endhint %}

## Download the publication document for a work

The full URL to download the publication document is part of the `publication_document` field in the details of the work.

{% openapi src="/files/4kNXPoVcclCo2986qOST" path="/v3/{frbr\_uri}/media/publication/{filename}" method="get" %}
[Laws.Africa Content API 2024-04-23.yaml](https://4163728571-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LrrJ5L0RJ3goKWzqKVC%2Fuploads%2FYkGoMScj3txq3sbsbbwo%2FLaws.Africa%20Content%20API%20\(v3\)%20\(2\).yaml?alt=media\&token=7476d4cb-8aff-4c63-8ec8-44782e977024)
{% endopenapi %}


# Table of Contents

Fetching the Table of Contents for an expression.

You can get a description of the table of contents (TOC) of a work. This includes the chapters, parts, sections and schedules that make up the legislation.

{% content-ref url="/pages/-LrrqlH6pyLthTgiKZs0" %}
[How to use the Table of Contents API](/content-api/how-to-use-the-table-of-contents-api)
{% endcontent-ref %}

## Get the Table of Contents for an expression

{% openapi src="/files/4kNXPoVcclCo2986qOST" path="/v3/{frbr\_uri}/toc" method="get" %}
[Laws.Africa Content API 2024-04-23.yaml](https://4163728571-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LrrJ5L0RJ3goKWzqKVC%2Fuploads%2FYkGoMScj3txq3sbsbbwo%2FLaws.Africa%20Content%20API%20\(v3\)%20\(2\).yaml?alt=media\&token=7476d4cb-8aff-4c63-8ec8-44782e977024)
{% endopenapi %}

## Individual parts, chapters and sections

You can use the `url` field from an item in the Table of Contents to fetch the details of just that item in XML or HTML.

## Content for a single part, chapter or section

<mark style="color:blue;">`GET`</mark> `https://api.laws.africa/v3/:frbr-uri/:toc-item.:format`

Get the content of a particular Table of Contents item.

#### Path Parameters

| Name     | Type   | Description                           |
| -------- | ------ | ------------------------------------- |
| frbr-uri | string | The full FRBR URI for the expression. |
| toc-item | string | The Table of Contents item to fetch.  |
| format   | string | Response format: XML or HTML.         |

{% tabs %}
{% tab title="200 The HTML or XML of the requested item." %}

```markup
<section class="akn-section" id="section-9" data-id="section-9"><h3>9. The rescue of stray dogs</h3>
<section class="akn-paragraph akn--no-indent" id="section-9.paragraph-0" data-id="section-9.paragraph-0">
<span class="akn-content"><span class="akn-p">A <span class="akn-term" data-refersTo="#term-person" id="trm257" data-id="trm257">person</span> who rescues a stray <span class="akn-term" data-refersTo="#term-dog" id="trm258" data-id="trm258">dog</span> shall report the date and time of the rescue and a description of the <span class="akn-term" data-refersTo="#term-dog" id="trm259" data-id="trm259">dog</span> to the <span class="akn-term" data-refersTo="#term-Council" id="trm260" data-id="trm260">Council</span> within twenty four hours.</span></span></section></section>
```

{% endtab %}
{% endtabs %}


# Timeline

Fetch the timeline description for a work expression.

The timeline for a work includes details of publication, amendment, commencement, consolidation and repeal events.

## Get the timeline for an expression

{% openapi src="/files/4kNXPoVcclCo2986qOST" path="/v3/{frbr\_uri}/timeline" method="get" %}
[Laws.Africa Content API 2024-04-23.yaml](https://4163728571-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LrrJ5L0RJ3goKWzqKVC%2Fuploads%2FYkGoMScj3txq3sbsbbwo%2FLaws.Africa%20Content%20API%20\(v3\)%20\(2\).yaml?alt=media\&token=7476d4cb-8aff-4c63-8ec8-44782e977024)
{% endopenapi %}


# Taxonomy topics

Taxonomies are used to classify works.

Taxonomies are used to categorise and group works. Taxonomies are made up of topics that form a tree structure. Each topic has a unique `slug` which identifies the topic.

A work may be associated with zero, one or many taxonomy topics.

## List taxonomy topics

{% openapi src="/files/4kNXPoVcclCo2986qOST" path="/v3/taxonomy-topics" method="get" %}
[Laws.Africa Content API 2024-04-23.yaml](https://4163728571-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LrrJ5L0RJ3goKWzqKVC%2Fuploads%2FYkGoMScj3txq3sbsbbwo%2FLaws.Africa%20Content%20API%20\(v3\)%20\(2\).yaml?alt=media\&token=7476d4cb-8aff-4c63-8ec8-44782e977024)
{% endopenapi %}

## Get a taxonomy topic

{% openapi src="/files/4kNXPoVcclCo2986qOST" path="/v3/taxonomy-topics/{slug}" method="get" %}
[Laws.Africa Content API 2024-04-23.yaml](https://4163728571-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LrrJ5L0RJ3goKWzqKVC%2Fuploads%2FYkGoMScj3txq3sbsbbwo%2FLaws.Africa%20Content%20API%20\(v3\)%20\(2\).yaml?alt=media\&token=7476d4cb-8aff-4c63-8ec8-44782e977024)
{% endopenapi %}

## List work expressions tagged with a taxonomy topic

{% openapi src="/files/4kNXPoVcclCo2986qOST" path="/v3/taxonomy-topics/{slug}/work-expressions" method="get" %}
[Laws.Africa Content API 2024-04-23.yaml](https://4163728571-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LrrJ5L0RJ3goKWzqKVC%2Fuploads%2FYkGoMScj3txq3sbsbbwo%2FLaws.Africa%20Content%20API%20\(v3\)%20\(2\).yaml?alt=media\&token=7476d4cb-8aff-4c63-8ec8-44782e977024)
{% endopenapi %}


# Enrichment datasets

Enrichment datasets add additional detail to provisions of a work.

The provisions (chapters, sections, paragraphs, etc.) of a work can be enriched with additional information. This information is stored separately to the content of the provision.

{% hint style="info" %}
Explore the tutorial for working with enrichment datasets.

[Module 2: Enrichments and interactivity](/tutorials/module-2-enrichments-and-interactivity)
{% endhint %}

These enrichments are grouped into **enrichment datasets**. An enrichment dataset contains multiple enrichments for multiple works.

An enrichment dataset has a **root taxonomy topic**. This is the root of the taxonomy tree that the enrichment dataset uses. Provisions enriched by the dataset can be tagged with a taxonomy topic that is part of the enrichment dataset's taxonomy tree.

An enrichment is made up of:

* the work being enriched
* the eId of the provision being enriched
* the enrichment data:
  * a topic in the taxonomy topic tree associated with the enrichment dataset

{% hint style="info" %}
An enrichment applies to a provision across all expressions of a work.
{% endhint %}

## List enrichment datasets

{% openapi src="/files/4kNXPoVcclCo2986qOST" path="/v3/enrichment-datasets" method="get" %}
[Laws.Africa Content API 2024-04-23.yaml](https://4163728571-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LrrJ5L0RJ3goKWzqKVC%2Fuploads%2FYkGoMScj3txq3sbsbbwo%2FLaws.Africa%20Content%20API%20\(v3\)%20\(2\).yaml?alt=media\&token=7476d4cb-8aff-4c63-8ec8-44782e977024)
{% endopenapi %}

## Get details of an enrichment dataset

{% openapi src="/files/4kNXPoVcclCo2986qOST" path="/v3/enrichment-datasets/{id}" method="get" %}
[Laws.Africa Content API 2024-04-23.yaml](https://4163728571-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LrrJ5L0RJ3goKWzqKVC%2Fuploads%2FYkGoMScj3txq3sbsbbwo%2FLaws.Africa%20Content%20API%20\(v3\)%20\(2\).yaml?alt=media\&token=7476d4cb-8aff-4c63-8ec8-44782e977024)
{% endopenapi %}


# Changelog

Changes to the Laws.Africa API.

{% hint style="info" %}
Version V2 of the API is deprecated and will be disabled during 2024. Please migrate to V3 of the API.
{% endhint %}

## New in V3

* The `/v3/taxonomy-topics/<slug>/work-expressions` URL is new. It lists work expressions under a taxonomy topic.

## Differences between V2 and V3

There are only minor changes between V2 and V3 of the Content API.

* In V3, the `commencements` field for work expressions is removed and is available at its own URL `/v3/<frbr-uri>/commencements.json` . This was done for performance reasons because for some works `commencements` is very large, but the data is not always used.
* The taxonomy topics URL has moved from `/v2/taxonomy_topics` in V2 to `/v3/taxonomy-topics` in V3.


# Content API legislation reader

This advanced tutorial guides you through building a small Django application that lets a user read and interact with legislation loaded from the Laws.Africa Content API.

The functionality includes:

* Fetching and storing legislation from the Laws.Africa Content API
* Listing legislation
* A legislation detail page with a Table of Contents and well-styled text
* Interactivity and enrichments using the Laws.Africa Law Widgets

The tutorial is divided into three modules:

1. [Module 1](/tutorials/module-1-build-a-legislation-reader): building the app basics; data modelling; fetching and storing data; listing legislation; reading and styling legislation
2. [Module 2](/tutorials/module-2-enrichments-and-interactivity): basic and advanced enrichments and interactivity
3. [Module 3](/tutorials/module-3-text-extraction-for-search-and-analysis): extracting text from Akoma Ntoso XML for full-text search and machine learning

The full code for the Django app is available at <https://github.com/laws-africa/legislation-reader>

[Let's get started](/tutorials/module-1-build-a-legislation-reader)!


# Module 1: Build a legislation reader

Building a basic Django app to display legislation fetched from the Laws.Africa Content API.

In this module we'll cover the following:

* Setting up the basic Django app
* Database models
* Working with the Laws.Africa Content API
* Listing works
* Displaying the content of a document
* Basic interactivity

We'll be following the [Django tutorial](https://docs.djangoproject.com/en/3.2/intro/tutorial01/) for setting up an app, but we'll build a legislation reader.

At the end of the module, you should have a working legislation reader app with a legislation listing page and detail pages for each piece of legislation.

We will use:

* Django – for storing and displaying the data
* Python – for extracting data from the API
* Law widgets – a library for styling and working with interactive elements on the page

The complete working code is available in GitHub at <https://github.com/laws-africa/legislation-reader>.


# Introductory concepts

Some key details before we build our legislation app.

### In this section

* Works and Expressions
* FRBR URIs

### Works and expressions

<figure><img src="/files/2KaJg66d7G0h4RoTNRgC" alt=""><figcaption></figcaption></figure>

A **work** is a piece of legislation.

It *does* include all the metadata concerning publication and other lifecycle events (commencements, amendments, and repeals).

It *does not* include the text and other content, because there can be more than one version of the content (depending on the language and date).

An **expression** is the content of a work, in a given language and at a given date.

A work can have multiple expressions. Most of the metadata on an expression is inherited from its work.

One key difference between a work and an expression is that an expression can have a different title from the work's, according to its language and date.

The language and date belong to the expression only.

{% hint style="info" %}
Read more about the terminology used by Laws.Africa in our [Terminology Guide](https://docs.laws.africa/getting-started/terminology-guide).
{% endhint %}

#### FRBR URI essentials

An FRBR URI uniquely identifies a work and an expression.

The FRBR URI is created using some core metadata of the work and expression.

Work-level FRBR URI example:

> `/akn/za/act/1996/93`

Because an expression inherits these core pieces of metadata from its work, an expression-level FRBR URI is the parent URI, plus the language and the date of the expression.

Expression-level FRBR URI examples:

> `/akn/za/act/1996/93/eng@1996-11-22`
>
> `/akn/za/act/1996/93/eng@2000-08-01`

In the above example, the expressions are the English-language versions, from 22 November 1996 onwards, and from 1 August 2000 onwards, of a South African national Act, 93 of 1996. The work is Act 93 of 1996, which can and does have multiple expressions.

While the work's title is the National Road Traffic Act, the expressions may have different titles. This is more common in the case of different language versions, although some pieces of legislation are renamed over time. In those cases, the older expressions will have the old title, and the work and the newer expressions will have the new one.


# Create a basic Django app

## In this section

* Setup a Python environment
* Create a basic Django app

## Create a basic Django app

We will build our example application using Django. There is nothing special about using Django, you can easily build an equivalent application using another language and framework.

Follow the first steps for setting up a Django app using the [Django tutorial](https://docs.djangoproject.com/en/3.2/intro/tutorial01/), using `legislation` and `reader` for your project and app, respectively.

{% hint style="warning" %}
Ignore the instruction to create a `urls.py` file in your `reader` app; we'll cover these in [Expression detail page](/tutorials/module-1-build-a-legislation-reader/expression-detail-page).
{% endhint %}

Here is a summary of the commands to run:

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install django==3.2
pip install requests
django-admin startproject legislation
cd legislation

python manage.py startapp reader
# see Create database models
python manage.py makemigrations reader
python manage.py migrate

# see Fetching the data
python manage.py ingest_capetown_bylaws <YOUR_AUTH_TOKEN>
python manage.py runserver
```

We'll use the default settings, adding `legislation` and `reader` to the the list of installed apps:

{% code title="settings.py" %}

```python
INSTALLED_APPS = [
    'legislation',
    'reader',
    'django.contrib.admin',
    ...
]
```

{% endcode %}

Instead of creating objects manually to play with, as they do in the [Django tutorial](https://docs.djangoproject.com/en/3.2/intro/tutorial02/#playing-with-the-api), we'll ingest some live data from the publicly available Indigo API in [Fetching the data](/tutorials/module-1-build-a-legislation-reader/fetching-the-data).

We won't cover the [Django Admin](https://docs.djangoproject.com/en/3.2/intro/tutorial02/#introducing-the-django-admin) in this tutorial, but you might find it useful.


# Create database models

Create models for storing data in the database.

## In this section

* Create the Django models for storing data in the database

## Creating the database models

We're going to create a **Work** model and an **Expression** model. Both models are uniquely identified by their respective **FRBR URIs**.

{% hint style="info" %}
See [Introductory concepts](/tutorials/module-1-build-a-legislation-reader/introductory-concepts) for background on **Works**, **Expressions** and **FRBR URI**s.
{% endhint %}

We'll store all the raw metadata in a single `metadata` JSON field. We'll also extract certain fields from that metadata and store that information separately, so we can query it more easily.

{% code title="models.py" %}

```python
from django.db import models


class Work(models.Model):
    frbr_uri = models.CharField(max_length=512, unique=True)
    title = models.CharField(max_length=512)
    metadata = models.JSONField()

    class Meta:
        ordering = ['title']

    def __str__(self):
        return f'{self.title} ({self.frbr_uri})'


class Expression(models.Model):
    # the expression inherits most of its metadata from its work
    work = models.ForeignKey(Work, related_name='expressions', on_delete=models.PROTECT)
    # the expression's FRBR URI is more specific than its work's
    frbr_uri = models.CharField(max_length=512, unique=True)
    # the expression's title may differ from its work's
    title = models.CharField(max_length=512)
    # the expression has a language, date, and content, which its work doesn't have
    language_code = models.CharField(max_length=3)
    date = models.DateField()
    content = models.TextField(null=True, blank=True)
    toc_json = models.JSONField()

    class Meta:
        # latest first
        ordering = ['-date']

    def __str__(self):
        return f'{self.title} ({self.frbr_uri})'

```

{% endcode %}

{% hint style="info" %}
Note:

* The `frbr_uri` on both `Work` and `Expression` must be unique.
* One work can have multiple expressions, hence the ForeignKey from `Expression` to `Work`.
* Expressions require a work, so `PROTECT` any expressions if a work is deleted.
* Only key pieces of metadata needed for the legislation reader app have been specified on the `Work` model. The `metadata` field is used for storing the rest.
  {% endhint %}

You'll need to [make and run migrations](https://docs.djangoproject.com/en/3.2/intro/tutorial02/#activating-models) after adding these models.

```sh
python manage.py makemigrations
python manage.py migrate
```


# Fetching the data

Fetching and storing data from the Laws.Africa Content API.

## In this section

* Working with the Laws.Africa Content API
* Creating a management command to fetch data from the Content API

## Using the Laws.Africa Content API

If you haven't already, follow the instructions at the [Laws.Africa Developer Guide](https://developers.laws.africa/quick-start) for setting up an account and getting your API token.

Visit <https://api.laws.africa/v3/akn/za-cpt/.json> to see the data that we'll be ingesting. Log into your Laws.Africa if necessary.

The <https://api.laws.africa/v3/akn/za-cpt/.json> endpoint returns a list of all the by-laws for the City of Cape Town, in South Africa. Each **work** is a by-law.

The items in the `results` array are the **most recent expression** of each by-law. Each item in the array has the complete information of both the expression and the work.

### Multiple points-in-time

Legislation changes over time (also called "points in time"), and may also be available in different languages. The content API returns the most recent (latest) available version, in the country's default language (English in this example).

Other points-in-time (**expressions**) may also be available. These are listed in the `points_in_time` attribute. It contains the dates and expressions available at those dates. There will only be different expressions at the same date if there are different languages. Each entry includes a URL with the full details of that particular expression.

* Each `point_in_time`'s `expression` has its own `url`, to which you can append `.json` to fetch the JSON details of the expression.
* If you don't append `.json`, it will return the XML of the expression.

## Create a management command

Rather than fetching and saving data on the command line, let's write a [Django management command](https://docs.djangoproject.com/en/3.2/howto/custom-management-commands/) that we can run from inside the app to fetch the data from the Laws.Africa Content API.

This command should do the following:

* Start at the <https://api.laws.africa/v3/akn/za-cpt/.json> endpoint and work with the `results` list.
  * The API token needs to be provided each time an API call is made.
  * The results may be paginated when getting all works in a place, so it's important to check for `next` in the response.
* For each result, create a `Work` object in the database using the data in the result.
  * In the example below, we use the `update_or_create` method, in case a `Work` object with the given FRBR URI already exists in the database. This allows us to run the command multiple times. You may wish to simply use `create`.
* Next, create the relevant `Expression` objects, with the related work being the one that has already been created for the current result.
  * A work can have multiple expressions, or no expressions.
  * You need to look at the list of `expressions` inside each entry in the `points_in_time` list to get the expression details.
  * The metadata for each expression is listed in the place's `results`, but the content of each expression and its table of contents require separate API calls:
    * For the HTML content, append `.html` to the `url` for the expression.
    * For the table of contents, append `/toc.json` to the `url` for the expression.

<figure><img src="/files/m9Wpg6PAVzTzqlFz8FhR" alt=""><figcaption></figcaption></figure>

Create the `management/commands/` directories inside `reader`:

```
reader/
    management/
        __init__.py
        commands/
            __init__.py
            ingest_capetown_bylaws.py
```

{% code title="ingest\_capetown\_bylaws.py" %}

```python
import requests

from django.core.management.base import BaseCommand

from reader.models import Work, Expression


class Command(BaseCommand):
    help = 'Ingest Cape Town By-laws'
    api_url = 'https://api.laws.africa/v3/akn/za-cpt/.json'
    api_token = None

    def add_arguments(self, parser):
        parser.add_argument('api_token', type=str)

    def handle(self, *args, **options):
        self.api_token = options['api_token']
        url = self.api_url

        # handle paginated results
        while url:
            resp = self.call_url_with_token(url).json()

            for result in resp.get('results', {}):
                self.create_work_and_expressions(result)

            url = resp['next']  # this is always present, but may be null

    def create_work_and_expressions(self, data):
        # for each result, create the Work object
        self.stdout.write(self.style.NOTICE(f"Creating or updating a Work for {data['frbr_uri']}"))
        work, new = Work.objects.update_or_create(
            frbr_uri=data['frbr_uri'],
            defaults={
                'title': data['title'],
                'metadata': data
            }
        )
        self.stdout.write(self.style.NOTICE(f'    Work {"created" if new else "updated"}: {work}'))

        # for each work, create the relevant Expression objects
        for date in data['points_in_time']:
            for expression in date['expressions']:
                self.create_expression(expression, work)

    def create_expression(self, data, work):
        self.stdout.write(self.style.NOTICE(f"Creating or updating an Expression for {data['expression_frbr_uri']}"))
        expression, new = Expression.objects.update_or_create(
            work=work,
            frbr_uri=data['expression_frbr_uri'],
            defaults={
                'title': data['title'],
                'language_code': data['language'],
                'date': data['expression_date'],
                'content': self.call_url_with_token(f"{data['url']}.html").content.decode('utf-8'),
                'toc_json': self.call_url_with_token(f"{data['url']}/toc.json").json(),
            }
        )
        self.stdout.write(self.style.NOTICE(f'    Expression {"created" if new else "updated"}: {expression}'))

    def call_url_with_token(self, url):
        self.stdout.write(self.style.NOTICE(f'Making a call to: {url}'))
        resp = requests.get(url, headers={'Authorization': f'token {self.api_token}'})
        resp.raise_for_status()
        self.stdout.write(self.style.NOTICE(f'    Response: {resp.status_code}'))
        return resp

```

{% endcode %}

This command takes your API key as a parameter and stores the content from the API in the database:

```bash
python manage.py ingest_capetown_bylaws <YOUR_AUTH_TOKEN>
```


# Work listing page

Add a page to list works.

## In this section

* Adding a view and template to list all works
* How to link to an expression
* Getting the latest expression for a work

## Work listing view

On our app's homepage, we're going to list all the **works**.

Let's create the View code to handle this in `views.py`. We'll use Django's generic `ListView` view which does all the work for us.

{% code title="views.py" %}

```python
from django.views.generic import ListView
from .models import Work

class WorkListView(ListView):
    model = Work
```

{% endcode %}

Now create the matching template. Django will automatically look for a file called `templates/reader/work_list.html`:

{% code title="work\_list.html" %}

```html
<html>
<head><title>All my works</title></head>
<body>
  <h1>Cape Town By-laws</h1>

  <table>
    {% for work in work_list %}
      <tr>
        <td>{{ work.title }}</td>
        <td>{{ work.frbr_uri }}</td>
      </tr>
    {% endfor %}
  </table>
</body>
```

{% endcode %}

Finally, let's add this view `legislation/urls.py`:

{% code title="urls.py" %}

```python
from django.urls import path
from reader.views import *

urlpatterns = [
    path('', WorkListView.as_view(), name="home"),
]
```

{% endcode %}

Now we need to run our server:

```
python manage.py runserver
```

Visit <http://localhost:8000> and see you list of by-laws!

We now have a basic listing page that shows the titles and FRBR URIs of all the works.

How can we create a link to read the content of a by-law? If we change the table to make the title a link, what exactly are we linking to?

## Linking to an expression

Recall that a work may have multiple expressions. For example, there could be different languages and different amended versions of a work.

When we link to the content of a work, which expression are we linking to?

For our app, we want to link to the **latest version** of a work. We're also going to add a `DEFAULT_LANGUAGE_CODE` setting for the site. We'll look for expressions that are in that language first, and then fall back to any language.

Let's add a helper method `latest_expression` to the Work model to help us fetch the latest expression for the work.

{% code title="models.py" %}

```python
from django.db import models
from django.conf import settings

default_language_code = settings.READER_APP_SETTINGS['DEFAULT_LANGUAGE_CODE']


class Work(models.Model):
    frbr_uri = models.CharField(max_length=512, unique=True)
    title = models.CharField(max_length=512)
    metadata = models.JSONField()

    def __str__(self):
        return f'{self.title} ({self.frbr_uri})'

    def default_expression(self):
        # first, try to get the latest expression in the default language
        expression = self.expressions.filter(language_code=default_language_code).first()
        # otherwise, get the latest expression in any other language
        if not expression:
            expression = self.expressions.first()

        return expression
```

{% endcode %}

Let's add the new `DEFAULT_LANGUAGE_CODE` to `legislation/settings.py`:

{% code title="settings.py" %}

```python
# ...

READER_APP_SETTINGS = {
    'DEFAULT_LANGUAGE_CODE': 'eng',
}
```

{% endcode %}

Now we can update the template to link to the latest expression.

{% code title="work\_list.html" %}

```html
<html>
<head><title>All my works</title></head>
<body>
  <h1>Cape Town By-laws</h1>

  <table>
    {% for work in work_list %}
      <tr>
        <td>
          {% with work.default_expression as expr %}
            {% if expr %}
              <a href="{% url 'expression' expr.frbr_uri %}">{{ expr.title }}</a>
            {% else %}
              {{ work.title }}
            {% endif %}
          {% endwith %}
        </td>
        <td>{{ work.frbr_uri }}</td>
      </tr>
    {% endfor %}
  </table>
</body>
```

{% endcode %}

We need to add the view and URL configuration for the `expression` URL before these changes will work. We'll do that in the next section.


# Expression detail page

Add a document detail page.

## In this section

* Displaying the details and content of an expression

## Expression detail page

Now let's add an expression detail page that shows the content of a single expression of a work.

Add a new view to your `views.py`:

{% code title="views.py" %}

```python
from django.views.generic import ListView, DetailView
from .models import Work, Expression

class WorkListView(ListView):
    model = Work

class ExpressionDetailView(DetailView):
    model = Expression
    slug_field = 'frbr_uri'
    slug_url_kwarg = 'frbr_uri'
```

{% endcode %}

Add the new view to your `urls.py`:

{% code title="urls.py" %}

```python
from django.urls import path
from reader.views import *

urlpatterns = [
    path('', WorkListView.as_view(), name="home"),
    # the FRBR URI starts with a /
    path('expression<path:frbr_uri>', ExpressionDetailView.as_view(), name="expression"),
]
```

{% endcode %}

Now create a template called `templates/reader/expression_detail.html`. We'll start with just the title and the expression content.

{% code title="expression\_detail.html" %}

```html
<html>
<head>
  <title>{{ expression.title }}</title>
</head>
<body>
  <a href="{% url 'home' %}">Home</a>

  <h1>{{ expression.title }}</h1>

  <div>{{ expression.content|safe }}</div>
</body>
```

{% endcode %}

{% hint style="info" %}
If there are other expressions on the same work (different language or date), how would the user find them?
{% endhint %}

If you visit an expression detail page, you'll see the title and some ugly content. In the next section we'll add formatting to the content.


# Styling with Law Widgets

Adding styles to the document content.

## In this section

* Installing the Law Widgets web components library
* Using Law Widgets to add styles to document content
* Customising styles with CSS

## Law Widgets

Akoma Ntoso HTML documents don't look very good without styling. Laws.Africa's [Law Widgets](https://github.com/laws-africa/law-widgets/blob/main/core/README.md) is a collection of web components that make it simple to style the HTML provided by the Laws.Africa Content API.

Using Law Widgets is easy:

1. Add a reference to the Law Widgets javascript.
2. Use the law widgets in your HTML.

We'll use the [\<la-akoma-ntoso>](https://github.com/laws-africa/law-widgets/blob/main/core/src/components/akoma-ntoso/readme.md) widget to apply appropriate styles to the expression HTML in `expression_detail.html`.

{% code title="expression\_detail.html" %}

```html
<html>
<head>
  <title>{{ expression.title }}</title>
  <!-- add the law widgets javascript -->
  <script
    type="module"
    src="https://cdn.jsdelivr.net/npm/@lawsafrica/law-widgets@latest/dist/lawwidgets/lawwidgets.esm.js"
  ></script>
</head>
<body>
  <a href="{% url 'home' %}">Home</a>

  <h1>{{ expression.title }}</h1>

  <!-- use la-akoma-ntoso law widget to apply styles -->
  <la-akoma-ntoso frbr-expression-uri="{{ expression.frbr_uri }}">
    {{ expression.content|safe }}
  </la-akoma-ntoso>
</body>
```

{% endcode %}

If you refresh your expression page in your browser, you should now have a well-styled document.

<figure><img src="/files/Xp9UbclF0oyEz3E2T0Jo" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
You can also work directly with the Law Widget SCSS styles without using Law Widgets. See <https://github.com/laws-africa/law-widgets/tree/main/law-widget-styles> for more details.
{% endhint %}

### The frbr-expression-uri attribute

We included the **frbr-expression-uri** attribute on the `<la-akoma-ntoso>` element. If you inspect the `<la-akoma-ntoso>` element in your browser's inspector, you'll see that new attributes have been added automatically:

```html
<la-akoma-ntoso
  frbr-expression-uri="/akn/za-cpt/act/by-law/2014/control-undertakings-liquor/eng@2014-12-12"
  frbr-country="za"
  frbr-type="act"
  frbr-subtype="by-law"
  frbr-date="2014"
  frbr-number="control-undertakings-liquor"
  frbr-expression-date="2014-12-12"
  frbr-language="eng"
>...</la-akoma-ntoso>
```

These additional attributes are derived from the Expression FRBR URI. They allows Law Widgets (and you) to customise the applied styles depending on a country's legal tradition.

For example, in Namibia's legal tradition editorial comments are <mark style="color:green;">**green**</mark> and **bold**. This styling is applied based on CSS selectors that work on the FRBR attributes added above.

{% hint style="info" %}
The different legal tradition styles that Law Widgets supports are in the [traditions folder in the GitHub repo](https://github.com/laws-africa/law-widgets/tree/main/law-widget-styles/scss/mixins/traditions).
{% endhint %}

## Customising styles

You can easily apply your own CSS styles to the Akoma Ntoso HTML using regular CSS selectors.

For example, we can add a red border to all sections, except for Kenya (country code KE) where we can add a green border:

```css
la-akoma-ntoso .akn-section {
  border: 1px solid red;
}
la-akoma-ntoso[frbr-country=ke] .akn-section {
  border-color: green;
}
```

You can use different attribute-based CSS selectors to change styling for different document types, countries, localities and languages.


# Adding interactivity

Adding some basic interactivity using Law Widgets.

## In this section

* Add a searchable Table of Contents using Law Widgets
* Add interactivity with Law Widgets for defined terms and internal references

Now that we have a detail page showing the content of the documents, let's add some interactivity using Laws.Africa's [Law Widgets](https://github.com/laws-africa/law-widgets).

## Table of Contents

A searchable Table of Contents is important to make long legislative documents easier to navigate. There are two widgets you can use for the Table of Contents:

* [\<la-table-of-contents>](https://github.com/laws-africa/law-widgets/blob/main/core/src/components/table-of-contents/readme.md) - this is a basic Table of Contents tree.
* [\<la-table-of-contents-controller>](https://github.com/laws-africa/law-widgets/blob/main/core/src/components/table-of-contents-controller/readme.md) - this is a fully searchable and collapsible Table of Contents tree that builds on top of \<la-table-of-contents>.

We'll use la-table-of-contents-controller because it gives us rich functionality right out the box.

Let's divide our content section into two columns to make room for the Table of Contents in a sidebar on the left of the page.

{% code title="expression\_detail.html" %}

```html
<html>
<head>
  <title>{{ expression.title }}</title>
  <!-- add the law widgets javascript -->
  <script
    type="module"
    src="https://cdn.jsdelivr.net/npm/@lawsafrica/law-widgets@latest/dist/lawwidgets/lawwidgets.esm.js"
  ></script>
</head>
<body>
  <a href="{% url 'home' %}">Home</a>

  <h1>{{ expression.title }}</h1>

  <div style="display: flex">
    <aside style="flex: 1">
      <la-table-of-contents-controller
        items="{{ toc_json }}"
        style="position: sticky; top: 0; max-height: 100vh; overflow-y: auto;"
      ></la-table-of-contents-controller>
    </aside>

    <div style="flex: 3">
      <!-- use la-akoma-ntoso law widget to apply styles -->
      <la-akoma-ntoso frbr-expression-uri="{{ expression.frbr_uri }}">
        {{ expression.content|safe }}
      </la-akoma-ntoso>
    </div>
  </div>
</body>
```

{% endcode %}

We need to provide the `<la-table-of-contents-controller>` with the JSON Table of Contents data in the items attribute. This is the data that is provided by the Laws.Africa Content API.

Update the `views.py` to add turn the TOC information into JSON.

{% code title="views.py" %}

```python
import json

# ...

class ExpressionDetailView(DetailView):
    model = Expression
    slug_field = 'frbr_uri'
    slug_url_kwarg = 'frbr_uri'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['toc_json'] = json.dumps(self.object.toc_json['toc'])
        return context
```

{% endcode %}

The page now has an interactive table of contents.

## Popups for defined terms

We're going to use the [\<la-decorate-terms>](https://github.com/laws-africa/law-widgets/blob/main/core/src/components/decorate-terms/readme.md) Law Widget to add popups for defined terms that occur in a document.

<figure><img src="/files/NY0eqecmQymtWTlVAW4q" alt=""><figcaption></figcaption></figure>

Modify `expression_detail.html` to include the new widget anywhere on the page:

<pre class="language-html" data-title="expression_detail.html"><code class="lang-html">...
<strong>&#x3C;la-decorate-terms popup-definitions link-terms>&#x3C;/la-decorate-terms>
</strong>&#x3C;la-akoma-ntoso ...>...&#x3C;/la-akoma-ntoso>
...
</code></pre>

This widget will add term definition popups and links in the first `<la-akoma-ntoso>` element on the page. If there is more than one, you can target it specifically by providing an **akoma-ntoso** attribute that uses a CSS selector to identify the target element: `<la-decorate-terms akoma-ntoso="#akn-doc">`.

You can control the behaviour by adding or removing these attributes. Try changing them and refreshing the page to see what changes.

* `popup-definitions` - controls whether definitions of terms are shown in popups
* `link-terms` - controls whether references to terms are shown as clickable links.

## Popups for internal references

We're going to use the [\<la-decorate-internal-refs>](https://github.com/laws-africa/law-widgets/blob/main/core/src/components/decorate-internal-refs/readme.md) widget to make internal references more interactive. The widget will show a purple bookmark at an internal reference, and show the content of the target element when the user hovers over the reference.

<figure><img src="/files/fgTEycxDcwI3DfaN5D12" alt=""><figcaption></figcaption></figure>

Modify `expression_detail.html` to include the new widget:

{% code title="expression\_detail.html" %}

```html
...
<la-decorate-internal-refs popups flag></la-decorate-internal-refs>
<la-akoma-ntoso ...>...</la-akoma-ntoso>
...
```

{% endcode %}

This widget will add interactivity to internal references in the first `<la-akoma-ntoso>` element on the page. Like with `<la-decorate-terms>` , you can use the **akoma-ntoso** attribute to target a specific `<la-akoma-ntoso>` element using a CSS selector, such as `<la-decorate-internal-refs akoma-ntoso="#akn-doc">`.

You can control the behaviour by adding or removing these attributes. Try changing them and refreshing the page to see what changes.

* `popups` - controls whether reference target elements are shown in popups
* `flag` - controls whether internal references are decorated with a purple bookmark icon


# Staying up to date

Getting the latest data from the Content API.

## In this section

* Getting recently updated content from the Content API
* Tracking deleted items

## Knowing when content has changed

Legislation changes over time. As new works and expressions are added to the Laws.Africa Content API, our app needs to be kept up to date.

When we run our `ingest_capetown_bylaws.py` command, it would be useful to ignore any content that hasn't changed, and only process content that has changed.

The Laws.Africa Content API includes an attribute `updated_at` which is a timestamp of the most recent change to that work and expression. We can use that to ignore content that hasn't changed.

Here are two ways of doing this:

1. When processing content from the Content API, compare the new `updated_at` with the `updated_at` of the work or expression already in your database. Only process the update if the new `updated_at` is more recent than your existing version.
2. Keep a timestamp in your database of the last time you processed content from the API. When processing new content, import anything with a `updated_at` field that is more recent than the timestamp from your database.

## Knowing when content has been removed

Sometimes content will be removed from the Content API, although this is rare. This usually happens when the FRBR URI for a document has to be changed.

There is no way to query the Content API for items that have been deleted. Instead, do the following:

1. Fetch a list of all expression FRBR URIs from the Content API.
2. Fetch a list of all expression FRBR URIs in your database.
3. Compare the lists, and delete anything that is in your database, but is no longer included in the Content API.


# Module 2: Enrichments and interactivity

Enriching your documents and adding advanced interactivity.

In Module 1, we created a Django app to fetch, list and show works and expressions from the Laws.Africa Content API.

In Module 2, we will enrich legislation documents using Javascript and supporting data and add advanced interactivity to the app.

You can see the complete Django app in GitHub at <https://github.com/laws-africa/legislation-reader>.


# Basic enrichments

Adding and displaying simple enrichments.

## In this section

* Element eIds.
* Adding enrichments to parts of the document.
* Showing enrichments in the document gutter.

## Element eIds

One of the big benefits of Akoma Ntoso XML is that we can treat a document as data, not just text. This is possible because every element in an AKN document has a well-formed, unique ID called an **eId**. These eIds are also included in the HTML form of the document, as both the `id` and the `data-eid` attributes.

<figure><img src="/files/DcZC3TwbIrvjWX9qpAwt" alt=""><figcaption><p>The XML eId is included as both the id and data-eid elements in the corresponding HTML.</p></figcaption></figure>

We can associate extra data with an element's eId, and then show it along with the document.

## Enriching a document

In this section, we'll build some interactivity that allows the user to comment on any item in the document, using this eId attributes. We will show the comments in the gutter to the right of the document.

Let's add the Law Widgets [\<la-gutter>](https://github.com/laws-africa/law-widgets/blob/main/core/src/components/gutter/readme.md) widget to the page, and a floating button to add a comment. We use the `.la-akoma-ntoso-with-gutter` helper class to provide styles and spacing for gutter items.

Modify your `expression_detail.html` file:

{% code title="expression\_detail.html" %}

```html
<!-- ... -->

    <div style="flex: 3">
      <la-decorate-terms popup-definitions link-terms></la-decorate-terms>
      <la-decorate-internal-refs popups flag></la-decorate-internal-refs>

      <div class="la-akoma-ntoso-with-gutter">
        <!-- use la-akoma-ntoso law widget to apply styles -->
        <la-akoma-ntoso frbr-expression-uri="{{ expression.frbr_uri }}">
          {{ expression.content|safe }}
        </la-akoma-ntoso>

        <la-gutter id="gutter"></la-gutter>
      </div>
    </div>

    <button id="btn-comment" style="position: fixed; top: 10px; right: 10px;">Add comment...</button>

```

{% endcode %}

Now we're going to add some javascript to support the following functionality:

1. The user selects text anywhere in the document
2. The user clicks on the "Add comment..." button
3. The user is prompted to type in their comment
4. We get the best eId for the selection
5. We create a new `<la-gutter-item>` widget that has the comment text, and add it to the gutter.

Add this script block to the bottom of `expression_detail.html`:

{% code title="expression\_detail.html" %}

```html
<!-- ... -->

  <script>
    // add a comment on the selected text
    document.getElementById('btn-comment').addEventListener('click', (e) => {
      const sel = document.getSelection();
      if (sel && !sel.isCollapsed) {
        let node = sel.getRangeAt(0).commonAncestorContainer;
        // go from a text node to an element
        if (node.nodeType !== document.ELEMENT_NODE) {
          node = node.parentElement;
        }
        // get the nearest element with an eId
        node = node.closest('[data-eid]');
        if (node) {
          const eId = node.getAttribute('data-eid');
          const comment = prompt(`What's your comment on this section? (#${eId})`);
          if (comment) {
            const item = document.createElement('la-gutter-item');
            item.setAttribute('anchor', `#${eId}`);
            item.innerText = comment;
            document.getElementById('gutter').appendChild(item);
          }
        }
      }
    });
  </script>
</body>
```

{% endcode %}

The user can now link a comment to any portion of the document. This example is very simple, but you can see how you can link arbitrary data to the various portions of the document.

<figure><img src="/files/m6iTgvbMIHvEREtSJEfX" alt=""><figcaption></figcaption></figure>

Remember that the Table of Contents JSON object has the titles and eIds of all the hierarchical elements in the document. You can use this to build an interface to help your editors to annotate different parts of the document.

The `<la-gutter>` widget makes it easy to show a comment or other information alongside a part of the document. Add a `<la-gutter-item>` element to the `<la-gutter>` and set its `anchor` attribute to the eId of the targeted element.

The gutter positions the gutter item at the correct height of its matching anchor. It will do its best to lay out other gutter items so that they don't overlap.

Clicking on a gutter item activates it, and forces it to be shown alongside its anchor. Surrounding gutter items will move out the way.

Gutter items can be anything: text, images, buttons, or cards. How you style them is up to you.


# Advanced enrichments

Fetching enrichments from the Laws.Africa Enrichments API.

## In this section

* Working with the Laws.Africa Enrichments API
* Displaying API-based enrichments in a document

We'll be using the [Cape Town Liquor Trading by-law](https://openbylaws.org.za/akn/za-cpt/act/by-law/2014/control-undertakings-liquor/eng@2014-12-12) for these examples, which has the FRBR URI `/akn/za-cpt/act/by-law/2014/control-undertakings-liquor`.

## Enrichments API

The Laws.Africa Enrichments API provides access to the enrichment datasets. The API endpoint is <https://api.laws.africa/v3/enrichment-datasets/>.

An **enrichment** is additional data that is associated with an expression, but is not included in the text of the document. Laws.Africa groups common enrichments together as an **enrichment dataset**. Each enrichment dataset has a **taxonomy tree** that is used to "tag" (enrich) elements.

In this example we will use the "Tutorial" enrichment dataset provided by the Laws.Africa API.

* Name: Tutorial Enrichment Dataset
* Taxonomy tree:
  * Tutorial
    * Red
    * Green
    * Blue

An enrichment is linked to a single provision (element) in a work, identified with the provision's **eId**.

For example, we can enrich Section 4(1) of the Cape Town Liquor Trading by-law with the taxonomy tag "Red":

* work: `/akn/za-cpt/act/by-law/2014/control-undertakings-liquor`
* provision: `sec_4__subsec_1`
* taxonomy topic: `red`

<figure><img src="/files/7wkSi63ENMMwzelJlY8r" alt=""><figcaption><p>Visualisation of section 4(1) enriched with "Red"</p></figcaption></figure>

{% hint style="info" %}
The enrichment is linked to a work, not an expression. The same enrichment applies to all expressions of the work, including different language and date versions.
{% endhint %}

## Storing enrichments

We need to model enrichments to store them in our Django app's database.

{% code title="models.py" %}

```python
# ...

class EnrichmentDataset(models.Model):
    name = models.CharField(max_length=512, unique=True)
    taxonomy_tree = models.JSONField()


class ProvisionEnrichment(models.Model):
    dataset = models.ForeignKey(EnrichmentDataset, related_name='enrichments', on_delete=models.CASCADE)
    work = models.ForeignKey(Work, related_name='enrichments', on_delete=models.CASCADE)
    provision_id = models.CharField(max_length=512)
    taxonomy_topic = models.CharField(max_length=1024)
```

{% endcode %}

Now create new database migrations and run them to update the database:

```sh
python manage.py makemigrations
python manage.py migrate
```

Now let's create a new management command to fetch the Tutorial Dataset from the Enrichments API endpoint <https://api.laws.africa/v3/enrichment-datasets/1.json>.

Create a new file `reader/management/commands/ingest_enrichments.py`:

{% code title="ingest\_enrichments.py" %}

```python
import requests

from django.core.management.base import BaseCommand

from reader.models import Work, Expression, EnrichmentDataset, ProvisionEnrichment


class Command(BaseCommand):
    help = 'Ingest Enrichments'
    api_url = 'https://api.laws.africa/v3/enrichment-datasets/1.json'
    api_token = None

    def add_arguments(self, parser):
        parser.add_argument('api_token', type=str)

    def handle(self, *args, **options):
        self.api_token = options['api_token']
        resp = self.call_url_with_token(self.api_url).json()

        self.stdout.write(self.style.NOTICE(f"Creating or updating enrichment dataset {resp['name']}"))
        dataset, new = EnrichmentDataset.objects.update_or_create(
            name=resp['name'],
            taxonomy_tree=resp['taxonomy']
        )

        # clear out existing enrichments
        dataset.enrichments.all().delete()

        # for each enrichment, create the relevant object
        for enrichment in resp['enrichments']:
            work = Work.objects.filter(frbr_uri=enrichment["work"]).first()
            if work:
                self.stdout.write(self.style.NOTICE(f"    Enrichment created for {enrichment['work']} -- {enrichment['provision_id']}"))
                ProvisionEnrichment.objects.create(
                    work=work,
                    dataset=dataset,
                    provision_id=enrichment["provision_id"],
                    taxonomy_topic=enrichment["taxonomy_topic"]
                )
            else:
                self.stdout.write(self.style.WARNING(f"Work does not exist: {enrichment['work']}"))

    def call_url_with_token(self, url):
        self.stdout.write(self.style.NOTICE(f'Making a call to: {url}'))
        resp = requests.get(url, headers={'Authorization': f'token {self.api_token}'})
        resp.raise_for_status()
        self.stdout.write(self.style.NOTICE(f'    Response: {resp.status_code}'))
        return resp

```

{% endcode %}

You will need to provide it with your API token:

```bash
python manage.py ingest_enrichments TOKEN
```

## Displaying enrichments

We're going to show the enrichments in the gutter, just like we did for the basic enrichments. This time, however, we'll load them when the page loads.

Let's update the `expression_detail.html` file to replace the existing `la-gutter` as follows:

<pre class="language-markup" data-title="expression_detail.html"><code class="lang-markup"><strong># ...
</strong>
<strong>        &#x3C;la-gutter id="gutter">
</strong>          

            &#x3C;la-gutter-item anchor="#{{ enrichment.provision_id }}">{{ enrichment.taxonomy_topic }}&#x3C;/la-gutter-item>
          {% endfor %}
        &#x3C;/la-gutter>

</code></pre>

Now the enrichments (if any) will show in the gutter when the page loads.

The tutorial enrichment dataset only has enrichments for the FRBR URI `/akn/za-cpt/act/by-law/2014/control-undertakings-liquor`.

Try visiting <http://localhost/expression/akn/za-cpt/act/by-law/2014/control-undertakings-liquor/eng@2014-12-12> to view the enrichments in your local dataset.

<figure><img src="/files/WFfevH0gfWUoApT39dwa" alt=""><figcaption><p>Showing enrichments pulled from the enrichments dataset.</p></figcaption></figure>


# Advanced interactivity

Adjusting content styles based on enrichment information.

## In this section

* Use enrichment information to dynamically adjust the styles for displayed content.

## Highlighting tagged provisions

Our final piece of interactivity will add an option to highlight provisions that have been tagged with a particular enrichment.

We're going to build the following:

* A dropdown that lets the user choose one of the enrichments.
* When an item is chosen, dim all the text of the document, except those portions that have the chosen enrichment.

Add a new button beneath the `btn-comment` button in `expression_detail.html`:

{% code title="expression\_detail.html" %}

```markup
# ...
    <button id="btn-comment" style="position: fixed; top: 10px; right: 10px;">Add comment...</button>
    <select id="select-enrichments" style="position: fixed; top: 40px; right: 10px;">xx</select>
```

{% endcode %}

Add a new javascript block to populate the dropdown from the items in the gutter, and handles the interactivity when an item is chosen.

{% code title="expression\_detail.html" %}

```html
<script>
  const select = document.getElementById('select-enrichments');

  // add a mutation observer to take action when items are added to the gutter
  function updateHighlightOptions () {
    // empty the options
    while (select.firstChild) select.removeChild(select.firstChild);
    // add the empty option
    const option = document.createElement('option');
    option.innerText = "(off)";
    option.value = "";
    select.appendChild(option);

    // unique text of gutter items
    const items = [...new Set([...document.querySelectorAll('la-gutter-item')].map((item) => item.innerText))];
    items.sort();

    // add the items as options to the dropdown
    for (const item of items) {
      const option = document.createElement('option');
      option.innerText = item;
      option.value = item;
      select.appendChild(option);
    }
  }

  const observer = new MutationObserver(() => {
    window.setTimeout(updateHighlightOptions, 500);
  });
  observer.observe(document.getElementById('gutter'), { childList: true });
  updateHighlightOptions();

  // when the select changes, highlight the provisions that match by adding a class to those provisions
  select.addEventListener('change', (e) => {
    const doc = document.getElementsByTagName('la-akoma-ntoso')[0];

    // remove any existing highlights
    for (const provision of document.querySelectorAll('.highlight')) {
      provision.classList.remove('highlight');
    }

    const selected = e.target.value;
    if (selected) {
      // turn on highlighting
      doc.classList.add('highlighting');

      // mark each highlighted element
      for (const item of document.querySelectorAll('la-gutter-item')) {
        if (item.innerText === selected) {
          const anchor = item.getAttribute('anchor');
          const provision = doc.querySelector(`[id="${anchor.slice(1)}"]`);
          if (provision) {
            provision.classList.add('highlight');
          }
        }
      }
    } else {
      // turn off highlighting
      doc.classList.remove('highlighting');
    }
  });
</script>
```

{% endcode %}

Finally, let's add the necessary styling to handle the new `.highlight` and `.highlighting` classes.

Add this block in the `<head>` tag of `expression_detail.html`.

{% code title="expression\_detail.html" %}

```markup
  <style>
    .highlighting {
      color: lightgrey;
    }

    .highlighting .highlight {
      color: black;
      background-color: palegreen;
    }
  </style>
```

{% endcode %}

Load the page and choose an item from the dropdown:

<figure><img src="/files/fCE1ha4zA7e4EdURI5bp" alt=""><figcaption></figcaption></figure>


# Module 3: Text extraction for search and analysis

How to extract text from Akoma Ntoso XML documents for use in full-text search indexing and machine learning analysis.

In this module we'll cover the following:

* Why extracting text is important
* The basics of extracting text
* Extracting text for specific document portions or provisions (eg. chapters, sections)

We will use:

* Python and the [lxml](https://lxml.de/) XML library

The full code for this module is available on Google Colab: <https://colab.research.google.com/drive/1BEPG5abvOKoCB5zmHWDUBNNPIof1cE0M>


# Why extracting text is important

Why would you want to extract text from an Akoma Ntoso XML document?

Akoma Ntoso XML documents and the resulting HTML documents contain rich, structured information. While this structure is important for displaying, formatting and analysis of the document, sometimes it's important to work with just the text content.

Extracting text content from a structure Akoma Ntoso XML or HTML document is useful for use cases such as:

* Indexing into Elasticsearch or other search engines to support full-text search
* Calculating [embeddings](https://en.wikipedia.org/wiki/Word_embedding) for use with machine learning models such as Large-Language Models (LLMs)
* Natural-language processing and analysis (NLP)

The text content we need depends on our use case. We may need to ignore certain text content such as metadata, editorial comments, embedded text, headings and portion numbers.

To extract text content from a document, we process the XML and use XML-based queries to extract just the text we're interested in.

In this module we'll explore how to extract text content for different uses cases.


# Basics of text extraction

Basic methods of extracting text from an Akoma Ntoso XML document.

## In this section

* parsing Akoma Ntoso XML using Python's LXML library
* extracting all text in a naive way
* ignoring editorial remarks

## XML vs HTML

Extracting text from Akoma Ntoso XML is the preferred process. You can also extract text from the HTML version of an AKN document, but it's a little different because HTML is not as explicitly structured as XML.

We'll be using the [Cape Town Liquor Trading by-law](https://openbylaws.org.za/akn/za-cpt/act/by-law/2014/control-undertakings-liquor/eng@2014-12-12) for these examples.

## Parsing Akoma Ntoso XML

Let's fetch the raw XML from the Laws.Africa API.

```python
from urllib.request import Request, urlopen

TOKEN = "your-auth-token"
url = "https://api.laws.africa/v3/akn/za-cpt/act/by-law/2014/control-undertakings-liquor/eng/.xml"
request = Request(url, headers={"Authorization": f"Token {TOKEN}"})
# raw_xml is a bytes object, not a str
raw_xml = urlopen(request).read()
print(raw_xml[:100])
# b'<akomaNtoso xmlns="http://docs.oasis-open.org/legaldocml/ns/akn/3.0"><act ...
```

Now, parse the bytes in `raw_xml` into an XML tree with lxml.

```python
from lxml import etree
# parse the raw XML bytes (even though the method name is "fromstring")
root = etree.fromstring(raw_xml)
```

## Extracting all text content (naive approach)

If you look at the XML file in a browser (which formats it), you'll see that the XML contains a mixture of metadata, structure and text content.

<figure><img src="/files/vZQEoUxrUzwmJAKolBZh" alt=""><figcaption><p>Example of Akoma Ntoso XML, including metadata and text.</p></figcaption></figure>

The simplest way of extracting text is to ignore the structure and just extract all text nodes.

<pre class="language-python"><code class="lang-python"><strong># itertext() iterates over all text nodes in the entire document
</strong><strong>text = ' '.join(root.itertext())
</strong>print(text[:100])
# To provide for the control of undertakings selling liquor to the public including the control of tra
</code></pre>

This is quick and easy, but includes **all** text nodes, which may not be what we want:

* It includes text from all structural elements including headings, numbers, editorial comments, footnotes, quoted and embedded elements, tables, etc.
* It includes all portions of the document, including attachments such as schedules and appendixes.

## Ignoring editorial remarks

When legislation is edited or amended, sometimes editorial remarks are added. For example:

<pre class="language-xml"><code class="lang-xml">&#x3C;p eId="sec_6__subsec_4__p_1">
<strong>  &#x3C;remark status="editorial">
</strong><strong>    [subsection (4) deleted by section 1 of the &#x3C;ref href="/akn/za-cpt/act/by-law/2014/control-undertakings-liquor-amendment" eId="sec_6__subsec_4__p_1__ref_1">Amendment By-law, 2014&#x3C;/ref>]
</strong>  &#x3C;/remark>
&#x3C;/p>
</code></pre>

These are not substantive and not officially part of the legal text of the document. We usually want to exclude these remarks when extracting text for full-text search purposes.

To do this, we can use [XPath](https://en.wikipedia.org/wiki/XPath), a powerful mechanism of querying elements in XML documents.

{% hint style="info" %}
Learn more about XPath at these resources:

* [Zyte's XPath tutorial for web scraping](https://www.zyte.com/blog/an-introduction-to-xpath-with-examples/)
* [Using lxml and XPath to extract text in Python](https://lxml.de/tutorial.html#using-xpath-to-find-text)
  {% endhint %}

XPath is rich and expressive and we will only cover the basics for this example. There are two key ideas we need:

* A **namespace** lets us ignore parts of the document that aren't part of the Akoma Ntoso XML standard. The [official Akoma Ntoso XML namespace](https://docs.oasis-open.org/legaldocml/akn-core/v1.0/akn-core-v1.0-part2-specs.html) is `http://docs.oasis-open.org/legaldocml/ns/akn/3.0`
* The **query** which describes the elements we want from the XML document. In this case, our query will mean "text nodes that aren't part of `<remark>` elements".

```python
# The AKN namespace is the default one for this XML document,
# which is given by the None entry in the namespace map (nsmap).
# This is the same as ns = "http://docs.oasis-open.org/legaldocml/ns/akn/3.0"
ns = root.nsmap[None]

# This tells xpath that when we use the "a" alias, we mean the AKN namespace.
# It saves us from writing the full namespace in the xpath query.
nsmap = {"a": ns}

# query all the text nodes that don't have <remark> as an ancestor
text = ' '.join(root.xpath("//text()[not(ancestor::a:remark)]", namespaces=nsmap))
print(text[:100])
# To provide for the control of undertakings selling liquor to the public including the control of tra
```

Here's a brief explanation of what the components of the XPath query mean:

<table><thead><tr><th width="258">XPath</th><th>Meaning</th></tr></thead><tbody><tr><td><code>//</code></td><td>Any node at any point in the tree. Alternatively, <code>./</code> means nodes at the current element (which is <code>root</code> in this case), or <code>/</code> which means the root of the entire document.</td></tr><tr><td><code>text()</code></td><td>This matches text nodes.</td></tr><tr><td><code>[...]</code></td><td>This applies additional conditions to the text nodes being matched. The conditions must evaluate to true to be included in the resulting node set.</td></tr><tr><td><code>not(...)</code></td><td>This negates the condition inside the brackets.</td></tr><tr><td><code>ancestor::a:remark</code></td><td>The <code>ancestor::</code> means any ancestor element of the text node, and <code>a:remark</code> limits the ancestors to those that are <code>&#x3C;remark></code> elements in the Akoma Ntoso namespace.</td></tr></tbody></table>

## Ignoring other content

Other types of text content you may want to ignore, depending on your use case:

* Text of headings and sub-headings: `<heading>`, `<subheading>`, `<crossHeading>`
* The numbers of chapters, sections, etc.: `<num>`
* Quoted and embedded content: `<quotedStructure>`, `<embeddedStructure>`

Depending on your needs, you can adjust the XPath to include these additional elements by including them in the `not(...)` clause and separating them with `or`.

```python
# get text that isn't in a remark, heading or num
text = ' '.join(root.xpath(
  "//text()[not(ancestor::a:remark or ancestor::a:num or ancestor::a:heading)]",
  namespaces=nsmap))
```

In the next section, we'll explore how to extract text only for certain portions of the document, such as a particular section or chapter, or only table elements.


# Advanced text extraction

How to extract text from particular elements or portions of a document.

## In this section

* Extracting text from a specific section (or other portion) of a document
* Extracting text from specific elements, such as headings
* Why separating text with spaces is important

## Extracting text from a section

Sometimes it's useful to extract text only for specific portions of a document, such as a particular chapter or section.

Let's extract the text from Section 3 of the Cape Town Liquor By-law.

<figure><img src="/files/v2NkgSn76WGzACRWoy2F" alt=""><figcaption><p>Section 3's HTML.</p></figcaption></figure>

Section 3's XML is below. Even a short section that looks quite simple can have complex XML.

<figure><img src="/files/w8uXxdTtdaoyt41Gem09" alt=""><figcaption><p>Section 3's XML.</p></figcaption></figure>

We can use the **eId** attribute to find Section 3.

{% hint style="info" %}
The **eId** attribute is a unique identifier that appears on (almost) all elements in an Akoma Ntoso XML document. You can read more about how they are generated in the [Akoma Ntoso XML specification](https://docs.oasis-open.org/legaldocml/akn-nc/v1.0/os/akn-nc-v1.0-os.html#_Toc531692303).
{% endhint %}

Let's use a new xpath query to find the `<section>` element that has an `eId` of "`sec_3`".

```python
# get section 3 using its eId
sec_3 = root.xpath('//a:section[@eId="sec_3"]', namespaces=nsmap)[0]
```

The XPath query is the equivalent of the CSS selector `.section[eId=sec_3]` and means:

<table><thead><tr><th width="256">XPath</th><th>Meaning</th></tr></thead><tbody><tr><td><code>//a:section</code></td><td>Find all AKN section elements anywhere in the document</td></tr><tr><td><code>[@eId="sec_3"]</code></td><td>Filter the section elements to return only those whose <code>eId</code> attribute is <code>"sec_3"</code>.</td></tr></tbody></table>

Once you have a reference to Section 3, it is simple to extract just the text for the section using the techniques from the previous section of this module.

```python
# NOTE: ".//" not "//"
text = ' '.join(sec_3.xpath('.//text()[not(ancestor::a:remark)]', namespaces=nsmap))
text[:100]
# 3. General prohibition No  person  may  sell   liquor  to the public for on consumption or off consu
```

Note that the XPath query now starts with a dot `.//` - this is to indicate we want all text nodes starting at the current node (which is `sec_3`) and not at the root of the document. Try removing the `.` from `.//` above and see what the value of `text` is afterwards.

## Extracting text from specific elements

Sometimes it's useful to extract text only from specific elements. For example, text only in tables, chapters, headings or sub-paragraphs.

We can do this using a new XPath query. Let's extract text from only heading elements:

```python
# extract text from all headings
text = ' '.join(root.xpath('//a:heading//text()', namespaces=nsmap))
print(text)
# Definitions Application General prohibition On-consumption premises Off-consumption premises Applica
```

Note that we used `a:heading//text()` and not `a:heading/text()`. If we used `a:heading/text()` then we would not get text inside elements that are nested inside the heading, such as superscripts and subscripts inside `<sup>` and `<sub>`.

For example, consider this XML and XPath outputs.

```xml
<heading>Release of atmospheric CO<sub>2</sub>.</heading>
```

In HTML, this would be shown as: Release of atmospheric CO₂.

| XPath                 | Text                          | Comment                                                                    |
| --------------------- | ----------------------------- | -------------------------------------------------------------------------- |
| `//a:heading/text()`  | `Release of atmospheric CO.`  | Only text nodes that are **immediate children** of `heading` are included. |
| `//a:heading//text()` | `Release of atmospheric CO2.` | All text nodes that are **descendants** of `heading` are included.         |

Finally, let's extract the text from all headings, subheadings and cross-headings. There are two equivalent ways of doing this.

```python
# option 1 - separate options with | (meaning OR)
text = ' '.join(
  root.xpath('//a:heading//text()[not(ancestor::a:remark)]'
             '| //a:subheading//text()[not(ancestor::a:remark)]'
             '| //a:crossHeading//text()[not(ancestor::a:remark)]',
             namespaces=nsmap))

# option 2
text = ' '.join(
  root.xpath('//a:*[self::a:heading or self::a:subheading or self::a:crossHeading]'
             '//text()[not(ancestor::a:remark)]',
             namespaces=nsmap))
```

The first option joins separate XPath queries with the OR `|` operator.

The second option uses one XPath and conditions to match multiple types of elements.

## Separating text nodes with spaces

In all the examples so far, we have used `text = ' '.join(...)`. What is the `join` and why is it important?

The `' '.join(items)` takes all the elements in `items` and joins them together with a single space. It's the equivalent of the Javascript `items.join(' ')`.

It ensures that all the text nodes are separated with a space.

Why do we need these spaces? Look what happens if we extract the text from all headings but don't join them with a space.

<pre class="language-python"><code class="lang-python"><strong># BAD: extract headings without joining with a space
</strong><strong>text = ''.join(root.xpath('//a:heading//text()', namespaces=nsmap))
</strong>print(text[:100])
# DefinitionsApplicationGeneral prohibitionOn-consumption premisesOff-consumption premisesApplication
</code></pre>

Notice that there are spaces **within** the headings, but not **between** them.

A text node includes the spaces within the text, but does not add spaces between them. In the XML document, there are **no** spaces outside of the text nodes. All the XML is actually on one line. Only when the document is displayed by the browser as HTML, are the headings placed on lines and visual spacing is added. We must therefore add them ourselves.

{% hint style="warning" %}
It's important to separate text nodes with spaces so that we don't combine words together accidentally. This would negatively affect analysis and full-text search.
{% endhint %}


# Extracting text for analysis and machine learning

How to extract text from all portions of a document, for use in analysis or machine learning.

## In this section

* Using the Table of Contents to extract text recursively from an entire document
* Extracting text only from the lowest hierarchical element

## Extracting text from different portions of a document

Sometimes it's useful to break a piece of legislation down into its constituent portions (sections, paragraphs, sub-sections, etc.) and extract the text from each of those parts and work with them individually, rather than the entire document as a whole.

Example use cases include:

* Semantic search or document similarity: using language embedding models to calculate embeddings for different parts of the document.
* Keyword and taxonomy tagging: using machine learning models to automatically apply taxonomy tags to different parts of a document.

## Recursive text extract using the Table of Contents

In this section, we'll recursively extract text from the different portions of the document, following the hierarchy defined by the Table of Contents.

### Fetch the Table of Contents (TOC)

The Laws.Africa Content API can provide the Table of Contents (TOC) hierarchy of a document in JSON format. This is often easier than trying to build it yourself.

Let's fetch it from the API for our example Cape Town Liquor By-law, using the same API token we used in [Basics of text extraction](/tutorials/module-3-text-extraction-for-search-and-analysis/basics-of-text-extraction).

```python
import json

url = "https://api.laws.africa/v3/akn/za-cpt/act/by-law/2014/control-undertakings-liquor/eng/toc.json"
request = Request(url, headers={"Authorization": f"Token {TOKEN}"})
toc = json.loads(urlopen(request).read())['toc']
toc
# [{'type': 'preface',
# 'component': 'main',
# 'title': 'Preface',
# 'children': [],
# 'basic_unit': False,
# 'num': None,
# ....
```

The `toc` variable now contains an array of TOC entries. Each entry has key details such as a type, a title, and an `id`. The `id` matches the **eId** of the corresponding XML element.

A TOC item can also have nested items in its `children` attribute.

### Extract text from TOC entries

Let's create a function that recursively extracts the text from each entry in the TOC, as follows:

1. iterate over each TOC entry, and its children
2. for each entry, get the corresponding XML element from the XML document tree
3. extract the text from the element, and store it on a new `text` attribute on the TOC entry

```python
# setup a namespace, as before
# we assume that we have already parsed the document xml into the variable "root"
ns = root.nsmap[None]
nsmap = {"a": ns}

def extract_item_text(item):
  if item['id']:
    # get the XML element with this id
    elements = root.xpath(f'//a:*[@eId="{item["id"]}"]', namespaces=nsmap)
    if elements:
      el = elements[0]
      # get text nodes from this element, ignoring editorial remarks
      item['text'] = ' '.join(el.xpath('.//text()[not(ancestor::a:remark)]', namespaces=nsmap))

  # recursively extract text from the children
  for child in item['children']:
    extract_item_text(child)

# for each item in the toc, add a "text" attribute with the text of that item
for item in toc:
  extract_item_text(item)

toc[3]
# {'type': 'section',
# 'component': 'main',
# 'title': '2. Application',
# 'children': [],
# 'basic_unit': True,
# 'num': '2.',
# 'id': 'sec_2',
# 'heading': 'Application',
# 'url': 'https://api.laws.africa/v3/akn/za-cpt/act/by-law/2014/control-undertakings-liquor/eng/!main~sec_2',
# 'text': '2. Application This By-law is applicable to licensees that  sell   liquor  to the public within the jurisdiction of the  City .'
# }
```

We now have the text of the entire document broken down into individual portions.

With this information we can:

* Calculate a text embedding for the **text** of each portion of the document, and index it into a vector database along with the **heading** and **id** of the portion. We can then run a semantic search query, and return the text, heading and id of matching portions.
* Apply taxonomy tags to the text of each portion of the document, using a tool like Pool Party. We can then store the resulting **tags** and the **id** of the portion, and use it to enrich the document as shown in [Advanced enrichments](/tutorials/module-2-enrichments-and-interactivity/advanced-enrichments).


