diff --git a/pkg/services/common/ape/checker.go b/pkg/services/common/ape/checker.go
index 30580da12..86021c3db 100644
--- a/pkg/services/common/ape/checker.go
+++ b/pkg/services/common/ape/checker.go
@@ -103,7 +103,7 @@ func (c *checkerCoreImpl) CheckAPE(prm CheckPrm) error {
 	if found && status == apechain.Allow {
 		return nil
 	}
-	return fmt.Errorf("access to operation %s is denied by access policy engine: %s", prm.Request.Operation(), status.String())
+	return newChainRouterError(prm.Request.Operation(), status)
 }
 
 // isValidBearer checks whether bearer token was correctly signed by authorized
diff --git a/pkg/services/common/ape/error.go b/pkg/services/common/ape/error.go
new file mode 100644
index 000000000..d3c381de7
--- /dev/null
+++ b/pkg/services/common/ape/error.go
@@ -0,0 +1,33 @@
+package ape
+
+import (
+	"fmt"
+
+	apechain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
+)
+
+// ChainRouterError is returned when chain router validation prevents
+// the APE request from being processed (no rule found, access denied, etc.).
+type ChainRouterError struct {
+	operation string
+	status    apechain.Status
+}
+
+func (e *ChainRouterError) Error() string {
+	return fmt.Sprintf("access to operation %s is denied by access policy engine: %s", e.Operation(), e.Status())
+}
+
+func (e *ChainRouterError) Operation() string {
+	return e.operation
+}
+
+func (e *ChainRouterError) Status() apechain.Status {
+	return e.status
+}
+
+func newChainRouterError(operation string, status apechain.Status) *ChainRouterError {
+	return &ChainRouterError{
+		operation: operation,
+		status:    status,
+	}
+}