diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json
index b9adc25046857afec4bad67e76932c59a9cf43d7..c1f7e439edae23e44ed6a45de1e1c0fae96c4ad9 100644
--- a/frontend/public/locales/en/common.json
+++ b/frontend/public/locales/en/common.json
@@ -99,10 +99,13 @@
     "nationalIdOrPassport": "National ID or passport information need to be entered"
   },
   "button": {
+    "yes": "Yes",
+    "no": "No",
     "back": "Back",
     "next": "Next",
     "save": "Save",
     "cancel": "Cancel",
+    "verify": "Verify",
     "backToFrontPage": "Go to front page",
     "cancelInvitation": "Cancel",
     "resendInvitation": "Send",
@@ -136,6 +139,8 @@
   "sponsorSubmitSuccessDescription": "Your registration has been completed. You will receive an e-mail when the guest has filled in the missing information, so that the guest account can be approved.",
   "guestSubmitSuccessDescription": "Your registration is now completed. You will receive an e-mail or SMS when your account has been created.",
   "confirmationDialog": {
+    "confirmIdentityText": "Confirm?",
+    "confirmIdentityTitle": "Are you sure you want to verify this identity?",
     "cancelInvitation": "Cancel invitation?",
     "cancelInvitationDescription": "Do you want to cancel the invitation?"
   }
diff --git a/frontend/public/locales/nb/common.json b/frontend/public/locales/nb/common.json
index f9172a826454e4ca3ad311af0c0cb9ef5b8aae31..64ac355125d35d8c40de428d91df23a59ca8e5ce 100644
--- a/frontend/public/locales/nb/common.json
+++ b/frontend/public/locales/nb/common.json
@@ -98,10 +98,13 @@
     "nationalIdOrPassport": "Fødselsnummer eller passinformasjon må oppgis"
   },
   "button": {
+    "yes": "Ja",
+    "no": "Nei",
     "back": "Tilbake",
     "next": "Neste",
     "save": "Lagre",
     "cancel": "Avbryt",
+    "verify": "Bekreft",
     "backToFrontPage": "Tilbake til forsiden",
     "resendInvitation": "Send",
     "cancelInvitation": "Kanseller",
@@ -135,6 +138,8 @@
   "sponsorSubmitSuccessDescription": "Din registrering er nå fullført. Du vil få en e-post når gjesten har fylt inn informasjonen som mangler, slik at gjestekontoen kan godkjennes.",
   "guestSubmitSuccessDescription": "Din registrering er nå fullført. Du vil få en e-post eller SMS når kontoen er opprettet.",
   "confirmationDialog": {
+    "confirmIdentityText": "Bekrefte?",
+    "confirmIdentityTitle": "Er du sikker på at du vil bekrefte denne identiteten?",
     "cancelInvitation": "Kanseller invitasjon?",
     "cancelInvitationDescription": "Vil du kansellere invitasjonen?"
   }
diff --git a/frontend/public/locales/nn/common.json b/frontend/public/locales/nn/common.json
index 8f13f7c9a04acca248f6ca85251b8b7b26bc97c8..6000ca6ee8d12388338fda29ebddcc6129a9ead7 100644
--- a/frontend/public/locales/nn/common.json
+++ b/frontend/public/locales/nn/common.json
@@ -99,10 +99,13 @@
     "nationalIdOrPassport": "Fødselsnummer eller passinformasjon må oppgjevast"
   },
   "button": {
+    "yes": "Ja",
+    "no": "Nei",
     "back": "Tilbake",
     "next": "Neste",
     "save": "Lagre",
     "cancel": "Avbryt",
+    "verify": "Bekreft",
     "backToFrontPage": "Tilbake til forsida",
     "resendInvitation": "Send",
     "cancelInvitation": "Kanseller",
@@ -136,6 +139,8 @@
   "sponsorSubmitSuccessDescription": "Di registrering er no fullført. Du vil få ein e-post når gjesten har fylt inn informasjonen som manglar, slik at gjestekontoen kan godkjennast.",
   "guestSubmitSuccessDescription": "Di registrering er no fullført. Du vil få ein e-post eller SMS når kontoen er oppretta.",
   "confirmationDialog": {
+    "confirmIdentityText": "Bekrefte?",
+    "confirmIdentityTitle": "Er du sikker på at du vil bekrefte denne identiteten?",
     "cancelInvitation": "Kanseller invitasjon?",
     "cancelInvitationDescription": "Vil du kansellere invitasjonen?"
   }
diff --git a/frontend/src/components/confirmDialog/index.tsx b/frontend/src/components/confirmDialog/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4540e3661a8aba7c35ed8aea0fe83baaea26e937
--- /dev/null
+++ b/frontend/src/components/confirmDialog/index.tsx
@@ -0,0 +1,52 @@
+import {
+  Button,
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogTitle,
+} from '@mui/material'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+
+type ConfirmDialogProps = {
+  title: string
+  open: boolean
+  setOpen: (value: boolean) => void
+  onConfirm: () => void
+  children: React.ReactNode
+}
+
+const ConfirmDialog = (props: ConfirmDialogProps) => {
+  const { title, children, open, setOpen, onConfirm } = props
+  const [t] = useTranslation('common')
+  return (
+    <Dialog
+      open={open}
+      onClose={() => setOpen(false)}
+      aria-labelledby="confirm-dialog"
+    >
+      <DialogTitle id="confirm-dialog">{title}</DialogTitle>
+      <DialogContent>{children}</DialogContent>
+      <DialogActions>
+        <Button
+          variant="contained"
+          onClick={() => setOpen(false)}
+          color="secondary"
+        >
+          {t('button.no')}
+        </Button>
+        <Button
+          variant="contained"
+          onClick={() => {
+            setOpen(false)
+            onConfirm()
+          }}
+          color="error"
+        >
+          {t('button.yes')}
+        </Button>
+      </DialogActions>
+    </Dialog>
+  )
+}
+export default ConfirmDialog
diff --git a/frontend/src/components/identityLine/index.tsx b/frontend/src/components/identityLine/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..54fbb24b2a7f1ed9addaed8e977f597480d36c9f
--- /dev/null
+++ b/frontend/src/components/identityLine/index.tsx
@@ -0,0 +1,57 @@
+import { Button, TableCell, TableRow } from '@mui/material'
+import ConfirmDialog from 'components/confirmDialog'
+import { Identity } from 'interfaces'
+import { useState } from 'react'
+import { submitJsonOpts } from 'utils'
+import CheckIcon from '@mui/icons-material/Check'
+import { useTranslation } from 'react-i18next'
+
+interface IdentityLineProps {
+  text: string
+  identity: Identity | null
+  reloadGuest: () => void
+}
+const IdentityLine = ({ text, identity, reloadGuest }: IdentityLineProps) => {
+  // Make a line with a confirmation button if the identity has not been verified
+  const [confirmOpen, setConfirmOpen] = useState(false)
+  const [t] = useTranslation('common')
+  const verifyIdentity = (id: string) => async () => {
+    await fetch(`/api/ui/v1/identity/${id}`, submitJsonOpts('PATCH', {}))
+    reloadGuest()
+  }
+
+  if (identity == null) {
+    return <></>
+  }
+  return (
+    <TableRow>
+      <TableCell align="left">{text}</TableCell>
+      <TableCell align="left">{identity ? identity.value : ''}</TableCell>
+      <TableCell>
+        {!identity.verified_at ? (
+          <div>
+            <Button
+              aria-label="verify"
+              onClick={() => setConfirmOpen(true)}
+              disabled={!identity}
+            >
+              {t('button.verify')}
+            </Button>
+            <ConfirmDialog
+              title={t('confirmationDialog.confirmIdentityTitle')}
+              open={confirmOpen}
+              setOpen={setConfirmOpen}
+              onConfirm={verifyIdentity(identity.id)}
+            >
+              {t('confirmationDialog.confirmIdentityText')}
+            </ConfirmDialog>
+          </div>
+        ) : (
+          <CheckIcon sx={{ fill: (theme) => theme.palette.success.main }} />
+        )}
+      </TableCell>
+    </TableRow>
+  )
+}
+
+export default IdentityLine
diff --git a/frontend/src/hooks/useGuest/index.tsx b/frontend/src/hooks/useGuest/index.tsx
index b719ca0a3aec9073e5e6d556c8090ddd70af30b6..62cf6e5b20f101c275c5cf478021f419611ddb45 100644
--- a/frontend/src/hooks/useGuest/index.tsx
+++ b/frontend/src/hooks/useGuest/index.tsx
@@ -1,6 +1,6 @@
 import { FetchedRole, Guest } from 'interfaces'
 import { useEffect, useState } from 'react'
-import { parseRole } from 'utils'
+import { parseRole, parseIdentity } from 'utils'
 
 const useGuest = (pid: string) => {
   const [guestInfo, setGuest] = useState<Guest>({
@@ -8,7 +8,8 @@ const useGuest = (pid: string) => {
     first: '',
     last: '',
     email: '',
-    fnr: '',
+    fnr: null,
+    passport: null,
     mobile: '',
     active: false,
     registered: false,
@@ -28,7 +29,8 @@ const useGuest = (pid: string) => {
           last: rjson.last,
           email: rjson.email,
           mobile: rjson.mobile,
-          fnr: rjson.fnr,
+          fnr: parseIdentity(rjson.fnr),
+          passport: parseIdentity(rjson.passport),
           active: rjson.active,
           registered: rjson.registered,
           verified: rjson.verified,
diff --git a/frontend/src/hooks/useGuests/index.tsx b/frontend/src/hooks/useGuests/index.tsx
index d18b246546fb848b0a2ad5641e09fd865f674343..04e62653ac34b8b3b2a8882899d508db454fac05 100644
--- a/frontend/src/hooks/useGuests/index.tsx
+++ b/frontend/src/hooks/useGuests/index.tsx
@@ -1,6 +1,6 @@
 import { useEffect, useState } from 'react'
-import { FetchedGuest, Guest } from '../../interfaces'
-import { parseRole } from '../../utils'
+import { FetchedGuest, Guest } from 'interfaces'
+import { parseIdentity, parseRole } from 'utils'
 
 const useGuests = () => {
   const [guests, setGuests] = useState<Guest[]>([])
@@ -10,7 +10,7 @@ const useGuests = () => {
       const response = await fetch('/api/ui/v1/guests/?format=json')
       const jsonResponse = await response.json()
       if (response.ok) {
-        const persons = await jsonResponse.persons
+        const persons = await jsonResponse
         setGuests(
           persons.map(
             (person: FetchedGuest): Guest => ({
@@ -19,7 +19,8 @@ const useGuests = () => {
               last: person.last,
               email: person.email,
               mobile: person.mobile,
-              fnr: person.fnr,
+              fnr: parseIdentity(person.fnr),
+              passport: parseIdentity(person.passport),
               active: person.active,
               roles: person.roles.map((role) => parseRole(role)),
               registered: person.registered,
diff --git a/frontend/src/hooks/useOus/index.tsx b/frontend/src/hooks/useOus/index.tsx
index 01459176acb55d3eeada3c9271b7ffc640d6b0b8..b8073e4e14c1263f6f5858712e41b1a72f433458 100644
--- a/frontend/src/hooks/useOus/index.tsx
+++ b/frontend/src/hooks/useOus/index.tsx
@@ -9,10 +9,10 @@ type OuData = {
 function useOus(): OuData[] {
   const [ous, setOus] = useState<OuData[]>([])
   const getOptions = async () => {
-    const response = await fetch('/api/ui/v1/ous/?format=json')
+    const response = await fetch('/api/ui/v1/ous?format=json')
     if (response.ok) {
       const ousJson = await response.json()
-      setOus(ousJson.ous)
+      setOus(ousJson)
     }
   }
 
diff --git a/frontend/src/interfaces/index.ts b/frontend/src/interfaces/index.ts
index 242b67852addc2434756b1113697b20975126120..9cd159ff4da87bee6e6eaa6a7544c8ad98e04bb5 100644
--- a/frontend/src/interfaces/index.ts
+++ b/frontend/src/interfaces/index.ts
@@ -4,20 +4,36 @@ export type Guest = {
   last: string
   email: string
   mobile: string
-  fnr: string
+  fnr: Identity | null
+  passport: Identity | null
   active: boolean
   registered: boolean
   verified: boolean
   roles: Role[]
 }
 
+export type Identity = {
+  id: string
+  type: string
+  verified_at: Date | null
+  value: string
+}
+
+export type FetchedIdentity = {
+  id: string
+  type: string
+  verified_at: string | null
+  value: string
+}
+
 export interface FetchedGuest {
   pid: string
   first: string
   last: string
   email: string
   mobile: string
-  fnr: string
+  fnr: FetchedIdentity | null
+  passport: FetchedIdentity | null
   active: boolean
   registered: boolean
   verified: boolean
diff --git a/frontend/src/routes/sponsor/guest/guestInfo/index.tsx b/frontend/src/routes/sponsor/guest/guestInfo/index.tsx
index d01f6f02b20756eab34cb3b86ed523662c72803f..4d2132c36c1538d9abb0477e0b726171bacd124b 100644
--- a/frontend/src/routes/sponsor/guest/guestInfo/index.tsx
+++ b/frontend/src/routes/sponsor/guest/guestInfo/index.tsx
@@ -23,7 +23,8 @@ import { Guest } from 'interfaces'
 import SponsorInfoButtons from 'routes/components/sponsorInfoButtons'
 import { useEffect, useState } from 'react'
 import { SubmitHandler, useForm } from 'react-hook-form'
-import { isValidEmail, submitJsonOpts } from '../../../../utils'
+import IdentityLine from 'components/identityLine'
+import { isValidEmail, submitJsonOpts } from 'utils'
 
 type GuestInfoParams = {
   pid: string
@@ -34,6 +35,7 @@ type GuestInfoProps = {
   updateEmail: (validEmail: string) => void
   resend: () => void
   reloadGuests: () => void
+  reloadGuest: () => void
 }
 
 type CancelConfirmationDialogProps = {
@@ -83,6 +85,7 @@ export default function GuestInfo({
   updateEmail,
   resend,
   reloadGuests,
+  reloadGuest,
 }: GuestInfoProps) {
   const { pid } = useParams<GuestInfoParams>()
   const [t] = useTranslation(['common'])
@@ -164,6 +167,7 @@ export default function GuestInfo({
             <TableRow>
               <TableCell align="left">{t('guestInfo.contactInfo')}</TableCell>
               <TableCell />
+              <TableCell />
             </TableRow>
           </TableHead>
           <TableBody>
@@ -172,6 +176,7 @@ export default function GuestInfo({
               <TableCell align="left">
                 {`${guest.first} ${guest.last}`}
               </TableCell>
+              <TableCell />
             </TableRow>
             <TableRow>
               <TableCell align="left">{t('input.email')}</TableCell>
@@ -224,11 +229,18 @@ export default function GuestInfo({
                   onClose={handleDialogClose}
                 />
               </TableCell>
+              <TableCell />
             </TableRow>
-            <TableRow>
-              <TableCell align="left">{t('input.nationalIdNumber')}</TableCell>
-              <TableCell align="left">{guest.fnr}</TableCell>
-            </TableRow>
+            <IdentityLine
+              text={t('input.nationalIdNumber')}
+              identity={guest.fnr}
+              reloadGuest={reloadGuest}
+            />
+            <IdentityLine
+              text={t('input.passportNumber')}
+              identity={guest.passport}
+              reloadGuest={reloadGuest}
+            />
             <TableRow>
               <TableCell align="left">{t('input.mobilePhone')}</TableCell>
               <TableCell align="left">{guest.mobile}</TableCell>
diff --git a/frontend/src/routes/sponsor/guest/index.tsx b/frontend/src/routes/sponsor/guest/index.tsx
index 8046bae7ae41923a7b2517d68c258d563a258ff2..49a092dd5ea76030576489ef922838f921279813 100644
--- a/frontend/src/routes/sponsor/guest/index.tsx
+++ b/frontend/src/routes/sponsor/guest/index.tsx
@@ -67,6 +67,7 @@ function GuestRoutes({ reloadGuests }: GuestRoutesProps) {
           updateEmail={updateEmail}
           resend={resend}
           reloadGuests={reloadGuests}
+          reloadGuest={reloadGuestInfo}
         />
       </Route>
     </>
diff --git a/frontend/src/routes/sponsor/index.tsx b/frontend/src/routes/sponsor/index.tsx
index 0ae812164733dbdfabd7d28e81ce72ae24425666..e429db43d1ce22f8696458662efc9e8d3bff00e6 100644
--- a/frontend/src/routes/sponsor/index.tsx
+++ b/frontend/src/routes/sponsor/index.tsx
@@ -1,8 +1,8 @@
 import { Route } from 'react-router-dom'
 
 import FrontPage from 'routes/sponsor/frontpage'
+import useGuests from 'hooks/useGuests'
 import GuestRoutes from './guest'
-import useGuests from '../../hooks/useGuests'
 
 function Sponsor() {
   const { guests, reloadGuests } = useGuests()
diff --git a/frontend/src/routes/sponsor/register/frontPage.tsx b/frontend/src/routes/sponsor/register/frontPage.tsx
index d53ef156375a9e4bdff4b9ffaebac85544ce6548..21b38e57ec36fcda71249d411f5c1bcf10a20668 100644
--- a/frontend/src/routes/sponsor/register/frontPage.tsx
+++ b/frontend/src/routes/sponsor/register/frontPage.tsx
@@ -22,12 +22,12 @@ function FrontPage() {
     if (event.target.value) {
       console.log('searching')
       const response = await fetch(
-        `/api/ui/v1/person/search/${event.target.value}`
+        `/api/ui/v1/person/search/${event.target.value}?format=json`
       )
       const repjson = await response.json()
       console.log(repjson)
       if (response.ok) {
-        setGuests(repjson.persons)
+        setGuests(repjson)
       }
     }
   }
@@ -52,7 +52,11 @@ function FrontPage() {
         guests.map((guest) => {
           const guestTo = `/sponsor/guest/${guest.pid}`
           return (
-            <MenuItem component={Link} to={guestTo}>
+            <MenuItem
+              key={`${guest.pid}-${guest.value}`}
+              component={Link}
+              to={guestTo}
+            >
               {guest.first} {guest.last}
               <br />
               {guest.value}
diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts
index 08dfed2f91f97bb866ac7efbabd88983711472d0..88b23193afdf34bca4ddb5f26a20dd3227d518f1 100644
--- a/frontend/src/utils/index.ts
+++ b/frontend/src/utils/index.ts
@@ -1,7 +1,7 @@
 import validator from '@navikt/fnrvalidator'
 import { parseISO } from 'date-fns'
 import i18n from 'i18next'
-import { FetchedRole, Role } from 'interfaces'
+import { FetchedIdentity, FetchedRole, Identity, Role } from 'interfaces'
 import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js'
 
 const validEmailRegex =
@@ -122,3 +122,17 @@ export function parseRole(role: FetchedRole): Role {
     max_days: role.max_days,
   }
 }
+
+export function parseIdentity(
+  identity: FetchedIdentity | null
+): Identity | null {
+  if (identity == null) {
+    return null
+  }
+  return {
+    id: identity.id,
+    type: identity.type,
+    value: identity.value,
+    verified_at: identity.verified_at ? parseISO(identity.verified_at) : null,
+  }
+}
diff --git a/greg/api/serializers/identity.py b/greg/api/serializers/identity.py
index 8cfd25538ddfee0fb033819807b73d76e3c596d2..9caf8270f2b517874a2f3f3056e288e2f346b64f 100644
--- a/greg/api/serializers/identity.py
+++ b/greg/api/serializers/identity.py
@@ -24,3 +24,9 @@ class IdentitySerializer(serializers.ModelSerializer):
         if self.is_duplicate(attrs["type"], attrs["value"]):
             raise ValidationError("Identity already exists")
         return attrs
+
+
+class SpecialIdentitySerializer(serializers.ModelSerializer):
+    class Meta:
+        model = Identity
+        fields = ["id", "value", "type", "verified_at"]
diff --git a/greg/api/serializers/organizational_unit.py b/greg/api/serializers/organizational_unit.py
index 95258a1070bdb4324e0f5d1c288e03d8365794bf..1053f79bf934f59c4b6df11d1a2b4f8cd3ac3384 100644
--- a/greg/api/serializers/organizational_unit.py
+++ b/greg/api/serializers/organizational_unit.py
@@ -1,4 +1,5 @@
-from rest_framework.serializers import ModelSerializer
+from rest_framework.serializers import ModelSerializer, CharField
+
 from greg.api.serializers.ouidentifier import OuIdentifierSerializer
 
 from greg.models import OrganizationalUnit
@@ -20,3 +21,12 @@ class OrganizationalUnitSerializer(ModelSerializer):
             "parent",
             "identifiers",
         ]
+
+
+class SponsorOrgUnitsSerializer(ModelSerializer):
+    nb = CharField(source="name_nb")
+    en = CharField(source="name_en")
+
+    class Meta:
+        model = OrganizationalUnit
+        fields = ["id", "nb", "en"]
diff --git a/greg/api/serializers/person.py b/greg/api/serializers/person.py
index f8fe4c9f45c4fe546c32bdb116b810e62c776cda..4327b3d94538e49537a05dca0efcc221df5177b0 100644
--- a/greg/api/serializers/person.py
+++ b/greg/api/serializers/person.py
@@ -1,9 +1,10 @@
 from rest_framework import serializers
+from rest_framework.fields import BooleanField, CharField, SerializerMethodField
 
 from greg.api.serializers.consent import ConsentSerializerBrief
-from greg.api.serializers.identity import IdentitySerializer
-from greg.api.serializers.role import RoleSerializer
-from greg.models import Person
+from greg.api.serializers.identity import IdentitySerializer, SpecialIdentitySerializer
+from greg.api.serializers.role import RoleSerializer, SpecialRoleSerializer
+from greg.models import Person, Identity
 
 
 class PersonSerializer(serializers.ModelSerializer):
@@ -23,3 +24,59 @@ class PersonSerializer(serializers.ModelSerializer):
             "roles",
             "consents",
         ]
+
+
+class SpecialPersonSerializer(serializers.ModelSerializer):
+    """
+    Serializer for the person endpoint
+
+    Can be used to change or add an email to the person
+
+    """
+
+    pid = CharField(source="id", read_only=True)
+    first = CharField(source="first_name", read_only=True)
+    last = CharField(source="last_name", read_only=True)
+    email = SerializerMethodField(source="private_email")
+    mobile = SerializerMethodField(source="private_mobile", read_only=True)
+    fnr = SpecialIdentitySerializer(read_only=True)
+    passport = SpecialIdentitySerializer(read_only=True)
+    active = SerializerMethodField(source="active", read_only=True)
+    registered = BooleanField(source="is_registered", read_only=True)
+    verified = BooleanField(source="is_verified", read_only=True)
+    roles = SpecialRoleSerializer(many=True, read_only=True)
+
+    def get_email(self, obj):
+        return obj.private_email and obj.private_email.value
+
+    def get_mobile(self, obj):
+        return obj.private_mobile and obj.private_mobile.value
+
+    def get_active(self, obj):
+        return obj.is_registered and obj.is_verified
+
+    class Meta:
+        model = Person
+        fields = [
+            "pid",
+            "first",
+            "last",
+            "mobile",
+            "fnr",
+            "email",
+            "passport",
+            "active",
+            "registered",
+            "verified",
+            "roles",
+        ]
+
+
+class PersonSearchSerializer(serializers.ModelSerializer):
+    pid = CharField(source="person.id")
+    first = CharField(source="person.first_name")
+    last = CharField(source="person.last_name")
+
+    class Meta:
+        model = Identity
+        fields = ["pid", "first", "last", "value", "type"]
diff --git a/greg/api/serializers/role.py b/greg/api/serializers/role.py
index 31b2a43f187644fd83675982c5738567a3eea0ea..5b9c3b20a12a78fc4c5bfba273560434d5f127f1 100644
--- a/greg/api/serializers/role.py
+++ b/greg/api/serializers/role.py
@@ -1,5 +1,5 @@
 from rest_framework import serializers
-from rest_framework.fields import IntegerField
+from rest_framework.fields import IntegerField, SerializerMethodField
 
 from greg.api.serializers.organizational_unit import OrganizationalUnitSerializer
 from greg.models import Role, RoleType
@@ -33,3 +33,39 @@ class RoleWriteSerializer(RoleSerializer):
     """
 
     orgunit = IntegerField(source="orgunit_id")  # type: ignore
+
+
+class SpecialRoleSerializer(serializers.ModelSerializer):
+    name_nb = SerializerMethodField(source="type")
+    name_en = SerializerMethodField(source="type")
+    ou_nb = SerializerMethodField(source="orgunit")
+    ou_en = SerializerMethodField(source="orgunit")
+    max_days = SerializerMethodField(source="type")
+
+    def get_name_nb(self, obj):
+        return obj.type.name_nb
+
+    def get_name_en(self, obj):
+        return obj.type.name_en
+
+    def get_ou_nb(self, obj):
+        return obj.orgunit.name_nb
+
+    def get_ou_en(self, obj):
+        return obj.orgunit.name_en
+
+    def get_max_days(self, obj):
+        return obj.type.max_days
+
+    class Meta:
+        model = Role
+        fields = [
+            "id",
+            "name_nb",
+            "name_en",
+            "ou_nb",
+            "ou_en",
+            "start_date",
+            "end_date",
+            "max_days",
+        ]
diff --git a/greg/migrations/0016_identity_allow_blank.py b/greg/migrations/0016_identity_allow_blank.py
new file mode 100644
index 0000000000000000000000000000000000000000..fdbf3188e2057b32389762cd98ab06c7a2354de8
--- /dev/null
+++ b/greg/migrations/0016_identity_allow_blank.py
@@ -0,0 +1,30 @@
+# Generated by Django 3.2.9 on 2021-11-23 12:09
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("greg", "0015_add_feide_email"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="identity",
+            name="verified_at",
+            field=models.DateTimeField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name="identity",
+            name="verified_by",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.PROTECT,
+                related_name="sponsor",
+                to="greg.sponsor",
+            ),
+        ),
+    ]
diff --git a/greg/models.py b/greg/models.py
index a80b7f17d8a83633f1ae90ac00a949caa47800c9..83b64287984912fe3d870e05b5178c15b08e3815 100644
--- a/greg/models.py
+++ b/greg/models.py
@@ -289,9 +289,13 @@ class Identity(BaseModel):
     value = models.CharField(max_length=256)
     verified = models.CharField(max_length=9, choices=Verified.choices, blank=True)
     verified_by = models.ForeignKey(
-        "Sponsor", on_delete=models.PROTECT, related_name="sponsor", null=True
+        "Sponsor",
+        on_delete=models.PROTECT,
+        related_name="sponsor",
+        null=True,
+        blank=True,
     )
-    verified_at = models.DateTimeField(null=True)
+    verified_at = models.DateTimeField(null=True, blank=True)
 
     def __str__(self):
         return "{}(id={!r}, type={!r}, value={!r})".format(
diff --git a/gregui/api/serializers/identity.py b/gregui/api/serializers/identity.py
new file mode 100644
index 0000000000000000000000000000000000000000..df8d06b1a9b1bc059696953b1ef5869bfd22b06e
--- /dev/null
+++ b/gregui/api/serializers/identity.py
@@ -0,0 +1,52 @@
+from django.utils import timezone
+from rest_framework import serializers
+
+from greg.models import Identity
+from gregui.models import GregUserProfile
+
+
+class IdentitySerializer(serializers.ModelSerializer):
+    """Serializer for the Identity model with validation of various fields"""
+
+    class Meta:
+        model = Identity
+        fields = "__all__"
+        read_only_fields = [
+            "id",
+            "person",
+            "type",
+            "source",
+            "value",
+            "verified",
+            "verified_by",
+        ]
+
+    def _get_sponsor(self):
+        """
+        Fetch the sponsor doing the request
+
+        Since the ViewSet using this Serializer uses the IsSponsor permission, we know
+        that the user is connect to a GregUserProfile with a sponsor object.
+        """
+        user = None
+        request = self.context.get("request")
+        if request and hasattr(request, "user"):
+            user = request.user
+        return GregUserProfile.objects.get(user=user).sponsor
+
+    def validate(self, attrs):
+        """
+        Set values automatically when updating.
+
+        - All updates are manual from the ui endpoints.
+        - The one verifying must be the logged in user which we know is a sponsor since
+          the user passed the IsSponsor permission in the view using this serializer.
+        - No point leaving setting the verified_at time to anyone other than the
+          server itself.
+
+        Note: Get requests do not use this method, making it safe.
+        """
+        attrs["verified"] = Identity.Verified.MANUAL
+        attrs["verified_by"] = self._get_sponsor()
+        attrs["verified_at"] = timezone.now()
+        return attrs
diff --git a/gregui/api/urls.py b/gregui/api/urls.py
index 6cb9764a4785481399ff927cc176716ea5ef527b..d7fa5f1febaa1b411ae31c6ed5e84fbe4dd587c6 100644
--- a/gregui/api/urls.py
+++ b/gregui/api/urls.py
@@ -1,5 +1,6 @@
 from django.urls import re_path, path
 from rest_framework.routers import DefaultRouter
+from gregui.api.views.identity import IdentityViewSet
 
 from gregui.api.views.invitation import (
     CheckInvitationView,
@@ -7,13 +8,18 @@ from gregui.api.views.invitation import (
     InvitationView,
     InvitedGuestView,
 )
-from gregui.api.views.person import PersonSearchView, PersonView
+from gregui.api.views.ou import OusViewSet
+from gregui.api.views.person import GuestInfoViewSet, PersonSearchViewSet, PersonViewSet
 from gregui.api.views.role import RoleInfoViewSet
 from gregui.api.views.roletypes import RoleTypeViewSet
 from gregui.api.views.unit import UnitsViewSet
 
 router = DefaultRouter(trailing_slash=False)
 router.register(r"role", RoleInfoViewSet, basename="role")
+router.register(r"identity", IdentityViewSet, basename="identity")
+router.register(r"ous", OusViewSet, basename="ou")
+router.register(r"person", PersonViewSet, basename="person")
+router.register(r"guests/", GuestInfoViewSet, basename="guests")
 
 urlpatterns = router.urls
 urlpatterns += [
@@ -27,8 +33,9 @@ urlpatterns += [
         name="invite-resend",
     ),
     path("invite/", InvitationView.as_view(), name="invitation"),
-    path("person/<int:id>", PersonView.as_view(), name="person-get"),
-    path(
-        "person/search/<searchstring>", PersonSearchView.as_view(), name="person-search"
+    re_path(
+        r"person/search/(?P<searchstring>\S+)",  # search for sequence of any non-whitespace char
+        PersonSearchViewSet.as_view({"get": "list"}),
+        name="person-search",
     ),
 ]
diff --git a/gregui/api/views/identity.py b/gregui/api/views/identity.py
new file mode 100644
index 0000000000000000000000000000000000000000..18c88586611af8ad85e7a291b9bb115960d6a9ea
--- /dev/null
+++ b/gregui/api/views/identity.py
@@ -0,0 +1,31 @@
+from rest_framework.authentication import SessionAuthentication, BasicAuthentication
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.viewsets import GenericViewSet
+from rest_framework import mixins
+from rest_framework.exceptions import MethodNotAllowed
+from greg.models import Identity
+from greg.permissions import IsSponsor
+from gregui.api.serializers.identity import IdentitySerializer
+from gregui.models import GregUserProfile
+
+
+class IdentityViewSet(
+    mixins.RetrieveModelMixin,
+    mixins.UpdateModelMixin,
+    GenericViewSet,
+):
+    """
+    Fetch or update identity info for any guest as long as you are a sponsor.
+
+    This is required for when a host(sponsor) needs to verify the identity of a guest
+    so that they are considered "active".
+
+    Limited to GET and PATCH so that we can only update or get a single identity at a
+    time.
+    """
+
+    queryset = Identity.objects.all()
+    authentication_classes = [SessionAuthentication, BasicAuthentication]
+    permission_classes = [IsAuthenticated, IsSponsor]
+    serializer_class = IdentitySerializer
+    http_method_names = ["get", "patch"]
diff --git a/gregui/api/views/ou.py b/gregui/api/views/ou.py
new file mode 100644
index 0000000000000000000000000000000000000000..362f2211d1a9eb073657644f50a9d797d28e7f9d
--- /dev/null
+++ b/gregui/api/views/ou.py
@@ -0,0 +1,20 @@
+from rest_framework import mixins
+from rest_framework.authentication import SessionAuthentication, BasicAuthentication
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.viewsets import GenericViewSet
+from greg.api.serializers.organizational_unit import SponsorOrgUnitsSerializer
+
+from greg.permissions import IsSponsor
+from gregui.models import GregUserProfile
+
+
+class OusViewSet(mixins.ListModelMixin, GenericViewSet):
+    """Fetch Ous related to the authenticated sponsor."""
+
+    authentication_classes = [SessionAuthentication, BasicAuthentication]
+    permission_classes = [IsAuthenticated, IsSponsor]
+    serializer_class = SponsorOrgUnitsSerializer
+
+    def get_queryset(self):
+        sponsor = GregUserProfile.objects.get(user=self.request.user).sponsor
+        return sponsor.units.all()
diff --git a/gregui/api/views/person.py b/gregui/api/views/person.py
index 50ed292122d6e3b9fe71e715c8b9510475ebe926..fec45bb3b1467dccb7dd6dcae4b5e01705a9c2da 100644
--- a/gregui/api/views/person.py
+++ b/gregui/api/views/person.py
@@ -1,18 +1,18 @@
-from django.http.response import JsonResponse
-from django.utils.timezone import now
-from rest_framework import status
+from rest_framework import mixins
 from rest_framework.authentication import SessionAuthentication, BasicAuthentication
 from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-from rest_framework.views import APIView
+from rest_framework.viewsets import GenericViewSet
+from greg.api.serializers.person import PersonSearchSerializer, SpecialPersonSerializer
 
-from greg.models import Identity, Person, InvitationLink
+from greg.models import Identity, Person
 from greg.permissions import IsSponsor
+from gregui import validation
 from gregui.api.serializers.guest import create_identity_or_update
-from gregui.validation import validate_email
+from gregui.models import GregUserProfile
 
 
-class PersonView(APIView):
+class PersonViewSet(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, GenericViewSet):
+
     """
     Fetch person info for any guest as long as you are a sponsor
 
@@ -24,72 +24,48 @@ class PersonView(APIView):
 
     authentication_classes = [SessionAuthentication, BasicAuthentication]
     permission_classes = [IsAuthenticated, IsSponsor]
+    queryset = Person.objects.all()
+    http_methods = ["get", "patch"]
+    serializer_class = SpecialPersonSerializer
 
-    def get(self, request, id):
-        person = Person.objects.get(id=id)
-        response = {
-            "pid": person.id,
-            "first": person.first_name,
-            "last": person.last_name,
-            "email": person.private_email and person.private_email.value,
-            "mobile": person.private_mobile and person.private_mobile.value,
-            "fnr": person.fnr and "".join((person.fnr.value[:-5], "*****")),
-            "active": person.is_registered and person.is_verified,
-            "registered": person.is_registered,
-            "verified": person.is_verified,
-            "roles": [
-                {
-                    "id": role.id,
-                    "name_nb": role.type.name_nb,
-                    "name_en": role.type.name_en,
-                    "ou_nb": role.orgunit.name_nb,
-                    "ou_en": role.orgunit.name_en,
-                    "start_date": role.start_date,
-                    "end_date": role.end_date,
-                    "max_days": role.type.max_days,
-                }
-                for role in person.roles.all()
-            ],
-        }
-        return JsonResponse(response)
-
-    def patch(self, request, id):
-        person = Person.objects.get(id=id)
-        # For now only the e-mail is allowed to be updated
-        email = request.data.get("email")
-        if not email:
-            return Response(status=status.HTTP_400_BAD_REQUEST)
-        validate_email(email)
-        # The following line will raise an exception if the e-mail is not valid
+    def perform_update(self, serializer):
+        """Update email when doing patch"""
+        email = self.request.data.get("email")
+        person = self.get_object()
+        validation.validate_email(email)
         create_identity_or_update(Identity.IdentityType.PRIVATE_EMAIL, email, person)
-
-        return Response(status=status.HTTP_200_OK)
+        return super().perform_update(serializer)
 
 
-class PersonSearchView(APIView):
+class PersonSearchViewSet(mixins.ListModelMixin, GenericViewSet):
     """Search for persons using email or phone number"""
 
     authentication_classes = [SessionAuthentication, BasicAuthentication]
     permission_classes = [IsAuthenticated, IsSponsor]
+    serializer_class = PersonSearchSerializer
 
-    def get(self, requests, searchstring):
-        search = Identity.objects.filter(
-            value__icontains=searchstring,  # icontains to include wrong case emails
+    def get_queryset(self):
+        search = self.kwargs["searchstring"]
+        return Identity.objects.filter(
+            value__icontains=search,  # icontains to include wrong case emails
             type__in=[
                 Identity.IdentityType.PRIVATE_EMAIL,
                 Identity.IdentityType.PRIVATE_MOBILE_NUMBER,
             ],
         )[:10]
-        response = {
-            "persons": [
-                {
-                    "pid": i.person.id,
-                    "first": i.person.first_name,
-                    "last": i.person.last_name,
-                    "value": i.value,
-                    "type": i.type,
-                }
-                for i in search
-            ]
-        }
-        return JsonResponse(response)
+
+
+class GuestInfoViewSet(mixins.ListModelMixin, GenericViewSet):
+    """
+    Fetch all the sponsor's guests.
+
+    Lists all persons connected to the roles the logged in sponsor is connected to.
+    """
+
+    authentication_classes = [SessionAuthentication, BasicAuthentication]
+    permission_classes = [IsAuthenticated, IsSponsor]
+    serializer_class = SpecialPersonSerializer
+
+    def get_queryset(self):
+        user = GregUserProfile.objects.get(user=self.request.user)
+        return Person.objects.filter(roles__sponsor=user.sponsor).distinct()
diff --git a/gregui/tests/api/views/test_person.py b/gregui/tests/api/views/test_person.py
index 4afb1f771111df9a3266ae98622b9b05d934a824..e4a5578c3c37600409cd38843b7f3856f9383e85 100644
--- a/gregui/tests/api/views/test_person.py
+++ b/gregui/tests/api/views/test_person.py
@@ -6,7 +6,7 @@ from rest_framework.reverse import reverse
 @pytest.mark.django_db
 def test_get_person_fail(client):
     """Anonymous user cannot get person info"""
-    url = reverse("gregui-v1:person-get", kwargs={"id": 1})
+    url = reverse("gregui-v1:person-detail", kwargs={"pk": 1})
     response = client.get(url)
     assert response.status_code == status.HTTP_403_FORBIDDEN
 
@@ -15,7 +15,7 @@ def test_get_person_fail(client):
 def test_get_person(client, log_in, user_sponsor, invited_person):
     """Logged in sponsor can get person info"""
     person, _ = invited_person
-    url = reverse("gregui-v1:person-get", kwargs={"id": person.id})
+    url = reverse("gregui-v1:person-detail", kwargs={"pk": person.id})
     log_in(user_sponsor)
     response = client.get(url)
     assert response.status_code == status.HTTP_200_OK
@@ -25,7 +25,7 @@ def test_get_person(client, log_in, user_sponsor, invited_person):
 def test_patch_person_no_data_fail(client, log_in, user_sponsor, invited_person):
     """No data in patch should fail"""
     person, _ = invited_person
-    url = reverse("gregui-v1:person-get", kwargs={"id": person.id})
+    url = reverse("gregui-v1:person-detail", kwargs={"pk": person.id})
     log_in(user_sponsor)
     response = client.patch(url)
     assert response.status_code == status.HTTP_400_BAD_REQUEST
@@ -35,7 +35,7 @@ def test_patch_person_no_data_fail(client, log_in, user_sponsor, invited_person)
 def test_patch_person_new_email_ok(client, log_in, user_sponsor, invited_person):
     """Logged in sponsor can update email address of person"""
     person, _ = invited_person
-    url = reverse("gregui-v1:person-get", kwargs={"id": person.id})
+    url = reverse("gregui-v1:person-detail", kwargs={"pk": person.id})
     log_in(user_sponsor)
     assert person.private_email.value == "foo@example.org"
     response = client.patch(url, data={"email": "new@example.com"})
diff --git a/gregui/urls.py b/gregui/urls.py
index c8a47cbfd83d1644e146a099de86790e6898a16b..4292a73e4146722848d2b9f7d476f037bd99e87a 100644
--- a/gregui/urls.py
+++ b/gregui/urls.py
@@ -6,7 +6,6 @@ from django.urls.resolvers import URLResolver
 
 from gregui.api import urls as api_urls
 from gregui.api.views.userinfo import UserInfoView
-from gregui.views import OusView, GuestInfoView
 from . import views
 
 urlpatterns: List[URLResolver] = [
@@ -20,6 +19,4 @@ urlpatterns: List[URLResolver] = [
     path("api/ui/v1/testmail/", views.send_test_email, name="api-testmail"),
     path("api/ui/v1/whoami/", views.WhoAmIView.as_view(), name="api-whoami"),
     path("api/ui/v1/userinfo/", UserInfoView.as_view(), name="api-userinfo"),  # type: ignore
-    path("api/ui/v1/ous/", OusView.as_view()),
-    path("api/ui/v1/guests/", GuestInfoView.as_view()),
 ]
diff --git a/gregui/validation.py b/gregui/validation.py
index 18ad553d8687672412ef1cbb148bccf7705f5c53..ecdfca28a2465283a56a50a98747a03fd79a378f 100644
--- a/gregui/validation.py
+++ b/gregui/validation.py
@@ -1,4 +1,5 @@
 import re
+from typing import Optional
 import phonenumbers
 from rest_framework import serializers
 
@@ -19,6 +20,6 @@ def validate_phone_number(value):
         raise serializers.ValidationError("Invalid phone number")
 
 
-def validate_email(value):
-    if not re.fullmatch(_valid_email_regex, value):
+def validate_email(value: Optional[str]):
+    if not value or not re.fullmatch(_valid_email_regex, value):
         raise serializers.ValidationError("Invalid e-mail")
diff --git a/gregui/views.py b/gregui/views.py
index 4721cccb27e0426fa2874796671bb62d1b55dbaa..a60d3b760e25993ea4005b78fad4d6850fc472f9 100644
--- a/gregui/views.py
+++ b/gregui/views.py
@@ -6,10 +6,7 @@ from rest_framework.authentication import SessionAuthentication, BasicAuthentica
 from rest_framework.permissions import IsAuthenticated
 from rest_framework.views import APIView
 
-from greg.models import Person, Sponsor
-from greg.permissions import IsSponsor
 from gregui import mailutils
-from gregui.models import GregUserProfile
 
 
 def get_csrf(request):
@@ -61,70 +58,3 @@ class WhoAmIView(APIView):
     # pylint: disable=W0622
     def get(request, format=None):
         return JsonResponse({"username": request.user.username})
-
-
-class OusView(APIView):
-    """Fetch Ous related to the authenticated sponsor."""
-
-    authentication_classes = [SessionAuthentication, BasicAuthentication]
-    permission_classes = [IsAuthenticated, IsSponsor]
-
-    @staticmethod
-    # pylint: disable=W0622
-    def get(request, format=None):
-        profile = GregUserProfile.objects.get(user=request.user)
-        sponsor = Sponsor.objects.get(id=profile.sponsor.id)
-        return JsonResponse(
-            {
-                "ous": [
-                    {"id": i.id, "nb": i.name_nb, "en": i.name_en}
-                    for i in sponsor.units.all()
-                ]
-            }
-        )
-
-
-class GuestInfoView(APIView):
-    """Fetch all the sponsors guests"""
-
-    authentication_classes = [SessionAuthentication, BasicAuthentication]
-    permission_classes = [IsAuthenticated, IsSponsor]
-
-    @staticmethod
-    # pylint: disable=W0622
-    def get(request, format=None):
-        user = GregUserProfile.objects.get(user=request.user)
-
-        return JsonResponse(
-            {
-                "persons": [
-                    {
-                        "pid": person.id,
-                        "first": person.first_name,
-                        "last": person.last_name,
-                        "email": person.private_email and person.private_email.value,
-                        "mobile": person.private_mobile and person.private_mobile.value,
-                        "fnr": person.fnr and "".join((person.fnr.value[:-5], "*****")),
-                        "active": person.is_registered and person.is_verified,
-                        "registered": person.is_registered,
-                        "verified": person.is_verified,
-                        "roles": [
-                            {
-                                "id": role.id,
-                                "name_nb": role.type.name_nb,
-                                "name_en": role.type.name_en,
-                                "ou_nb": role.orgunit.name_nb,
-                                "ou_en": role.orgunit.name_en,
-                                "start_date": role.start_date,
-                                "end_date": role.end_date,
-                                "max_days": role.type.max_days,
-                            }
-                            for role in person.roles.all()
-                        ],
-                    }
-                    for person in Person.objects.filter(
-                        roles__sponsor=user.sponsor
-                    ).distinct()
-                ]
-            }
-        )