diff --git a/app/api/main.py b/app/api/main.py
index e2cf9ca50382db8cd61a974147881cc5f284cbe5..f778224b76bfdea9b67062177d984a6918d73725 100644
--- a/app/api/main.py
+++ b/app/api/main.py
@@ -14,7 +14,7 @@ from flask import (current_app, Blueprint, jsonify, request)
 from flask_jwt_extended import create_access_token, jwt_required
 from flask_ldap3_login import AuthenticationResponseStatus
 from ..extensions import ldap_manager, db
-from ..models import Item, Manufacturer, Model, Location, Status, Action
+from ..models import Item, Manufacturer, Model, Location, Status, Action, Network
 from .. import utils
 from ..decorators import jwt_groups_accepted
 
@@ -48,13 +48,14 @@ def get_generic_model(model, args):
     return jsonify(data)
 
 
-def create_generic_model(model, mandatory_field='name'):
+def create_generic_model(model, mandatory_fields=('name',)):
     data = request.get_json()
     if data is None:
         raise utils.CSEntryError('Body should be a JSON object')
     current_app.logger.debug(f'Received: {data}')
-    if mandatory_field not in data:
-        raise utils.CSEntryError(f"Missing mandatory field '{mandatory_field}'", status_code=422)
+    for mandatory_field in mandatory_fields:
+        if mandatory_field not in data:
+            raise utils.CSEntryError(f"Missing mandatory field '{mandatory_field}'", status_code=422)
     try:
         instance = model(**data)
     except TypeError as e:
@@ -119,7 +120,7 @@ def create_item():
     # an item so ics_id should also be a mandatory field.
     # But there are existing items (in confluence and JIRA) that we want to
     # import and associate after they have been created.
-    return create_generic_model(Item, mandatory_field='serial_number')
+    return create_generic_model(Item, mandatory_fields=('serial_number',))
 
 
 @bp.route('/items/<id_>', methods=['PATCH'])
@@ -234,3 +235,21 @@ def get_status():
 @jwt_groups_accepted('admin', 'create')
 def create_status():
     return create_generic_model(Status)
+
+
+@bp.route('/networks')
+@jwt_required
+def get_networks():
+    # TODO: add pagination
+    query = utils.get_query(Network.query, request.args)
+    networks = query.order_by(Network.id)
+    data = [network.to_dict() for network in networks]
+    return jsonify(data)
+
+
+@bp.route('/networks', methods=['POST'])
+@jwt_required
+@jwt_groups_accepted('admin')
+def create_network():
+    """Create a new network"""
+    return create_generic_model(Network, mandatory_fields=('prefix', 'first', 'last'))
diff --git a/tests/functional/test_api.py b/tests/functional/test_api.py
index 11dd1984bfab7625f151956d1eecc345b4f8d227..4b0b7375c8419747c19c0f290b9ebebee0d3b264 100644
--- a/tests/functional/test_api.py
+++ b/tests/functional/test_api.py
@@ -21,9 +21,10 @@ ENDPOINT_MODEL = {
     'locations': models.Location,
     'status': models.Status,
     'items': models.Item,
+    'networks': models.Network,
 }
-GENERIC_GET_ENDPOINTS = [key for key in ENDPOINT_MODEL.keys() if key != 'items']
-GENERIC_CREATE_ENDPOINTS = [key for key in ENDPOINT_MODEL.keys() if key not in ('items', 'actions')]
+GENERIC_GET_ENDPOINTS = [key for key in ENDPOINT_MODEL.keys() if key not in ('items', 'networks')]
+GENERIC_CREATE_ENDPOINTS = [key for key in ENDPOINT_MODEL.keys() if key not in ('items', 'actions', 'networks')]
 CREATE_AUTH_ENDPOINTS = [key for key in ENDPOINT_MODEL.keys() if key != 'actions']
 
 
@@ -106,10 +107,10 @@ def check_names(response, names):
     assert set(names) == response_names
 
 
-def check_items(response, inputs):
+def check_input_is_subset_of_response(response, inputs):
     # Sort the response by id to match the inputs order
-    response_items = sorted(response.json, key=lambda d: d['id'])
-    for d1, d2 in zip(inputs, response_items):
+    response_elts = sorted(response.json, key=lambda d: d['id'])
+    for d1, d2 in zip(inputs, response_elts):
         assert set(d1.items()).issubset(set(d2.items()))
 
 
@@ -211,7 +212,7 @@ def test_create_item(client, user_token):
     # check all items that were created
     assert models.Item.query.count() == 3
     response = get(client, '/api/items', user_token)
-    check_items(response, (data, data, data2))
+    check_input_is_subset_of_response(response, (data, data, data2))
 
 
 def test_create_item_invalid_ics_id(client, user_token):
@@ -410,20 +411,121 @@ def test_get_items(client, session, readonly_token):
     response = get(client, '/api/items', token=readonly_token)
     assert response.status_code == 200
     assert len(response.json) == 3
-    check_items(response, (item1.to_dict(), item2.to_dict(), item3.to_dict()))
+    check_input_is_subset_of_response(response, (item1.to_dict(), item2.to_dict(), item3.to_dict()))
 
     # test filtering
     response = get(client, '/api/items?serial_number=234567', token=readonly_token)
     assert response.status_code == 200
     assert len(response.json) == 1
-    check_items(response, (item2.to_dict(),))
+    check_input_is_subset_of_response(response, (item2.to_dict(),))
     # filtering on location_id works but not location (might want to change that)
     response = get(client, f'/api/items?location_id={item1.location_id}', token=readonly_token)
     assert response.status_code == 200
     assert len(response.json) == 1
-    check_items(response, (item1.to_dict(),))
+    check_input_is_subset_of_response(response, (item1.to_dict(),))
     response = get(client, '/api/items?location=ESS', token=readonly_token)
     check_response_message(response, 'Invalid query arguments', 422)
     # using an unknown key raises a 422
     response = get(client, '/api/items?foo=bar', token=readonly_token)
     check_response_message(response, 'Invalid query arguments', 422)
+
+
+def test_get_networks(client, session, readonly_token):
+    # Create some networks
+    location = models.Location(name='G02')
+    session.add(location)
+    session.flush()
+    network1 = models.Network(prefix='172.16.1.0/24', first='172.16.1.1', last='172.16.1.254', label='network1')
+    network2 = models.Network(prefix='172.16.20.0/22', first='172.16.20.11', last='172.16.20.250')
+    network3 = models.Network(prefix='172.16.5.0/24', first='172.16.5.10', last='172.16.5.254', location_id=location.id)
+    for network in (network1, network2, network3):
+        session.add(network)
+    session.commit()
+
+    response = get(client, '/api/networks', token=readonly_token)
+    assert response.status_code == 200
+    assert len(response.json) == 3
+    check_input_is_subset_of_response(response, (network1.to_dict(), network2.to_dict(), network3.to_dict()))
+
+    # test filtering by location_id
+    response = get(client, f'/api/networks?location_id={location.id}', token=readonly_token)
+    assert response.status_code == 200
+    assert len(response.json) == 1
+    check_input_is_subset_of_response(response, (network3.to_dict(),))
+
+    # test filtering by prefix
+    response = get(client, '/api/networks?prefix=172.16.20.0/22', token=readonly_token)
+    assert response.status_code == 200
+    assert len(response.json) == 1
+    check_input_is_subset_of_response(response, (network2.to_dict(),))
+
+
+def test_create_network_auth_fail(client, session, user_token):
+    # admin is required to create networks
+    response = post(client, '/api/networks', data={}, token=user_token)
+    check_response_message(response, "User doesn't have the required group", 403)
+
+
+def test_create_network(client, session, admin_token):
+    location = models.Location(name='G02')
+    session.add(location)
+    session.commit()
+    # check that prefix, first and last are mandatory
+    response = post(client, '/api/networks', data={}, token=admin_token)
+    check_response_message(response, "Missing mandatory field 'prefix'", 422)
+    response = post(client, '/api/networks', data={'first': '172.16.1.10', 'last': '172.16.1.250'}, token=admin_token)
+    check_response_message(response, "Missing mandatory field 'prefix'", 422)
+    response = post(client, '/api/networks', data={'prefix': '172.16.1.0/24'}, token=admin_token)
+    check_response_message(response, "Missing mandatory field 'first'", 422)
+    response = post(client, '/api/networks', data={'prefix': '172.16.1.0/24', 'first': '172.16.1.10'}, token=admin_token)
+    check_response_message(response, "Missing mandatory field 'last'", 422)
+
+    data = {'prefix': '172.16.1.0/24',
+            'first': '172.16.1.10',
+            'last': '172.16.1.250'}
+    response = post(client, '/api/networks', data=data, token=admin_token)
+    assert response.status_code == 201
+    assert {'id', 'prefix', 'first', 'last', 'label', 'vlanid', 'gateway', 'location'} == set(response.json.keys())
+    assert response.json['prefix'] == '172.16.1.0/24'
+    assert response.json['first'] == '172.16.1.10'
+    assert response.json['last'] == '172.16.1.250'
+
+    # Check that prefix shall be unique
+    response = post(client, '/api/networks', data=data, token=admin_token)
+    check_response_message(response, 'IntegrityError', 409)
+
+    # Check that all parameters can be passed
+    data2 = {'prefix': '172.16.5.0/24',
+             'first': '172.16.5.11',
+             'last': '172.16.5.250',
+             'label': 'G02',
+             'gateway': '172.16.5.10',
+             'vlanid': 1601,
+             'location_id': location.id}
+    response = post(client, '/api/networks', data=data2, token=admin_token)
+    assert response.status_code == 201
+    assert response.json['location'] == location.name
+
+    # check all items that were created
+    assert models.Network.query.count() == 2
+
+
+def test_create_network_constraint_fail(client, session, admin_token):
+    # first not in prefix
+    data = {'prefix': '172.16.1.0/24',
+            'first': '172.16.2.10',
+            'last': '172.16.1.250'}
+    response = post(client, '/api/networks', data=data, token=admin_token)
+    check_response_message(response, 'IntegrityError', 409)
+    # last not in prefix
+    data = {'prefix': '172.16.1.0/24',
+            'first': '172.16.1.10',
+            'last': '172.16.5.250'}
+    response = post(client, '/api/networks', data=data, token=admin_token)
+    check_response_message(response, 'IntegrityError', 409)
+    # first > last
+    data = {'prefix': '172.16.1.0/24',
+            'first': '172.16.1.10',
+            'last': '172.16.1.9'}
+    response = post(client, '/api/networks', data=data, token=admin_token)
+    check_response_message(response, 'IntegrityError', 409)