diff --git a/src/main/java/it/inaf/oats/vospace/TransferController.java b/src/main/java/it/inaf/oats/vospace/TransferController.java
index 4b1e68ceea02b804a65d39fc88f04cab58591b04..1618fed28bb3bc07993c04cd8534edd4cab2c402 100644
--- a/src/main/java/it/inaf/oats/vospace/TransferController.java
+++ b/src/main/java/it/inaf/oats/vospace/TransferController.java
@@ -1,18 +1,90 @@
 package it.inaf.oats.vospace;
 
+import it.inaf.ia2.aa.data.User;
+import it.inaf.oats.vospace.persistence.JobDAO;
+import java.util.UUID;
+import javax.servlet.http.HttpServletRequest;
+import net.ivoa.xml.uws.v1.ExecutionPhase;
+import net.ivoa.xml.uws.v1.JobSummary;
 import net.ivoa.xml.vospace.v2.Transfer;
+import org.springframework.beans.factory.annotation.Autowired;
+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.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 @RestController
 public class TransferController {
 
-    @PostMapping(value = "/transfer",
+    @Autowired
+    private JobDAO jobDAO;
+
+    @Autowired
+    private HttpServletRequest request;
+
+    @Autowired
+    private UriService uriService;
+
+    @PostMapping(value = "/transfers",
             consumes = {MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE},
-             produces = {MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE})
-    public Transfer postTransfer(@RequestBody Transfer transfer) {
-        return transfer;
+            produces = {MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE})
+    public ResponseEntity<?> postTransfer(@RequestBody Transfer transfer, User principal) {
+
+        String jobId = UUID.randomUUID().toString().replace("-", "");
+
+        JobSummary jobSummary = new JobSummary();
+        jobSummary.setJobId(jobId);
+        jobSummary.setOwnerId(principal.getName());
+        jobSummary.setPhase(ExecutionPhase.PENDING);
+        JobSummary.JobInfo jobInfo = new JobSummary.JobInfo();
+        jobInfo.getAny().add(transfer);
+        jobSummary.setJobInfo(jobInfo);
+
+        jobDAO.createJob(jobSummary);
+
+        return getJobRedirect(jobId);
+    }
+
+    @GetMapping(value = "/transfers/{jobId}", produces = {MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE})
+    public ResponseEntity<JobSummary> getJob(@PathVariable("jobId") String jobId) {
+        return jobDAO.getJob(jobId).map(j -> ResponseEntity.ok(j)).orElse(ResponseEntity.notFound().build());
+    }
+
+    @PostMapping(value = "/transfers/{jobId}/phase")
+    public ResponseEntity<?> setJobPhase(@PathVariable("jobId") String jobId, @RequestParam("PHASE") String phase, User principal) {
+
+        return jobDAO.getJob(jobId).map(job -> {
+            if (!job.getOwnerId().equals(principal.getName())) {
+                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
+            }
+
+            // TODO: check allowed job phase transitions
+            switch (phase) {
+                case "RUN":
+                    job.setPhase(ExecutionPhase.EXECUTING);
+                    uriService.setTransferJobResult(job);
+                    jobDAO.updateJob(job);
+                    break;
+                case "ABORT":
+                    throw new UnsupportedOperationException("Not implemented yet");
+                default:
+                    throw new IllegalArgumentException("Invalid phase parameter: " + phase);
+            }
+
+            return getJobRedirect(job.getJobId());
+
+        }).orElse(ResponseEntity.notFound().build());
+    }
+
+    private ResponseEntity<?> getJobRedirect(String jobId) {
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("Location", request.getContextPath() + "/transfers/" + jobId);
+        return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER);
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/it/inaf/oats/vospace/UriService.java b/src/main/java/it/inaf/oats/vospace/UriService.java
new file mode 100644
index 0000000000000000000000000000000000000000..0d28a0ed44cb483cf0a808d99dcbf1107f89868e
--- /dev/null
+++ b/src/main/java/it/inaf/oats/vospace/UriService.java
@@ -0,0 +1,52 @@
+package it.inaf.oats.vospace;
+
+import it.inaf.oats.vospace.persistence.NodeDAO;
+import java.util.ArrayList;
+import java.util.List;
+import net.ivoa.xml.uws.v1.JobSummary;
+import net.ivoa.xml.uws.v1.ResultReference;
+import net.ivoa.xml.vospace.v2.Node;
+import net.ivoa.xml.vospace.v2.Transfer;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UriService {
+
+    @Value("${vospace-authority}")
+    private String authority;
+
+    @Value("${file-service-url}")
+    private String fileServiceUrl;
+
+    @Autowired
+    private NodeDAO nodeDao;
+
+    public void setTransferJobResult(JobSummary job) {
+
+        List<ResultReference> results = new ArrayList<>();
+
+        ResultReference result = new ResultReference();
+        result.setHref(getUri(job));
+        results.add(result);
+
+        job.setResults(results);
+    }
+
+    private String getUri(JobSummary job) {
+
+        // TODO add checks on data type
+        Transfer transfer = (Transfer) job.getJobInfo().getAny().get(0);
+
+        String relativePath = transfer.getTarget().substring("vos://".length() + authority.length());
+
+        // TODO handle node not found
+        Node node = nodeDao.listNode(relativePath).get();
+
+        // TODO build the path according to node type
+        //
+        // TODO add token for authenticated access
+        return fileServiceUrl + relativePath + "?jobId=" + job.getJobId();
+    }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 8b2697437b3ca10bdf9179f3c7f47b0dea7f31dc..aa0868511f095d90ae369c60372f4a698af7063b 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -34,3 +34,5 @@ logging.level.org.springframework.web=TRACE
 #logging.file=path/to/log/file.log
 
 vospace-authority=example.com!vospace
+
+file-service-url=http://localhost:8087
\ No newline at end of file
diff --git a/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java b/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..d8f401827d7a1aefdc72704beff23bf58b3e4e4e
--- /dev/null
+++ b/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java
@@ -0,0 +1,52 @@
+package it.inaf.oats.vospace;
+
+import static it.inaf.oats.vospace.VOSpaceXmlTestUtil.loadDocument;
+import it.inaf.oats.vospace.persistence.JobDAO;
+import java.util.Optional;
+import net.ivoa.xml.uws.v1.JobSummary;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.Test;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+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.http.MediaType;
+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.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import org.w3c.dom.Document;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+public class TransferControllerTest {
+
+    @MockBean
+    private JobDAO dao;
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @Test
+    public void testGetJob() throws Exception {
+
+        JobSummary job = new JobSummary();
+
+        when(dao.getJob(eq("123"))).thenReturn(Optional.of(job));
+
+        String xml = mockMvc.perform(get("/transfers/123")
+                .accept(MediaType.APPLICATION_XML))
+                .andDo(print())
+                .andExpect(status().isOk())
+                .andReturn().getResponse().getContentAsString();
+
+        Document doc = loadDocument(xml);
+        assertEquals("uws:job", doc.getDocumentElement().getNodeName());
+
+        verify(dao, times(1)).getJob(eq("123"));
+    }
+}
diff --git a/src/test/java/it/inaf/oats/vospace/UriServiceTest.java b/src/test/java/it/inaf/oats/vospace/UriServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..7112eac8c679b40cec8d6d5b6f4a4110215c86c4
--- /dev/null
+++ b/src/test/java/it/inaf/oats/vospace/UriServiceTest.java
@@ -0,0 +1,52 @@
+package it.inaf.oats.vospace;
+
+import it.inaf.oats.vospace.persistence.NodeDAO;
+import java.util.Optional;
+import net.ivoa.xml.uws.v1.JobSummary;
+import net.ivoa.xml.vospace.v2.DataNode;
+import net.ivoa.xml.vospace.v2.Node;
+import net.ivoa.xml.vospace.v2.Transfer;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.Test;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+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.context.TestPropertySource;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+@TestPropertySource(properties = {"vospace-authority=example.com!vospace", "file-service-url=http://file-service"})
+public class UriServiceTest {
+
+    @MockBean
+    private NodeDAO dao;
+
+    @Autowired
+    private UriService transferService;
+
+    @Test
+    public void testSimpleUrl() {
+
+        Node node = new DataNode();
+
+        when(dao.listNode(eq("/mydata1"))).thenReturn(Optional.of(node));
+
+        Transfer transfer = new Transfer();
+        transfer.setTarget("vos://example.com!vospace/mydata1");
+
+        JobSummary job = new JobSummary();
+        job.setJobId("job-id");
+
+        JobSummary.JobInfo jobInfo = new JobSummary.JobInfo();
+        jobInfo.getAny().add(transfer);
+
+        job.setJobInfo(jobInfo);
+
+        transferService.setTransferJobResult(job);
+
+        assertEquals("http://file-service/mydata1?jobId=job-id", job.getResults().get(0).getHref());
+    }
+}