diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/export/phases/training/TrainingPhaseExportDTO.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/export/phases/training/TrainingPhaseExportDTO.java
index 8d53385c8ef8c7c88e4dd4b1eb31b66018f6a415..a19e7a94cd21e947efc33647309e7043c5b4e6da 100644
--- a/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/export/phases/training/TrainingPhaseExportDTO.java
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/export/phases/training/TrainingPhaseExportDTO.java
@@ -107,7 +107,6 @@ public class TrainingPhaseExportDTO extends AbstractPhaseExportDTO {
                 ", solution='" + solution + '\'' +
                 ", incorrectFlagLimit=" + incorrectFlagLimit +
                 ", title='" + title + '\'' +
-                ", maxScore=" + maxScore +
                 ", levelType=" + phaseType +
                 '}';
     }
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/BadRequestException.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/BadRequestException.java
new file mode 100644
index 0000000000000000000000000000000000000000..7e011b8e2e896deb6600bc3f824332a2d9a06dc4
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/BadRequestException.java
@@ -0,0 +1,28 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(value = HttpStatus.BAD_REQUEST,
+        reason = "The server cannot or will not process the request due to an apparent client error (e.g., malformed request syntax, size too large, invalid request message framing, or deceptive request routing).")
+public class BadRequestException extends RuntimeException {
+
+    public BadRequestException() {
+    }
+
+    public BadRequestException(String message) {
+        super(message);
+    }
+
+    public BadRequestException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public BadRequestException(Throwable cause) {
+        super(cause);
+    }
+
+    public BadRequestException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/CustomWebClientException.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/CustomWebClientException.java
new file mode 100644
index 0000000000000000000000000000000000000000..9c8b5c1f33cce7b028d5f0bbd10ef086aacfffc9
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/CustomWebClientException.java
@@ -0,0 +1,89 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions;
+
+import cz.muni.ics.kypo.training.adaptive.exceptions.errors.ApiSubError;
+import org.springframework.http.HttpStatus;
+
+/**
+ * The type Rest template exception.
+ */
+public class CustomWebClientException extends RuntimeException {
+    private HttpStatus statusCode;
+    private ApiSubError apiSubError;
+
+    /**
+     * Instantiates a new Rest template exception.
+     *
+     * @param message    the message
+     * @param statusCode the status code
+     */
+    public CustomWebClientException(String message, HttpStatus statusCode) {
+        super(message);
+        this.statusCode = statusCode;
+    }
+
+    /**
+     * Instantiates a new Rest template exception.
+     *
+     * @param message    the message
+     * @param ex         the ex
+     * @param statusCode the status code
+     */
+    public CustomWebClientException(String message, Throwable ex, HttpStatus statusCode) {
+        super(message, ex);
+        this.statusCode = statusCode;
+    }
+
+    /**
+     * Instantiates a new Rest template exception.
+     *
+     * @param apiSubError detailed information about error.
+     */
+    public CustomWebClientException(ApiSubError apiSubError) {
+        super();
+        this.apiSubError = apiSubError;
+        this.statusCode = apiSubError.getStatus();
+    }
+
+    /**
+     * Instantiates a new Rest template exception.
+     *
+     * @param message    the message
+     * @param apiSubError detailed information about error
+     */
+    public CustomWebClientException(String message, ApiSubError apiSubError) {
+        super(message);
+        this.apiSubError = apiSubError;
+        this.statusCode = apiSubError.getStatus();
+    }
+
+    /**
+     * Instantiates a new Rest template exception.
+     *
+     * @param message    the message
+     * @param ex         the ex
+     * @param apiSubError detailed information about error
+     */
+    public CustomWebClientException(String message, Throwable ex, ApiSubError apiSubError) {
+        super(message, ex);
+        this.apiSubError = apiSubError;
+        this.statusCode = apiSubError.getStatus();
+    }
+
+    /**
+     * Gets detailed information about error.
+     *
+     * @return detailed information about error
+     */
+    public ApiSubError getApiSubError() {
+        return apiSubError;
+    }
+
+    /**
+     * Gets status code.
+     *
+     * @return the status code
+     */
+    public HttpStatus getStatusCode() {
+        return statusCode;
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/EntityConflictException.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/EntityConflictException.java
new file mode 100644
index 0000000000000000000000000000000000000000..23405682904524bd6ff0977ff1dfb0bc8d465ac4
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/EntityConflictException.java
@@ -0,0 +1,38 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(value = HttpStatus.CONFLICT, reason = "The request could not be completed due to a conflict with the current state of the target resource.")
+public class EntityConflictException extends ExceptionWithEntity {
+
+    public EntityConflictException() {
+        super();
+    }
+
+    public EntityConflictException(EntityErrorDetail entityErrorDetail) {
+        super(entityErrorDetail);
+    }
+
+    public EntityConflictException(EntityErrorDetail entityErrorDetail, Throwable cause) {
+        super(entityErrorDetail, cause);
+    }
+
+    public EntityConflictException(Throwable cause) {
+        super(cause);
+    }
+
+    protected String createDefaultReason(EntityErrorDetail entityErrorDetail) {
+        StringBuilder reason = new StringBuilder("Conflict with the current state of the target entity ")
+                .append(entityErrorDetail.getEntity());
+        if (entityErrorDetail.getIdentifier() != null && entityErrorDetail.getIdentifierValue() != null) {
+            reason.append(" (")
+                    .append(entityErrorDetail.getIdentifier())
+                    .append(": ")
+                    .append(entityErrorDetail.getIdentifierValue())
+                    .append(")");
+        }
+        reason.append(".");
+        return reason.toString();
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/EntityErrorDetail.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/EntityErrorDetail.java
new file mode 100644
index 0000000000000000000000000000000000000000..023ddba5ed482ac0e70389210f4844bad13ec136
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/EntityErrorDetail.java
@@ -0,0 +1,110 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.util.Objects;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class EntityErrorDetail {
+    @ApiModelProperty(value = "Class of the entity.", example = "IDMGroup")
+    private String entity;
+    @ApiModelProperty(value = "Identifier of the entity.", example = "id")
+    private String identifier;
+    @ApiModelProperty(value = "Value of the identifier.", example = "1")
+    private Object identifierValue;
+    @ApiModelProperty(value = "Detailed message of the exception", example = "Group with same name already exists.")
+    private String reason;
+
+    public EntityErrorDetail() {
+    }
+
+    public EntityErrorDetail(@NotBlank String reason) {
+        this.reason = reason;
+    }
+
+    public EntityErrorDetail(@NotNull Class<?> entityClass,
+                             @NotBlank String reason) {
+        this(reason);
+        this.entity = entityClass.getSimpleName();
+    }
+
+    public EntityErrorDetail(@NotNull Class<?> entityClass,
+                             @NotBlank String identifier,
+                             @NotNull Class<?> identifierClass,
+                             @NotNull Object identifierValue,
+                             @NotBlank String reason) {
+        this(entityClass, reason);
+        this.identifier = identifier;
+        this.identifierValue = identifierClass.cast(identifierValue);
+    }
+
+    public EntityErrorDetail(@NotNull Class<?> entityClass,
+                             @NotBlank String identifier,
+                             @NotNull Class<?> identifierClass,
+                             @NotNull Object identifierValue) {
+        this.entity = entityClass.getSimpleName();
+        this.identifier = identifier;
+        this.identifierValue = identifierClass.cast(identifierValue);
+    }
+
+    public String getEntity() {
+        return entity;
+    }
+
+    public void setEntity(@NotBlank String entity) {
+        this.entity = entity;
+    }
+
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public void setIdentifier(@NotBlank String identifier) {
+        this.identifier = identifier;
+    }
+
+    public Object getIdentifierValue() {
+        return identifierValue;
+    }
+
+    public void setIdentifierValue(@NotNull Object identifierValue) {
+        this.identifierValue = identifierValue;
+    }
+
+    public String getReason() {
+        return reason;
+    }
+
+    public void setReason(@NotBlank String reason) {
+        this.reason = reason;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        EntityErrorDetail entity = (EntityErrorDetail) o;
+        return Objects.equals(getEntity(), entity.getEntity()) &&
+                Objects.equals(getIdentifier(), entity.getIdentifier()) &&
+                Objects.equals(getIdentifierValue(), entity.getIdentifierValue()) &&
+                Objects.equals(getReason(), entity.getReason());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getEntity(), getIdentifier(), getIdentifierValue(), getReason());
+    }
+
+    @Override
+    public String toString() {
+        return "EntityErrorDetail{" +
+                "entity='" + entity + '\'' +
+                ", identifier='" + identifier + '\'' +
+                ", identifierValue=" + identifierValue +
+                ", reason='" + reason + '\'' +
+                '}';
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/EntityNotFoundException.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/EntityNotFoundException.java
new file mode 100644
index 0000000000000000000000000000000000000000..c30c77f11f5c9b61521caee3d42625525b76a7dc
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/EntityNotFoundException.java
@@ -0,0 +1,37 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "The requested entity could not be found")
+public class EntityNotFoundException extends ExceptionWithEntity {
+    public EntityNotFoundException() {
+        super();
+    }
+
+    public EntityNotFoundException(EntityErrorDetail entityErrorDetail) {
+        super(entityErrorDetail);
+    }
+
+    public EntityNotFoundException(EntityErrorDetail entityErrorDetail, Throwable cause) {
+        super(entityErrorDetail, cause);
+    }
+
+    public EntityNotFoundException(Throwable cause) {
+        super(cause);
+    }
+
+    protected String createDefaultReason(EntityErrorDetail entityErrorDetail) {
+        StringBuilder reason = new StringBuilder("Entity ")
+                .append(entityErrorDetail.getEntity());
+        if (entityErrorDetail.getIdentifier() != null && entityErrorDetail.getIdentifierValue() != null) {
+            reason.append(" (")
+                    .append(entityErrorDetail.getIdentifier())
+                    .append(": ")
+                    .append(entityErrorDetail.getIdentifierValue())
+                    .append(")");
+        }
+        reason.append(" not found.");
+        return reason.toString();
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/ExceptionWithEntity.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/ExceptionWithEntity.java
new file mode 100644
index 0000000000000000000000000000000000000000..ba05f5c4634182ee020dd05cc63d169d28d4fd69
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/ExceptionWithEntity.java
@@ -0,0 +1,40 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions;
+
+public abstract class ExceptionWithEntity extends RuntimeException {
+    private EntityErrorDetail entityErrorDetail;
+
+    protected ExceptionWithEntity() {
+        super();
+    }
+
+    protected ExceptionWithEntity(EntityErrorDetail entityErrorDetail) {
+        this.entityErrorDetail = entityErrorDetail;
+        if (entityErrorDetail.getReason() == null) {
+            this.entityErrorDetail.setReason(createDefaultReason(this.entityErrorDetail));
+        }
+    }
+
+    protected ExceptionWithEntity(EntityErrorDetail entityErrorDetail, Throwable cause) {
+        super(cause);
+        this.entityErrorDetail = entityErrorDetail;
+        if (entityErrorDetail.getReason() == null) {
+            this.entityErrorDetail.setReason(createDefaultReason(this.entityErrorDetail));
+        }
+    }
+
+    protected ExceptionWithEntity(Throwable cause) {
+        super(cause);
+    }
+
+    public EntityErrorDetail getEntityErrorDetail() {
+        return entityErrorDetail;
+    }
+
+    /**
+     * Method to get default reason of error based on other attributes when no reason is provided.
+     * @param entityErrorDetail
+     * @return default detailed reason of error.
+     */
+    protected abstract String createDefaultReason(EntityErrorDetail entityErrorDetail);
+
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/ForbiddenException.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/ForbiddenException.java
new file mode 100644
index 0000000000000000000000000000000000000000..44ed5c1a9a37812c539acb3105434ace59572894
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/ForbiddenException.java
@@ -0,0 +1,24 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "Request is formed correctly, but the server doesn't want to carry it out.")
+public class ForbiddenException extends RuntimeException {
+
+    public ForbiddenException() {
+    }
+
+    public ForbiddenException(String message) {
+        super(message);
+    }
+
+    public ForbiddenException(String message, Throwable ex) {
+        super(message, ex);
+    }
+
+    public ForbiddenException(Throwable ex) {
+        super(ex);
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/InternalServerErrorException.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/InternalServerErrorException.java
new file mode 100644
index 0000000000000000000000000000000000000000..92455272ab081212c8dadefa8e6253885d088908
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/InternalServerErrorException.java
@@ -0,0 +1,24 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR,
+        reason = "A generic error message, given when an unexpected condition was encountered and no more specific message is suitable.")
+public class InternalServerErrorException extends RuntimeException {
+
+    public InternalServerErrorException() {
+    }
+
+    public InternalServerErrorException(String message) {
+        super(message);
+    }
+
+    public InternalServerErrorException(String message, Throwable ex) {
+        super(message, ex);
+    }
+
+    public InternalServerErrorException(Throwable e) {
+        super(e);
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/MicroserviceApiException.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/MicroserviceApiException.java
new file mode 100644
index 0000000000000000000000000000000000000000..75b11b18339129097707abb34f4ecdde2ee0877b
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/MicroserviceApiException.java
@@ -0,0 +1,46 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions;
+
+import cz.muni.ics.kypo.training.adaptive.exceptions.errors.ApiSubError;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+import javax.validation.ConstraintViolationException;
+
+@ResponseStatus(reason = "Error when calling external service API")
+public class MicroserviceApiException extends RuntimeException {
+    private ApiSubError apiSubError;
+
+    public MicroserviceApiException() {
+        super();
+    }
+
+    public MicroserviceApiException(ApiSubError apiSubError) {
+        super();
+        this.apiSubError = apiSubError;
+
+    }
+
+    public MicroserviceApiException(String message) {
+        super(message);
+    }
+
+    public MicroserviceApiException(String message, ConstraintViolationException exception) {
+        super(message + " Constraint violations: " + exception.getConstraintViolations().toString());
+
+    }
+
+    public MicroserviceApiException(String message, ApiSubError apiSubError) {
+        super(message);
+        this.apiSubError = apiSubError;
+    }
+
+
+    public MicroserviceApiException(ApiSubError apiSubError, Throwable cause) {
+        super(cause);
+        this.apiSubError = apiSubError;
+
+    }
+
+    public ApiSubError getApiSubError() {
+        return apiSubError;
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/ResourceNotFoundException.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/ResourceNotFoundException.java
new file mode 100644
index 0000000000000000000000000000000000000000..b65c9ad60182be09148e7fbdbe4e6dfbd2b86efd
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/ResourceNotFoundException.java
@@ -0,0 +1,24 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "The requested resource was not found")
+public class ResourceNotFoundException extends RuntimeException {
+
+    public ResourceNotFoundException() {
+    }
+
+    public ResourceNotFoundException(String message) {
+        super(message);
+    }
+
+    public ResourceNotFoundException(String message, Throwable ex) {
+        super(message, ex);
+    }
+
+    public ResourceNotFoundException(Throwable ex) {
+        super(ex);
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/ResourceNotModifiedException.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/ResourceNotModifiedException.java
new file mode 100644
index 0000000000000000000000000000000000000000..e3fc5429d22e1ced36e351c31a89204fd80d2b24
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/ResourceNotModifiedException.java
@@ -0,0 +1,24 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(value = HttpStatus.NOT_MODIFIED, reason = "The requested resource was not modified")
+public class ResourceNotModifiedException extends RuntimeException {
+
+    public ResourceNotModifiedException() {
+    }
+
+    public ResourceNotModifiedException(String message) {
+        super(message);
+    }
+
+    public ResourceNotModifiedException(String message, Throwable ex) {
+        super(message, ex);
+    }
+
+    public ResourceNotModifiedException(Throwable ex) {
+        super(ex);
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/TooManyRequestsException.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/TooManyRequestsException.java
new file mode 100644
index 0000000000000000000000000000000000000000..cce0720310bd63c08bfa1558c2eaa0954a29af0f
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/TooManyRequestsException.java
@@ -0,0 +1,38 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(value = HttpStatus.TOO_MANY_REQUESTS, reason = "The user has sent too many requests in a given amount of time (\"rate limiting\").")
+public class TooManyRequestsException extends ExceptionWithEntity {
+
+    public TooManyRequestsException() {
+        super();
+    }
+
+    public TooManyRequestsException(EntityErrorDetail entityErrorDetail) {
+        super(entityErrorDetail);
+    }
+
+    public TooManyRequestsException(EntityErrorDetail entityErrorDetail, Throwable cause) {
+        super(entityErrorDetail, cause);
+    }
+
+    public TooManyRequestsException(Throwable cause) {
+        super(cause);
+    }
+
+    protected String createDefaultReason(EntityErrorDetail entityErrorDetail) {
+        StringBuilder reason = new StringBuilder("User has sent too many requests to obtain entity ")
+                .append(entityErrorDetail.getEntity());
+        if (entityErrorDetail.getIdentifier() != null && entityErrorDetail.getIdentifierValue() != null) {
+            reason.append(" (")
+                    .append(entityErrorDetail.getIdentifier())
+                    .append(": ")
+                    .append(entityErrorDetail.getIdentifierValue())
+                    .append(")");
+        }
+        reason.append(".");
+        return reason.toString();
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/UnprocessableEntityException.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/UnprocessableEntityException.java
new file mode 100644
index 0000000000000000000000000000000000000000..a68947d846fe81f36adbf09d8b8a9d3c564d9626
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/UnprocessableEntityException.java
@@ -0,0 +1,39 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY, reason = "The requested data cannot be processed.")
+public class UnprocessableEntityException extends ExceptionWithEntity {
+
+    public UnprocessableEntityException() {
+        super();
+    }
+
+    public UnprocessableEntityException(EntityErrorDetail entityErrorDetail) {
+        super(entityErrorDetail);
+    }
+
+    public UnprocessableEntityException(EntityErrorDetail entityErrorDetail, Throwable cause) {
+        super(entityErrorDetail, cause);
+    }
+
+    public UnprocessableEntityException(Throwable cause) {
+        super(cause);
+    }
+
+    protected String createDefaultReason(EntityErrorDetail entityErrorDetail) {
+        StringBuilder reason = new StringBuilder("Unable to be process entity ")
+                .append(entityErrorDetail.getEntity());
+        if (entityErrorDetail.getIdentifier() != null && entityErrorDetail.getIdentifierValue() != null) {
+            reason.append(" (")
+                    .append(entityErrorDetail.getIdentifier())
+                    .append(": ")
+                    .append(entityErrorDetail.getIdentifierValue())
+                    .append(")");
+        }
+        reason.append(" not found.");
+        return reason.toString();
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/errors/ApiSubError.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/errors/ApiSubError.java
new file mode 100644
index 0000000000000000000000000000000000000000..2d3b5321ac530354586542845817ebc348eab4ee
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/errors/ApiSubError.java
@@ -0,0 +1,24 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions.errors;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import org.springframework.http.HttpStatus;
+
+@ApiModel(value = "ApiSubError", subTypes = {JavaApiError.class, PythonApiError.class},
+        description = "Superclass for classes JavaApiError and PythonApiError")
+@JsonSubTypes({
+        @JsonSubTypes.Type(value = JavaApiError.class, name = "JavaApiError"),
+        @JsonSubTypes.Type(value = PythonApiError.class, name = "PythonApiError")})
+public abstract class ApiSubError {
+    @ApiModelProperty(value = "The HTTP response status code", example = "404 Not found (different for each type of exception).")
+    private HttpStatus status;
+
+    public HttpStatus getStatus() {
+        return status;
+    }
+
+    public void setStatus(HttpStatus status) {
+        this.status = status;
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/errors/JavaApiError.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/errors/JavaApiError.java
new file mode 100644
index 0000000000000000000000000000000000000000..22e42035b7a193d1901908c2f3b0256ad2919f79
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/errors/JavaApiError.java
@@ -0,0 +1,147 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions.errors;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityErrorDetail;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import org.springframework.http.HttpStatus;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+@ApiModel(value = "JavaApiError", description = "A detailed error from another Java mircorservice.", parent = ApiSubError.class)
+public class JavaApiError extends ApiSubError {
+    @ApiModelProperty(value = "The time when the exception occurred", example = "1574062900 (different for each type of exception)")
+    private long timestamp;
+    @ApiModelProperty(value = "The specific description of the ApiError.", example = "The IDMGroup could not be found in database (different for each type of exception).")
+    private String message;
+    @ApiModelProperty(value = "The list of main reasons of the ApiError.", example = "[The requested resource was not found (different for each type of exception).]")
+    private List<String> errors;
+    @ApiModelProperty(value = "The requested URI path which caused error.", example = "/kypo2-rest-user-and-group/api/v1/groups/1000 (different for each type of exception).")
+    private String path;
+    @ApiModelProperty(value = "Entity detail related to the error.")
+    @JsonProperty("entity_error_detail")
+    private EntityErrorDetail entityErrorDetail;
+
+    private JavaApiError() {
+    }
+
+    public static JavaApiError of(HttpStatus httpStatus, String message, List<String> errors, String path) {
+        JavaApiError apiError = new JavaApiError();
+        apiError.setTimestamp(System.currentTimeMillis());
+        apiError.setStatus(httpStatus);
+        apiError.setMessage(message);
+        apiError.setErrors(errors);
+        apiError.setPath(path);
+        return apiError;
+    }
+
+    public static JavaApiError of(HttpStatus httpStatus, String message, String error, String path) {
+        JavaApiError apiError = new JavaApiError();
+        apiError.setTimestamp(System.currentTimeMillis());
+        apiError.setStatus(httpStatus);
+        apiError.setMessage(message);
+        apiError.setError(error);
+        apiError.setPath(path);
+        return apiError;
+    }
+
+    public static JavaApiError of(HttpStatus httpStatus, String message, List<String> errors) {
+        JavaApiError apiError = new JavaApiError();
+        apiError.setTimestamp(System.currentTimeMillis());
+        apiError.setStatus(httpStatus);
+        apiError.setMessage(message);
+        apiError.setErrors(errors);
+        apiError.setPath("");
+        return apiError;
+    }
+
+    public static JavaApiError of(HttpStatus httpStatus, String message, String error) {
+        JavaApiError apiError = new JavaApiError();
+        apiError.setTimestamp(System.currentTimeMillis());
+        apiError.setStatus(httpStatus);
+        apiError.setMessage(message);
+        apiError.setError(error);
+        apiError.setPath("");
+        return apiError;
+    }
+
+    public EntityErrorDetail getEntityErrorDetail() {
+        return entityErrorDetail;
+    }
+
+    public void setEntityErrorDetail(EntityErrorDetail entityErrorDetail) {
+        this.entityErrorDetail = entityErrorDetail;
+    }
+
+    public long getTimestamp() {
+        return timestamp;
+    }
+
+    public void setTimestamp(long timestamp) {
+        this.timestamp = timestamp;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(final String message) {
+        this.message = message;
+    }
+
+    public List<String> getErrors() {
+        return errors;
+    }
+
+    public void setErrors(final List<String> errors) {
+        this.errors = errors;
+    }
+
+    public void setError(final String error) {
+        errors = Arrays.asList(error);
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    public void setPath(String path) {
+        this.path = path;
+    }
+
+    @Override
+    public String toString() {
+        return "ApiError{" +
+                "timestamp=" + timestamp +
+                ", status=" + getStatus() +
+                ", message='" + message + '\'' +
+                ", errors=" + errors +
+                ", path='" + path + '\'' +
+                ", entityErrorDetail=" + entityErrorDetail +
+                '}';
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(timestamp, getStatus(), message, errors, path);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (!(obj instanceof JavaApiError))
+            return false;
+        JavaApiError other = (JavaApiError) obj;
+        return Objects.equals(errors, other.getErrors()) &&
+                Objects.equals(message, other.getMessage()) &&
+                Objects.equals(path, other.getPath()) &&
+                Objects.equals(getStatus(), other.getStatus()) &&
+                Objects.equals(timestamp, other.getTimestamp());
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/errors/PythonApiError.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/errors/PythonApiError.java
new file mode 100644
index 0000000000000000000000000000000000000000..bc61e89789ef78370cd8428c618fc87f1afb3c15
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/errors/PythonApiError.java
@@ -0,0 +1,62 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions.errors;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+import java.util.Map;
+import java.util.Objects;
+
+@ApiModel(value = "PythonApiError", description = "A detailed error from another Python mircorservice.", parent = ApiSubError.class)
+public class PythonApiError extends ApiSubError {
+
+    @ApiModelProperty(value = "Detail message of the error.", example = "Sandbox could not be found.")
+    private String detail;
+    @ApiModelProperty(value = "Parameters to specify details of the error.", example = "name: sandbox" )
+    private Map<String, String> parameters;
+
+    public PythonApiError() {
+    }
+
+    public PythonApiError(String detail) {
+        this.detail = detail;
+    }
+
+    public String getDetail() {
+        return detail;
+    }
+
+    public void setDetail(String detail) {
+        this.detail = detail;
+    }
+
+    public Map<String, String> getParameters() {
+        return parameters;
+    }
+
+    public void setParameters(Map<String, String> parameters) {
+        this.parameters = parameters;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof PythonApiError)) return false;
+        PythonApiError that = (PythonApiError) o;
+        return Objects.equals(getDetail(), that.getDetail()) &&
+                Objects.equals(getParameters(), that.getParameters());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getDetail(), getParameters());
+    }
+
+    @Override
+    public String toString() {
+        return "PythonApiError{" +
+                "detail='" + detail + '\'' +
+                ", status=" + getStatus() +
+                ", parameters=" + parameters +
+                '}';
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/responsehandlers/JavaApiResponseErrorHandler.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/responsehandlers/JavaApiResponseErrorHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..317e68f21dec639e237703787287a894f142f67d
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/responsehandlers/JavaApiResponseErrorHandler.java
@@ -0,0 +1,45 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions.responsehandlers;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import cz.muni.ics.kypo.training.adaptive.exceptions.CustomWebClientException;
+import cz.muni.ics.kypo.training.adaptive.exceptions.errors.JavaApiError;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.util.StreamUtils;
+import org.springframework.web.client.ResponseErrorHandler;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+
+import static org.springframework.http.HttpStatus.Series.CLIENT_ERROR;
+import static org.springframework.http.HttpStatus.Series.SERVER_ERROR;
+
+public class JavaApiResponseErrorHandler implements ResponseErrorHandler {
+
+    private ObjectMapper mapper;
+
+    /**
+     * Instantiates a new Python api response error handler.
+     *
+     * @param mapper the mapper
+     */
+    public JavaApiResponseErrorHandler(ObjectMapper mapper) {
+        this.mapper = mapper;
+    }
+
+    @Override
+    public boolean hasError(ClientHttpResponse response) throws IOException {
+        return (
+                response.getStatusCode().series() == CLIENT_ERROR
+                        || response.getStatusCode().series() == SERVER_ERROR);
+    }
+
+    @Override
+    public void handleError(ClientHttpResponse response) throws IOException {
+        String responseBody = StreamUtils.copyToString(response.getBody(), Charset.defaultCharset());
+        if(responseBody.isBlank()) {
+            throw new CustomWebClientException("Error from external microservice. No specific message provided.", response.getStatusCode());
+        }
+        JavaApiError javaApiError = mapper.readValue(responseBody, JavaApiError.class);
+        throw new CustomWebClientException(javaApiError);
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/responsehandlers/PythonApiResponseErrorHandler.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/responsehandlers/PythonApiResponseErrorHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..a6b97d58db5096faeb17f7a1509d6add51c3b390
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/responsehandlers/PythonApiResponseErrorHandler.java
@@ -0,0 +1,49 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions.responsehandlers;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import cz.muni.ics.kypo.training.adaptive.exceptions.CustomWebClientException;
+import cz.muni.ics.kypo.training.adaptive.exceptions.errors.PythonApiError;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.util.StreamUtils;
+import org.springframework.web.client.ResponseErrorHandler;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+
+import static org.springframework.http.HttpStatus.Series.CLIENT_ERROR;
+import static org.springframework.http.HttpStatus.Series.SERVER_ERROR;
+
+/**
+ * Handler used for errors returned from Python api.
+ */
+public class PythonApiResponseErrorHandler implements ResponseErrorHandler {
+
+    private ObjectMapper mapper;
+
+    /**
+     * Instantiates a new Python api response error handler.
+     *
+     * @param mapper the mapper
+     */
+    public PythonApiResponseErrorHandler(ObjectMapper mapper) {
+        this.mapper = mapper;
+    }
+
+    @Override
+    public boolean hasError(ClientHttpResponse response) throws IOException {
+        return (
+            response.getStatusCode().series() == CLIENT_ERROR
+            || response.getStatusCode().series() == SERVER_ERROR);
+    }
+
+    @Override
+    public void handleError(ClientHttpResponse response) throws IOException {
+        String responseBody = StreamUtils.copyToString(response.getBody(), Charset.defaultCharset());
+        if(responseBody.isBlank()) {
+            throw new CustomWebClientException("Error from external microservice. No specific message provided.", response.getStatusCode());
+        }
+        PythonApiError pythonApiError = mapper.readValue(response.getBody(), PythonApiError.class);
+        pythonApiError.setStatus(response.getStatusCode());
+        throw new CustomWebClientException(pythonApiError);
+    }
+}