diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java
index 9cfa265bca80dbb957c26008ee923795a4f04fbd..c7bb9c48f643964c4e6a1a890ff2d6a5229f4117 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java
@@ -14,6 +14,7 @@ import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
 import java.net.http.HttpResponse.BodyHandlers;
+import java.util.List;
 import java.util.Scanner;
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.ForkJoinPool;
@@ -23,6 +24,7 @@ import javax.servlet.http.HttpSession;
 import javax.xml.bind.JAXB;
 import net.ivoa.xml.uws.v1.JobSummary;
 import net.ivoa.xml.vospace.v2.Node;
+import net.ivoa.xml.vospace.v2.Protocol;
 import net.ivoa.xml.vospace.v2.Transfer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -82,6 +84,17 @@ public class VOSpaceClient {
         return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, JobSummary.class));
     }
 
+    public List<Protocol> getDownloadEndpoints(Transfer transfer) {
+
+        HttpRequest request = getRequest("/synctrans")
+                .header("Accept", useJson ? "application/json" : "text/xml")
+                .header("Content-Type", useJson ? "application/json" : "text/xml")
+                .POST(HttpRequest.BodyPublishers.ofString(marshal(transfer)))
+                .build();
+
+        return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Transfer.class)).getProtocols();
+    }
+
     private <T, U> U call(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler, int expectedStatusCode, Function<T, U> responseHandler) {
         try {
             return httpClient.sendAsync(request, responseBodyHandler)
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java
index 5ef1a00529f3f16fc1936c1b69417ef5a20f3498..904a26b4e08350365899079bc9706cc27cb38af9 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java
@@ -38,7 +38,7 @@ public class JobController {
         transfer.setTarget("vos://" + authority + paths.get(0));
         Protocol protocol = new Protocol();
         protocol.setUri("ia2:tape-recall");
-        transfer.getProtocol().add(protocol);
+        transfer.getProtocols().add(protocol);
 
         JobSummary job = client.startTransferJob(transfer);
 
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java
index af16e5202a176e391d2eafda7dabee7d5fe397da..199fbfc642657fdeb6b9c67745f65e0a16968833 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java
@@ -1,19 +1,33 @@
 package it.inaf.ia2.vospace.ui.controller;
 
+import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
 import it.inaf.ia2.vospace.ui.service.NodesService;
 import javax.servlet.http.HttpServletRequest;
+import net.ivoa.xml.vospace.v2.Transfer;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RestController;
 
 @RestController
 public class NodesController {
 
+    @Value("${vospace-authority}")
+    private String authority;
+
     @Autowired
     private NodesService nodesService;
 
+    @Autowired
+    private VOSpaceClient client;
+
+    @Autowired
+    private HttpServletRequest servletRequest;
+
     /**
      * This is the only API endpoint that returns HTML code instead of JSON. The
      * reason is that JavaScript frameworks are not very efficient in handling
@@ -23,21 +37,42 @@ public class NodesController {
      * document.
      */
     @GetMapping(value = {"/nodes", "/nodes/**"}, produces = MediaType.TEXT_PLAIN_VALUE)
-    public String listNodes(HttpServletRequest request) throws Exception {
+    public String listNodes() throws Exception {
+
+        String path = getPath("/nodes/");
+
+        return nodesService.generateNodesHtml(path);
+    }
+
+    @GetMapping(value = "/download/**")
+    public ResponseEntity<?> directDownload() {
 
-        String requestURL = request.getRequestURL().toString();
-        String[] split = requestURL.split("/nodes/");
+        String path = getPath("/download/");
+
+        Transfer transfer = new Transfer();
+        transfer.setDirection("pullFromVoSpace");
+        transfer.setTarget("vos://" + authority + path);
+
+        String url = client.getDownloadEndpoints(transfer).get(0).getEndpoint();
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("Location", url);
+        return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER);
+    }
+
+    /**
+     * Slash is a special character in defining REST endpoints and trying to
+     * define a PathVariable containing slashes doesn't work, so the endpoint
+     * has been defined using "/nodes/**" instead of "/nodes/{path}" and the
+     * path is extracted manually parsing the request URL.
+     */
+    protected String getPath(String prefix) {
+        String requestURL = servletRequest.getRequestURL().toString();
+        String[] split = requestURL.split(prefix);
 
         String path = "/";
         if (split.length == 2) {
             path += split[1];
         }
-
-        return nodesService.generateNodesHtml(path);
-    }
-
-    @GetMapping(value = "/download/{path}")
-    public void directDownload(@PathVariable("path") String path) {
-        // TODO: call pullFromVoSpace sync transfer
+        return path;
     }
 }
diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java
index dc127f7c5e6645fd9b0ca5be55cbf8161d17698c..9bd05f73b36de314bde2a51041f78c45fbba343d 100644
--- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java
+++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java
@@ -3,26 +3,28 @@ package it.inaf.ia2.vospace.ui.controller;
 import it.inaf.ia2.vospace.ui.service.NodesService;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
 import static org.mockito.ArgumentMatchers.eq;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
 import static org.mockito.Mockito.verify;
-import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.test.web.servlet.MockMvc;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 import org.springframework.test.web.servlet.setup.MockMvcBuilders;
 
-@ExtendWith(MockitoExtension.class)
+@SpringBootTest
+@AutoConfigureMockMvc
 public class NodesControllerTest {
 
-    @Mock
+    @MockBean
     private NodesService nodesService;
 
-    @InjectMocks
+    @Autowired
     private NodesController controller;
 
+    @Autowired
     private MockMvc mockMvc;
 
     @BeforeEach