diff --git a/.deadcode-out b/.deadcode-out
index 403a6e40d2..e46e09de9a 100644
--- a/.deadcode-out
+++ b/.deadcode-out
@@ -87,6 +87,9 @@ code.gitea.io/gitea/modules/eventsource
 	Event.String
 
 code.gitea.io/gitea/modules/forgefed
+	NewForgeUndoLike
+	ForgeUndoLike.UnmarshalJSON
+	ForgeUndoLike.Validate
 	GetItemByType
 	JSONUnmarshalerFn
 	NotEmpty
diff --git a/modules/forgefed/activity.go b/modules/forgefed/activity_like.go
similarity index 93%
rename from modules/forgefed/activity.go
rename to modules/forgefed/activity_like.go
index 247abd255a..0f001486b5 100644
--- a/modules/forgefed/activity.go
+++ b/modules/forgefed/activity_like.go
@@ -21,8 +21,8 @@ type ForgeLike struct {
 func NewForgeLike(actorIRI, objectIRI string, startTime time.Time) (ForgeLike, error) {
 	result := ForgeLike{}
 	result.Type = ap.LikeType
-	result.Actor = ap.IRI(actorIRI)   // That's us, a User
-	result.Object = ap.IRI(objectIRI) // That's them, a Repository
+	result.Actor = ap.IRI(actorIRI)
+	result.Object = ap.IRI(objectIRI)
 	result.StartTime = startTime
 	if valid, err := validation.IsValid(result); !valid {
 		return ForgeLike{}, err
@@ -46,20 +46,23 @@ func (like ForgeLike) Validate() []string {
 	var result []string
 	result = append(result, validation.ValidateNotEmpty(string(like.Type), "type")...)
 	result = append(result, validation.ValidateOneOf(string(like.Type), []any{"Like"}, "type")...)
+
 	if like.Actor == nil {
 		result = append(result, "Actor should not be nil.")
 	} else {
 		result = append(result, validation.ValidateNotEmpty(like.Actor.GetID().String(), "actor")...)
 	}
-	if like.Object == nil {
-		result = append(result, "Object should not be nil.")
-	} else {
-		result = append(result, validation.ValidateNotEmpty(like.Object.GetID().String(), "object")...)
-	}
+
 	result = append(result, validation.ValidateNotEmpty(like.StartTime.String(), "startTime")...)
 	if like.StartTime.IsZero() {
 		result = append(result, "StartTime was invalid.")
 	}
 
+	if like.Object == nil {
+		result = append(result, "Object should not be nil.")
+	} else {
+		result = append(result, validation.ValidateNotEmpty(like.Object.GetID().String(), "object")...)
+	}
+
 	return result
 }
diff --git a/modules/forgefed/activity_test.go b/modules/forgefed/activity_like_test.go
similarity index 80%
rename from modules/forgefed/activity_test.go
rename to modules/forgefed/activity_like_test.go
index 9a7979c4e6..6b83381cf9 100644
--- a/modules/forgefed/activity_test.go
+++ b/modules/forgefed/activity_like_test.go
@@ -16,11 +16,11 @@ import (
 )
 
 func Test_NewForgeLike(t *testing.T) {
+	want := []byte(`{"type":"Like","startTime":"2024-03-07T00:00:00Z","actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1","object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}`)
+
 	actorIRI := "https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"
 	objectIRI := "https://codeberg.org/api/v1/activitypub/repository-id/1"
-	want := []byte(`{"type":"Like","startTime":"2024-03-27T00:00:00Z","actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1","object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}`)
-
-	startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-27")
+	startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-07")
 	sut, err := NewForgeLike(actorIRI, objectIRI, startTime)
 	if err != nil {
 		t.Errorf("unexpected error: %v\n", err)
@@ -84,7 +84,6 @@ func Test_LikeUnmarshalJSON(t *testing.T) {
 		wantErr error
 	}
 
-	//revive:disable
 	tests := map[string]testPair{
 		"with ID": {
 			item: []byte(`{"type":"Like","actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1","object":"https://codeberg.org/api/activitypub/repository-id/1"}`),
@@ -100,10 +99,9 @@ func Test_LikeUnmarshalJSON(t *testing.T) {
 		"invalid": {
 			item:    []byte(`{"type":"Invalid","actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1","object":"https://codeberg.org/api/activitypub/repository-id/1"`),
 			want:    &ForgeLike{},
-			wantErr: fmt.Errorf("cannot parse JSON:"),
+			wantErr: fmt.Errorf("cannot parse JSON"),
 		},
 	}
-	//revive:enable
 
 	for name, test := range tests {
 		t.Run(name, func(t *testing.T) {
@@ -120,7 +118,9 @@ func Test_LikeUnmarshalJSON(t *testing.T) {
 	}
 }
 
-func TestActivityValidation(t *testing.T) {
+func Test_ForgeLikeValidation(t *testing.T) {
+	// Successful
+
 	sut := new(ForgeLike)
 	sut.UnmarshalJSON([]byte(`{"type":"Like",
 	"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
@@ -130,35 +130,37 @@ func TestActivityValidation(t *testing.T) {
 		t.Errorf("sut expected to be valid: %v\n", sut.Validate())
 	}
 
+	// Errors
+
 	sut.UnmarshalJSON([]byte(`{"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
 	"object":"https://codeberg.org/api/activitypub/repository-id/1",
 	"startTime": "2014-12-31T23:00:00-08:00"}`))
-	if sut.Validate()[0] != "type should not be empty" {
-		t.Errorf("validation error expected but was: %v\n", sut.Validate()[0])
+	if err := validateAndCheckError(sut, "type should not be empty"); err != nil {
+		t.Error(err)
 	}
 
 	sut.UnmarshalJSON([]byte(`{"type":"bad-type",
 		"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
 	"object":"https://codeberg.org/api/activitypub/repository-id/1",
 	"startTime": "2014-12-31T23:00:00-08:00"}`))
-	if sut.Validate()[0] != "Value bad-type is not contained in allowed values [Like]" {
-		t.Errorf("validation error expected but was: %v\n", sut.Validate()[0])
+	if err := validateAndCheckError(sut, "Value bad-type is not contained in allowed values [Like]"); err != nil {
+		t.Error(err)
 	}
 
 	sut.UnmarshalJSON([]byte(`{"type":"Like",
 		"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
-	"object":"https://codeberg.org/api/activitypub/repository-id/1",
-	"startTime": "not a date"}`))
-	if sut.Validate()[0] != "StartTime was invalid." {
-		t.Errorf("validation error expected but was: %v\n", sut.Validate())
+	  "object":"https://codeberg.org/api/activitypub/repository-id/1",
+	  "startTime": "not a date"}`))
+	if err := validateAndCheckError(sut, "StartTime was invalid."); err != nil {
+		t.Error(err)
 	}
 
 	sut.UnmarshalJSON([]byte(`{"type":"Wrong",
 		"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
-	"object":"https://codeberg.org/api/activitypub/repository-id/1",
-	"startTime": "2014-12-31T23:00:00-08:00"}`))
-	if sut.Validate()[0] != "Value Wrong is not contained in allowed values [Like]" {
-		t.Errorf("validation error expected but was: %v\n", sut.Validate())
+	  "object":"https://codeberg.org/api/activitypub/repository-id/1",
+	  "startTime": "2014-12-31T23:00:00-08:00"}`))
+	if err := validateAndCheckError(sut, "Value Wrong is not contained in allowed values [Like]"); err != nil {
+		t.Error(err)
 	}
 }
 
@@ -166,6 +168,6 @@ func TestActivityValidation_Attack(t *testing.T) {
 	sut := new(ForgeLike)
 	sut.UnmarshalJSON([]byte(`{rubbish}`))
 	if len(sut.Validate()) != 5 {
-		t.Errorf("5 validateion errors expected but was: %v\n", len(sut.Validate()))
+		t.Errorf("5 validation errors expected but was: %v\n", len(sut.Validate()))
 	}
 }
diff --git a/modules/forgefed/activity_undo_like.go b/modules/forgefed/activity_undo_like.go
new file mode 100644
index 0000000000..b6b13ba50d
--- /dev/null
+++ b/modules/forgefed/activity_undo_like.go
@@ -0,0 +1,80 @@
+// Copyright 2023, 2024 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package forgefed
+
+import (
+	"time"
+
+	"code.gitea.io/gitea/modules/validation"
+
+	ap "github.com/go-ap/activitypub"
+)
+
+// ForgeLike activity data type
+// swagger:model
+type ForgeUndoLike struct {
+	// swagger:ignore
+	ap.Activity
+}
+
+func NewForgeUndoLike(actorIRI, objectIRI string, startTime time.Time) (ForgeUndoLike, error) {
+	result := ForgeUndoLike{}
+	result.Type = ap.UndoType
+	result.Actor = ap.IRI(actorIRI)
+	result.StartTime = startTime
+
+	like := ap.Activity{}
+	like.Type = ap.LikeType
+	like.Actor = ap.IRI(actorIRI)
+	like.Object = ap.IRI(objectIRI)
+	result.Object = &like
+
+	if valid, err := validation.IsValid(result); !valid {
+		return ForgeUndoLike{}, err
+	}
+	return result, nil
+}
+
+func (undo *ForgeUndoLike) UnmarshalJSON(data []byte) error {
+	return undo.Activity.UnmarshalJSON(data)
+}
+
+func (undo ForgeUndoLike) Validate() []string {
+	var result []string
+	result = append(result, validation.ValidateNotEmpty(string(undo.Type), "type")...)
+	result = append(result, validation.ValidateOneOf(string(undo.Type), []any{"Undo"}, "type")...)
+
+	if undo.Actor == nil {
+		result = append(result, "Actor should not be nil.")
+	} else {
+		result = append(result, validation.ValidateNotEmpty(undo.Actor.GetID().String(), "actor")...)
+	}
+
+	result = append(result, validation.ValidateNotEmpty(undo.StartTime.String(), "startTime")...)
+	if undo.StartTime.IsZero() {
+		result = append(result, "StartTime was invalid.")
+	}
+
+	if undo.Object == nil {
+		result = append(result, "object should not be empty.")
+	} else if activity, ok := undo.Object.(*ap.Activity); !ok {
+		result = append(result, "object is not of type Activity")
+	} else {
+		result = append(result, validation.ValidateNotEmpty(string(activity.Type), "type")...)
+		result = append(result, validation.ValidateOneOf(string(activity.Type), []any{"Like"}, "type")...)
+
+		if activity.Actor == nil {
+			result = append(result, "Object.Actor should not be nil.")
+		} else {
+			result = append(result, validation.ValidateNotEmpty(activity.Actor.GetID().String(), "actor")...)
+		}
+
+		if activity.Object == nil {
+			result = append(result, "Object.Object should not be nil.")
+		} else {
+			result = append(result, validation.ValidateNotEmpty(activity.Object.GetID().String(), "object")...)
+		}
+	}
+	return result
+}
diff --git a/modules/forgefed/activity_undo_like_test.go b/modules/forgefed/activity_undo_like_test.go
new file mode 100644
index 0000000000..541e524cb3
--- /dev/null
+++ b/modules/forgefed/activity_undo_like_test.go
@@ -0,0 +1,246 @@
+// Copyright 2023, 2024 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package forgefed
+
+import (
+	"fmt"
+	"reflect"
+	"strings"
+	"testing"
+	"time"
+
+	"code.gitea.io/gitea/modules/validation"
+
+	ap "github.com/go-ap/activitypub"
+)
+
+func Test_NewForgeUndoLike(t *testing.T) {
+	actorIRI := "https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"
+	objectIRI := "https://codeberg.org/api/v1/activitypub/repository-id/1"
+	want := []byte(`{"type":"Undo","startTime":"2024-03-27T00:00:00Z",` +
+		`"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",` +
+		`"object":{` +
+		`"type":"Like",` +
+		`"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",` +
+		`"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`)
+
+	startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-27")
+	sut, err := NewForgeUndoLike(actorIRI, objectIRI, startTime)
+	if err != nil {
+		t.Errorf("unexpected error: %v\n", err)
+	}
+	if valid, _ := validation.IsValid(sut); !valid {
+		t.Errorf("sut expected to be valid: %v\n", sut.Validate())
+	}
+
+	got, err := sut.MarshalJSON()
+	if err != nil {
+		t.Errorf("MarshalJSON() error = \"%v\"", err)
+		return
+	}
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("MarshalJSON() got = %q, want %q", got, want)
+	}
+}
+
+func Test_UndoLikeMarshalJSON(t *testing.T) {
+	type testPair struct {
+		item    ForgeUndoLike
+		want    []byte
+		wantErr error
+	}
+
+	startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-27")
+	like, _ := NewForgeLike("https://repo.prod.meissa.de/api/v1/activitypub/user-id/1", "https://codeberg.org/api/v1/activitypub/repository-id/1", startTime)
+	tests := map[string]testPair{
+		"empty": {
+			item: ForgeUndoLike{},
+			want: nil,
+		},
+		"valid": {
+			item: ForgeUndoLike{
+				Activity: ap.Activity{
+					StartTime: startTime,
+					Actor:     ap.IRI("https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"),
+					Type:      "Undo",
+					Object:    like,
+				},
+			},
+			want: []byte(`{"type":"Undo",` +
+				`"startTime":"2024-03-27T00:00:00Z",` +
+				`"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",` +
+				`"object":{` +
+				`"type":"Like",` +
+				`"startTime":"2024-03-27T00:00:00Z",` +
+				`"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",` +
+				`"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`),
+		},
+	}
+
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			got, err := tt.item.MarshalJSON()
+			if (err != nil || tt.wantErr != nil) && tt.wantErr.Error() != err.Error() {
+				t.Errorf("MarshalJSON() error = \"%v\", wantErr \"%v\"", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("MarshalJSON() got = %q\nwant %q", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_UndoLikeUnmarshalJSON(t *testing.T) {
+	type testPair struct {
+		item    []byte
+		want    *ForgeUndoLike
+		wantErr error
+	}
+
+	startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-27")
+	like, _ := NewForgeLike("https://repo.prod.meissa.de/api/v1/activitypub/user-id/1", "https://codeberg.org/api/v1/activitypub/repository-id/1", startTime)
+
+	tests := map[string]testPair{
+		"valid": {
+			item: []byte(`{"type":"Undo",` +
+				`"startTime":"2024-03-27T00:00:00Z",` +
+				`"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",` +
+				`"object":{` +
+				`"type":"Like",` +
+				`"startTime":"2024-03-27T00:00:00Z",` +
+				`"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",` +
+				`"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`),
+			want: &ForgeUndoLike{
+				Activity: ap.Activity{
+					StartTime: startTime,
+					Actor:     ap.IRI("https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"),
+					Type:      "Undo",
+					Object:    like,
+				},
+			},
+			wantErr: nil,
+		},
+		"invalid": {
+			item:    []byte(`invalid JSON`),
+			want:    nil,
+			wantErr: fmt.Errorf("cannot parse JSON"),
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			got := new(ForgeUndoLike)
+			err := got.UnmarshalJSON(test.item)
+			if test.wantErr != nil {
+				if err == nil {
+					t.Errorf("UnmarshalJSON() error = nil, wantErr \"%v\"", test.wantErr)
+				} else if !strings.Contains(err.Error(), test.wantErr.Error()) {
+					t.Errorf("UnmarshalJSON() error = \"%v\", wantErr \"%v\"", err, test.wantErr)
+				}
+				return
+			}
+			remarshalledgot, _ := got.MarshalJSON()
+			remarshalledwant, _ := test.want.MarshalJSON()
+			if !reflect.DeepEqual(remarshalledgot, remarshalledwant) {
+				t.Errorf("UnmarshalJSON() got = %#v\nwant %#v", got, test.want)
+			}
+		})
+	}
+}
+
+func TestActivityValidationUndo(t *testing.T) {
+	sut := new(ForgeUndoLike)
+
+	_ = sut.UnmarshalJSON([]byte(`
+		{"type":"Undo",
+		 "startTime":"2024-03-27T00:00:00Z",
+		 "actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
+		 "object":{
+		   "type":"Like",
+		   "actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
+		   "object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`))
+	if res, _ := validation.IsValid(sut); !res {
+		t.Errorf("sut expected to be valid: %v\n", sut.Validate())
+	}
+
+	_ = sut.UnmarshalJSON([]byte(`
+		{"startTime":"2024-03-27T00:00:00Z",
+		"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
+		"object":{
+		  "type":"Like",
+		  "startTime":"2024-03-27T00:00:00Z",
+		  "actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
+		  "object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`))
+	if err := validateAndCheckError(sut, "type should not be empty"); err != nil {
+		t.Error(*err)
+	}
+
+	_ = sut.UnmarshalJSON([]byte(`
+		{"type":"Undo",
+		 "startTime":"2024-03-27T00:00:00Z",
+		 "object":{
+		   "type":"Like",
+		   "actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
+		   "object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`))
+	if err := validateAndCheckError(sut, "Actor should not be nil."); err != nil {
+		t.Error(*err)
+	}
+
+	_ = sut.UnmarshalJSON([]byte(`
+		{"type":"Undo",
+		"startTime":"2024-03-27T00:00:00Z",
+		"actor":"string",
+		"object":{
+		"type":"Like",
+			"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
+			"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`))
+	if err := validateAndCheckError(sut, "Actor should not be nil."); err != nil {
+		t.Error(*err)
+	}
+
+	_ = sut.UnmarshalJSON([]byte(`
+		{"type":"Undo",
+		"startTime":"2024-03-27T00:00:00Z",
+		"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"
+		}`))
+	if err := validateAndCheckError(sut, "object should not be empty."); err != nil {
+		t.Error(*err)
+	}
+
+	_ = sut.UnmarshalJSON([]byte(`
+		{"type":"Undo",
+		"startTime":"2024-03-27T00:00:00Z",
+		"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
+		"object":{
+		  "startTime":"2024-03-27T00:00:00Z",
+		  "actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
+		  "object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`))
+	if err := validateAndCheckError(sut, "object is not of type Activity"); err != nil {
+		t.Error(*err)
+	}
+
+	_ = sut.UnmarshalJSON([]byte(`
+		{"type":"Undo",
+		"startTime":"2024-03-27T00:00:00Z",
+		"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
+		"object":{
+		  "type":"Like",
+		  "object":""}}`))
+	if err := validateAndCheckError(sut, "Object.Actor should not be nil."); err != nil {
+		t.Error(*err)
+	}
+
+	_ = sut.UnmarshalJSON([]byte(`
+		{"type":"Undo",
+		"startTime":"2024-03-27T00:00:00Z",
+		"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
+		"object":{
+		  "type":"Like",
+		  "startTime":"2024-03-27T00:00:00Z",
+		  "actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"}}`))
+	if err := validateAndCheckError(sut, "Object.Object should not be nil."); err != nil {
+		t.Error(*err)
+	}
+}
diff --git a/modules/forgefed/activity_validateandcheckerror_test.go b/modules/forgefed/activity_validateandcheckerror_test.go
new file mode 100644
index 0000000000..f2f1fbcccb
--- /dev/null
+++ b/modules/forgefed/activity_validateandcheckerror_test.go
@@ -0,0 +1,23 @@
+// Copyright 2023, 2024 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package forgefed
+
+import (
+	"fmt"
+
+	"code.gitea.io/gitea/modules/validation"
+)
+
+func validateAndCheckError(subject validation.Validateable, expectedError string) *string {
+	errors := subject.Validate()
+	err := errors[0]
+	if len(errors) < 1 {
+		val := "Validation error should have been returned, but was not."
+		return &val
+	} else if err != expectedError {
+		val := fmt.Sprintf("Validation error should be [%v] but was: %v\n", expectedError, err)
+		return &val
+	}
+	return nil
+}
diff --git a/routers/api/v1/activitypub/repository.go b/routers/api/v1/activitypub/repository.go
index bc6e7905a6..14381664d4 100644
--- a/routers/api/v1/activitypub/repository.go
+++ b/routers/api/v1/activitypub/repository.go
@@ -70,8 +70,8 @@ func RepositoryInbox(ctx *context.APIContext) {
 
 	repository := ctx.Repo.Repository
 	log.Info("RepositoryInbox: repo: %v", repository)
-
 	form := web.GetForm(ctx)
+	// TODO: Decide between like/undo{like} activity
 	httpStatus, title, err := federation.ProcessLikeActivity(ctx, form, repository.ID)
 	if err != nil {
 		ctx.Error(httpStatus, title, err)