Index: backend/pom.xml
===================================================================
--- backend/pom.xml	(revision bee3afb8cd1e741c5eff5e30a0d90f874ad19d83)
+++ backend/pom.xml	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -144,4 +144,35 @@
 			<version>3.2.0</version>
 		</dependency>
+
+		<!-- Google Api -->
+		<dependency>
+			<groupId>com.google.guava</groupId>
+			<artifactId>guava</artifactId>
+			<version>32.1.2-jre</version>
+		</dependency>
+		<dependency>
+			<groupId>com.google.api-client</groupId>
+			<artifactId>google-api-client</artifactId>
+			<version>2.0.0</version>
+		</dependency>
+		<dependency>
+			<groupId>com.google.apis</groupId>
+			<artifactId>google-api-services-calendar</artifactId>
+			<version>v3-rev20250404-2.0.0</version>
+		</dependency>
+		<dependency>
+			<groupId>com.google.http-client</groupId>
+			<artifactId>google-http-client-jackson2</artifactId>
+			<version>1.45.0</version>
+		</dependency>
+
+		<!-- Email -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-mail</artifactId>
+		</dependency>
+
+
+
 	</dependencies>
 
Index: backend/src/main/java/com/shifterwebapp/shifter/ShifterApplication.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/ShifterApplication.java	(revision bee3afb8cd1e741c5eff5e30a0d90f874ad19d83)
+++ backend/src/main/java/com/shifterwebapp/shifter/ShifterApplication.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -4,5 +4,7 @@
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
 
+@EnableScheduling
 @SpringBootApplication
 public class ShifterApplication {
@@ -15,7 +17,9 @@
 		// Set env variables from .env file
 		System.setProperty("JWT_CONFIG_SECRET", dotenv.get("JWT_CONFIG_SECRET"));
+
 		System.setProperty("SPRING_DATASOURCE_URL", dotenv.get("SPRING_DATASOURCE_URL"));
 		System.setProperty("SPRING_DATASOURCE_USERNAME", dotenv.get("SPRING_DATASOURCE_USERNAME"));
 		System.setProperty("SPRING_DATASOURCE_PASSWORD", dotenv.get("SPRING_DATASOURCE_PASSWORD"));
+
 		System.setProperty("AWS_S3_REGION", dotenv.get("AWS_S3_REGION"));
 		System.setProperty("AWS_S3_BUCKET_NAME", dotenv.get("AWS_S3_BUCKET_NAME"));
@@ -23,4 +27,18 @@
 		System.setProperty("AWS_S3_SECRET_KEY", dotenv.get("AWS_S3_SECRET_KEY"));
 
+		System.setProperty("GOOGLE_CLIENT_ID", dotenv.get("GOOGLE_CLIENT_ID"));
+		System.setProperty("GOOGLE_CLIENT_SECRET", dotenv.get("GOOGLE_CLIENT_SECRET"));
+		System.setProperty("GOOGLE_EXPERT_CALENDAR_ID", dotenv.get("GOOGLE_EXPERT_CALENDAR_ID"));
+		System.setProperty("GOOGLE_REFRESH_TOKEN", dotenv.get("GOOGLE_REFRESH_TOKEN"));
+
+		System.setProperty("EMAIL_HOST", dotenv.get("EMAIL_HOST"));
+		System.setProperty("EMAIL_PORT", dotenv.get("EMAIL_PORT"));
+		System.setProperty("EMAIL_USERNAME", dotenv.get("EMAIL_USERNAME"));
+		System.setProperty("EMAIL_PASSWORD", dotenv.get("EMAIL_PASSWORD"));
+
+		System.setProperty("ZOOM_ACCOUNT_ID", dotenv.get("ZOOM_ACCOUNT_ID"));
+		System.setProperty("ZOOM_CLIENT_ID", dotenv.get("ZOOM_CLIENT_ID"));
+		System.setProperty("ZOOM_CLIENT_SECRET", dotenv.get("ZOOM_CLIENT_SECRET"));
+
 		SpringApplication.run(ShifterApplication.class, args);
 	}
Index: backend/src/main/java/com/shifterwebapp/shifter/exception/GlobalExceptionHandler.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/exception/GlobalExceptionHandler.java	(revision bee3afb8cd1e741c5eff5e30a0d90f874ad19d83)
+++ backend/src/main/java/com/shifterwebapp/shifter/exception/GlobalExceptionHandler.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -35,4 +35,21 @@
     }
 
+    @ExceptionHandler(TimeSlotUnavailableException.class)
+    public ResponseEntity<ErrorResponse> handleTimeSlotUnavailableException(TimeSlotUnavailableException ex) {
+        return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponse(ex.getMessage()));
+    }
+
+    @ExceptionHandler(GoogleCalendarException.class)
+    public ResponseEntity<String> handleGoogleCalendarException(GoogleCalendarException e) {
+        e.printStackTrace();
+        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Google Calendar error: " + e.getMessage());
+    }
+
+    @ExceptionHandler(ZoomMeetingException.class)
+    public ResponseEntity<String> handleZoomMeetingException(ZoomMeetingException e) {
+        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Zoom API error: " + e.getMessage());
+    }
+
+
     @ExceptionHandler(RuntimeException.class)
     public ResponseEntity<ErrorResponse> handleRuntimeException(RuntimeException ex) {
Index: backend/src/main/java/com/shifterwebapp/shifter/exception/GoogleCalendarException.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/exception/GoogleCalendarException.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
+++ backend/src/main/java/com/shifterwebapp/shifter/exception/GoogleCalendarException.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -0,0 +1,7 @@
+package com.shifterwebapp.shifter.exception;
+
+public class GoogleCalendarException extends RuntimeException {
+    public GoogleCalendarException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
Index: backend/src/main/java/com/shifterwebapp/shifter/exception/TimeSlotUnavailableException.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/exception/TimeSlotUnavailableException.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
+++ backend/src/main/java/com/shifterwebapp/shifter/exception/TimeSlotUnavailableException.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -0,0 +1,7 @@
+package com.shifterwebapp.shifter.exception;
+
+public class TimeSlotUnavailableException extends RuntimeException {
+    public TimeSlotUnavailableException(String message) {
+        super(message);
+    }
+}
Index: backend/src/main/java/com/shifterwebapp/shifter/exception/ZoomMeetingException.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/exception/ZoomMeetingException.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
+++ backend/src/main/java/com/shifterwebapp/shifter/exception/ZoomMeetingException.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -0,0 +1,7 @@
+package com.shifterwebapp.shifter.exception;
+
+public class ZoomMeetingException extends RuntimeException {
+    public ZoomMeetingException(String message) {
+        super(message);
+    }
+}
Index: backend/src/main/java/com/shifterwebapp/shifter/external/GoogleCalendarService.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/external/GoogleCalendarService.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
+++ backend/src/main/java/com/shifterwebapp/shifter/external/GoogleCalendarService.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -0,0 +1,247 @@
+package com.shifterwebapp.shifter.external;
+
+import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.api.client.auth.oauth2.Credential;
+import com.google.api.client.util.DateTime;
+import com.google.api.services.calendar.Calendar;
+import com.google.api.services.calendar.model.*;
+import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
+import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
+import com.google.api.client.util.store.MemoryDataStoreFactory;
+
+import java.io.StringReader;
+import java.util.Collections;
+
+import com.shifterwebapp.shifter.exception.GoogleCalendarException;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.stereotype.Service;
+
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.*;
+
+@Service
+public class GoogleCalendarService {
+
+    private static final String APPLICATION_NAME = "Shifter App";
+    private static final GsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();
+
+    private final String clientId = System.getProperty("GOOGLE_CLIENT_ID");
+    private final String clientSecret = System.getProperty("GOOGLE_CLIENT_SECRET");
+
+    @Getter
+    private final String expertCalendarId = System.getProperty("GOOGLE_EXPERT_CALENDAR_ID");
+
+    @Getter
+    @Setter
+    private String refreshToken = System.getProperty("GOOGLE_REFRESH_TOKEN");
+
+
+    /**
+     * Builds Google Credential object with tokens.
+     * Automatically refreshes access token if expired.
+     */
+    private Credential getCredential() throws Exception {
+        var httpTransport = GoogleNetHttpTransport.newTrustedTransport();
+        var jsonFactory = GsonFactory.getDefaultInstance();
+
+        // Build client secrets
+        String clientSecretsJson = String.format("""
+        {
+          "installed": {
+            "client_id": "%s",
+            "client_secret": "%s",
+            "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+            "token_uri": "https://oauth2.googleapis.com/token"
+          }
+        }
+        """, clientId, clientSecret);
+
+        GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(jsonFactory, new StringReader(clientSecretsJson));
+
+        GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(
+                httpTransport,
+                jsonFactory,
+                clientSecrets,
+                Collections.singleton("https://www.googleapis.com/auth/calendar.events") // only this scope
+        )
+                .setDataStoreFactory(new MemoryDataStoreFactory())
+                .setAccessType("offline")
+                .setApprovalPrompt("force")
+                .build();
+
+        // Create credential using refresh token
+        var tokenResponse = new com.google.api.client.auth.oauth2.TokenResponse()
+                .setRefreshToken(refreshToken);
+
+        Credential credential = flow.createAndStoreCredential(tokenResponse, "user-id");
+
+        // Refresh if needed
+        if (credential.getExpiresInSeconds() == null || credential.getExpiresInSeconds() <= 60) {
+            if (!credential.refreshToken()) {
+                throw new RuntimeException("Failed to refresh Google access token");
+            }
+        }
+
+        return credential;
+    }
+
+
+
+    public Map<String, List<String>> computeFreeSlotsByDate(
+            List<TimeInterval> busySlots,
+            ZonedDateTime scheduleStart,
+            ZonedDateTime scheduleEnd,
+            int slotDurationMinutes,
+            ZoneId userZone) {
+
+        Map<String, List<String>> freeSlotsByDate = new LinkedHashMap<>();
+
+        ZonedDateTime cursorDay = scheduleStart.truncatedTo(ChronoUnit.DAYS);
+
+        while (cursorDay.isBefore(scheduleEnd)) {
+            DayOfWeek dayOfWeek = cursorDay.getDayOfWeek();
+
+            if (dayOfWeek != DayOfWeek.SATURDAY && dayOfWeek != DayOfWeek.SUNDAY) {
+                ZonedDateTime workdayStart = cursorDay.withHour(8).withMinute(0);
+                ZonedDateTime workdayEnd = cursorDay.withHour(16).withMinute(30);
+
+                ZonedDateTime cursorSlot = workdayStart;
+
+                while (cursorSlot.plusMinutes(slotDurationMinutes).compareTo(workdayEnd) <= 0) {
+                    ZonedDateTime slotEnd = cursorSlot.plusMinutes(slotDurationMinutes);
+
+                    Instant slotStartInstant = cursorSlot.toInstant();
+                    Instant slotEndInstant = slotEnd.toInstant();
+
+                    boolean overlaps = busySlots.stream()
+                            .anyMatch(busy -> busy.overlaps(slotStartInstant, slotEndInstant));
+
+                    if (!overlaps && cursorSlot.isAfter(scheduleStart)) {
+                        ZonedDateTime userTimeSlot = cursorSlot.withZoneSameInstant(userZone);
+                        String date = userTimeSlot.toLocalDate().toString();
+                        String time = userTimeSlot.toLocalTime().truncatedTo(ChronoUnit.MINUTES).toString();
+
+                        freeSlotsByDate.computeIfAbsent(date, k -> new ArrayList<>()).add(time);
+                    }
+
+                    cursorSlot = cursorSlot.plusMinutes(slotDurationMinutes);
+                }
+            }
+            cursorDay = cursorDay.plusDays(1);
+        }
+
+        return freeSlotsByDate;
+    }
+
+
+    public boolean isSlotFree(FreeBusyResponse freeBusy, ZonedDateTime start, ZonedDateTime end) {
+        DayOfWeek startDay = start.getDayOfWeek();
+        DayOfWeek endDay = end.getDayOfWeek();
+        if (startDay == DayOfWeek.SATURDAY || startDay == DayOfWeek.SUNDAY
+                || endDay == DayOfWeek.SATURDAY || endDay == DayOfWeek.SUNDAY) {
+            return false;
+        }
+
+        var busyList = freeBusy.getCalendars().get(getExpertCalendarId()).getBusy();
+        for (var interval : busyList) {
+            Instant busyStart = Instant.ofEpochMilli(interval.getStart().getValue());
+            Instant busyEnd = Instant.ofEpochMilli(interval.getEnd().getValue());
+            TimeInterval busyInterval = new TimeInterval(busyStart, busyEnd);
+            if (busyInterval.overlaps(start.toInstant(), end.toInstant())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public FreeBusyResponse queryFreeBusy(Instant start, Instant end) {
+        try {
+            Calendar service = new Calendar.Builder(
+                    GoogleNetHttpTransport.newTrustedTransport(),
+                    JSON_FACTORY,
+                    getCredential()
+            )
+                    .setApplicationName(APPLICATION_NAME)
+                    .build();
+
+            FreeBusyRequest fbRequest = new FreeBusyRequest();
+            fbRequest.setTimeMin(new DateTime(Date.from(start)));
+            fbRequest.setTimeMax(new DateTime(Date.from(end)));
+            fbRequest.setTimeZone("Europe/Skopje");
+            fbRequest.setItems(Collections.singletonList(
+                    new FreeBusyRequestItem().setId(expertCalendarId)
+            ));
+
+            return service.freebusy().query(fbRequest).execute();
+
+        } catch (Exception e) {
+            throw new GoogleCalendarException("Failed to query free/busy times", e);
+        }
+    }
+
+    public String createEvent(String title, Instant start, Instant end) {
+        try {
+            Calendar service = new Calendar.Builder(
+                    GoogleNetHttpTransport.newTrustedTransport(),
+                    JSON_FACTORY,
+                    getCredential()
+            )
+                    .setApplicationName(APPLICATION_NAME)
+                    .build();
+
+            Event event = new Event()
+                    .setSummary(title)
+                    .setStart(new EventDateTime()
+                            .setDateTime(new DateTime(start.toEpochMilli()))
+                            .setTimeZone("Europe/Skopje"))
+                    .setEnd(new EventDateTime()
+                            .setDateTime(new DateTime(end.toEpochMilli()))
+                            .setTimeZone("Europe/Skopje"));
+
+            Event createdEvent = service.events().insert(getExpertCalendarId(), event).execute();
+
+            return createdEvent.getId();
+        } catch (Exception e) {
+            throw new GoogleCalendarException("Failed to create event", e);
+        }
+    }
+
+    public void deleteEvent(String eventId) {
+        try {
+            Calendar service = new Calendar.Builder(
+                    GoogleNetHttpTransport.newTrustedTransport(),
+                    JSON_FACTORY,
+                    getCredential()
+            )
+                    .setApplicationName(APPLICATION_NAME)
+                    .build();
+
+            service.events().delete(getExpertCalendarId(), eventId).execute();
+        } catch (Exception e) {
+            throw new GoogleCalendarException("Failed to delete event with ID: " + eventId, e);
+        }
+    }
+
+
+
+    /**
+     * Helper class for time intervals
+     */
+    public static class TimeInterval {
+        private final Instant start;
+        private final Instant end;
+
+        public TimeInterval(Instant start, Instant end) {
+            this.start = start;
+            this.end = end;
+        }
+
+        public boolean overlaps(Instant otherStart, Instant otherEnd) {
+            return !start.isAfter(otherEnd) && !end.isBefore(otherStart); // This checks if the intervals overlap. If 12.00-12.30 is taken, 11.30-12.00 and 12.30-13.00 isnt free
+        }
+    }
+}
Index: backend/src/main/java/com/shifterwebapp/shifter/external/ZoomService.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/external/ZoomService.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
+++ backend/src/main/java/com/shifterwebapp/shifter/external/ZoomService.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -0,0 +1,93 @@
+package com.shifterwebapp.shifter.external;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.shifterwebapp.shifter.exception.ZoomMeetingException;
+import com.shifterwebapp.shifter.meeting.ZoomMeetingRequest;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.http.*;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+@Service
+public class ZoomService {
+
+    public JsonNode createMeeting(ZoomMeetingRequest meetingRequest) {
+        String accessToken = getZoomAccessToken();
+
+        String url = "https://api.zoom.us/v2/users/me/meetings";
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setBearerAuth(accessToken);
+        headers.setContentType(MediaType.APPLICATION_JSON);
+
+        HttpEntity<ZoomMeetingRequest> entity = new HttpEntity<>(meetingRequest, headers);
+
+        ResponseEntity<String> response;
+        try {
+            RestTemplate restTemplate = new RestTemplate();
+            response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
+        } catch (HttpClientErrorException e) {
+            throw new ZoomMeetingException("Zoom API returned an error: " + e.getStatusCode() + " " + e.getResponseBodyAsString());
+        }
+
+        try {
+            ObjectMapper mapper = new ObjectMapper();
+            JsonNode jsonNode = mapper.readTree(response.getBody());
+            return jsonNode;
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to parse Zoom meeting response", e);
+        }
+    }
+
+    public void deleteMeeting(String meetingId) {
+        String accessToken = getZoomAccessToken();
+
+        String url = "https://api.zoom.us/v2/meetings/" + meetingId;
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setBearerAuth(accessToken);
+
+        HttpEntity<Void> entity = new HttpEntity<>(headers);
+
+        try {
+            RestTemplate restTemplate = new RestTemplate();
+            restTemplate.exchange(url, HttpMethod.DELETE, entity, String.class);
+        } catch (HttpClientErrorException e) {
+            throw new ZoomMeetingException("Failed to delete Zoom meeting: " + e.getStatusCode() + " " + e.getResponseBodyAsString());
+        }
+    }
+
+
+    private String getZoomAccessToken() {
+        String accountId = System.getProperty("ZOOM_ACCOUNT_ID");
+        String clientId = System.getProperty("ZOOM_CLIENT_ID");
+        String clientSecret = System.getProperty("ZOOM_CLIENT_SECRET");
+
+        String auth = clientId + ":" + clientSecret;
+        String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("Authorization", "Basic " + encodedAuth);
+        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+
+        HttpEntity<String> entity = new HttpEntity<>(headers);
+
+        String url = "https://zoom.us/oauth/token?grant_type=account_credentials&account_id=" + accountId;
+
+        RestTemplate restTemplate = new RestTemplate();
+        ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
+
+        // Parse JSON to extract only access_token
+        ObjectMapper mapper = new ObjectMapper();
+        try {
+            JsonNode jsonNode = mapper.readTree(response.getBody());
+            return jsonNode.get("access_token").asText();
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to parse Zoom access token", e);
+        }
+    }
+}
Index: backend/src/main/java/com/shifterwebapp/shifter/external/email/EmailScheduler.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/external/email/EmailScheduler.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
+++ backend/src/main/java/com/shifterwebapp/shifter/external/email/EmailScheduler.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -0,0 +1,33 @@
+package com.shifterwebapp.shifter.external.email;
+
+import com.shifterwebapp.shifter.scheduledemail.ScheduledEmail;
+import com.shifterwebapp.shifter.scheduledemail.ScheduledEmailService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@Service
+@RequiredArgsConstructor
+public class EmailScheduler {
+
+    private final EmailService emailService;
+    private final ScheduledEmailService scheduledEmailService;
+
+    @Scheduled(timeUnit = TimeUnit.MINUTES, fixedRate = 1) // every minute
+    public void sendScheduledEmails() {
+        List<ScheduledEmail> pendingEmails = scheduledEmailService.getPendingEmails();
+        for (ScheduledEmail email : pendingEmails) {
+            emailService.sendFreeConsultationReminder(
+                    email.getRecipientEmail(),
+                    email.getMeetingDateTime().toLocalDate().toString(),
+                    email.getMeetingDateTime().toLocalTime().toString(),
+                    email.getZoomLink()
+            );
+            email.setSent(true);
+            scheduledEmailService.saveScheduledEmail(email);
+        }
+    }
+}
Index: backend/src/main/java/com/shifterwebapp/shifter/external/email/EmailService.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/external/email/EmailService.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
+++ backend/src/main/java/com/shifterwebapp/shifter/external/email/EmailService.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -0,0 +1,187 @@
+package com.shifterwebapp.shifter.external.email;
+
+import com.shifterwebapp.shifter.meeting.UserMeetingInfoRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+
+@Service
+@RequiredArgsConstructor
+public class EmailService {
+
+    private final JavaMailSender mailSender;
+
+    public void sendFreeConsultationConfirmation(String to, String date, String time, String zoomLink) {
+        String subject = "Your Free Consultation Session is Scheduled - " + date + " at " + time;
+        String text = """
+                Hello,
+                
+                Your free consultation session has been successfully scheduled! 🎉
+                
+                📅 Date: {date} \s
+                ⏰ Time: {time} \s
+                📍 Location: Online - Zoom \s
+                🔗 Meeting Link: {zoomLink} \s
+                
+                This session is designed to understand your current challenges, goals, and preferences.
+                During this session, our expert will provide valuable insights based on your situation.
+                After the session, you will receive a personalized program recommendation tailored to your needs.
+                
+                If you have any questions or need to reschedule, please reply to this email.
+                
+                Excited to help you take the next step! \s
+                
+                Best regards, \s
+                The Shifter Team
+                """;
+        text = text
+                .replace("{date}", date)
+                .replace("{time}", time)
+                .replace("{zoomLink}", zoomLink);
+        SimpleMailMessage message = new SimpleMailMessage();
+        message.setTo(to);
+        message.setSubject(subject);
+        message.setText(text);
+
+        int maxRetries = 3;
+        int attempt = 0;
+        while (true) {
+            try {
+                mailSender.send(message);
+                return;
+            } catch (Exception e) {
+                attempt++;
+                if (attempt >= maxRetries) {
+                    throw new RuntimeException("Failed to send confirmation email after " + attempt + " attempts", e);
+                }
+            }
+        }
+    }
+
+    public void sendFreeConsultationReminder(String to, String meetingDate, String meetingTime, String zoomLink) {
+        LocalDate today = LocalDate.now();
+
+        String subject;
+        if (LocalDate.parse(meetingDate).isBefore(today))
+            subject = "Reminder: Tomorrow is your Free Consultation Session at " + meetingTime;
+        else
+            subject = "Reminder: Free Consultation Session in 2 hours";
+
+        String text = """
+                Hello,
+                
+                This is a friendly reminder for your upcoming free consultation session! ⏰
+                
+                📅 Date: {meetingDate} \s
+                ⏰ Time: {meetingTime} \s
+                📍 Location: Online - Zoom \s
+                🔗 Meeting Link: {zoomLink} \s
+                
+                This session is designed to understand your current challenges, goals, and preferences.
+                During this session, our expert will provide valuable insights based on your situation.
+                After the session, you will receive a personalized program recommendation tailored to your needs.
+                
+                If you have any questions or need to reschedule, please reply to this email.
+                
+                Excited to help you take the next step! \s
+                
+                Best regards, \s
+                The Shifter Team
+                """;
+        text = text
+                .replace("{meetingDate}", meetingDate)
+                .replace("{meetingTime}", meetingTime)
+                .replace("{zoomLink}", zoomLink);
+        SimpleMailMessage message = new SimpleMailMessage();
+        message.setTo(to);
+        message.setSubject(subject);
+        message.setText(text);
+        int maxRetries = 3;
+        int attempt = 0;
+        while (true) {
+            try {
+                mailSender.send(message);
+                return;
+            } catch (Exception e) {
+                attempt++;
+                if (attempt >= maxRetries) {
+                    throw new RuntimeException("Failed to send reminder email after " + attempt + " attempts", e);
+                }
+            }
+        }
+    }
+
+    public void sendExpertMeetingInformation(UserMeetingInfoRequest userMeetingInfoRequest, String time, String date, String userTimeZone, String zoomLink) {
+        String subject = "You Have an Upcoming Free Consultation Session - " + date + " at " + time;
+
+        String text = """
+        Hello,
+
+        A new user has booked a free consultation session. Here are their details and the meeting information:
+
+        📅 Date: {date}
+        ⏰ Time: {time}
+        🔗 Zoom Meeting Link: {zoomLink}
+
+        --- User Information ---
+
+        Name: {name}
+        Email: {email}
+        Company Type: {companyType}
+        Work Position: {workPosition}
+        Time Zone: {userTimeZone}
+
+        About the Company:
+        {aboutCompany}
+
+        Current Challenges:
+        {challenges}
+
+        Expectations from the Session:
+        {expectations}
+
+        Additional Information:
+        {otherInfo}
+
+        Please review this information before the session to provide the best personalized guidance.
+
+        Best regards,
+        The Shifter Team
+        """;
+
+        text = text
+                .replace("{date}", date)
+                .replace("{time}", time)
+                .replace("{zoomLink}", zoomLink)
+                .replace("{name}", userMeetingInfoRequest.getName())
+                .replace("{email}", userMeetingInfoRequest.getEmail())
+                .replace("{companyType}", userMeetingInfoRequest.getCompanyType().toString())
+                .replace("{workPosition}", userMeetingInfoRequest.getWorkPosition())
+                .replace("{userTimeZone}", userTimeZone)
+                .replace("{aboutCompany}", userMeetingInfoRequest.getAboutCompany() != null ? userMeetingInfoRequest.getAboutCompany() : "N/A")
+                .replace("{challenges}", userMeetingInfoRequest.getChallenges() != null ? userMeetingInfoRequest.getChallenges() : "N/A")
+                .replace("{expectations}", userMeetingInfoRequest.getExpectations() != null ? userMeetingInfoRequest.getExpectations() : "N/A")
+                .replace("{otherInfo}", userMeetingInfoRequest.getOtherInfo() != null ? userMeetingInfoRequest.getOtherInfo() : "N/A");
+
+        SimpleMailMessage message = new SimpleMailMessage();
+        message.setTo(System.getProperty("EMAIL_USERNAME"));
+        message.setSubject(subject);
+        message.setText(text);
+        int maxRetries = 3;
+        int attempt = 0;
+        while (true) {
+            try {
+                mailSender.send(message);
+                return;
+            } catch (Exception e) {
+                attempt++;
+                if (attempt >= maxRetries) {
+                    throw new RuntimeException("Failed to send expert email with meeting info after " + attempt + " attempts", e);
+                }
+            }
+        }
+    }
+}
Index: backend/src/main/java/com/shifterwebapp/shifter/meeting/MeetingController.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/meeting/MeetingController.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
+++ backend/src/main/java/com/shifterwebapp/shifter/meeting/MeetingController.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -0,0 +1,55 @@
+package com.shifterwebapp.shifter.meeting;
+
+import com.shifterwebapp.shifter.Validate;
+import com.shifterwebapp.shifter.meeting.service.MeetingService;
+import com.shifterwebapp.shifter.user.UserDto;
+import com.shifterwebapp.shifter.user.service.UserService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("${api.base.path}/meetings")
+public class MeetingController {
+
+    private final UserService userService;
+    private final MeetingService meetingService;
+    private final Validate validate;
+
+    @GetMapping("/free-time-slots")
+    public ResponseEntity<?> getExpertFreeTimeSlots(
+            @RequestParam String userTimeZone,
+            Authentication authentication
+    ) {
+        validate.validateUserIsAuthenticated(authentication);
+
+        Map<String, List<String>> freeSlots = meetingService.getExpertFreeTimeSlots(userTimeZone);
+        return ResponseEntity.ok(freeSlots);
+    }
+
+    @PostMapping("/schedule-free-consultation")
+    public ResponseEntity<?> scheduleMeeting (
+            @RequestBody UserMeetingInfoRequest userMeetingInfoRequest,
+            @RequestParam String startTime,
+            @RequestParam String userTimeZone,
+            @RequestParam String date,
+            Authentication authentication
+    ) {
+        Long userId = validate.extractUserId(authentication);
+        UserDto user = userService.getUserById(userId);
+        userMeetingInfoRequest.setEmail(user.getEmail());
+        userMeetingInfoRequest.setName(user.getName());
+        userMeetingInfoRequest.setCompanyType(user.getCompanyType());
+        userMeetingInfoRequest.setWorkPosition(user.getWorkPosition());
+
+        meetingService.scheduleMeeting(date, startTime, userTimeZone, userMeetingInfoRequest);
+
+
+        return ResponseEntity.ok("Meeting successfully arranged!");
+    }
+}
Index: backend/src/main/java/com/shifterwebapp/shifter/meeting/MeetingUtils.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/meeting/MeetingUtils.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
+++ backend/src/main/java/com/shifterwebapp/shifter/meeting/MeetingUtils.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -0,0 +1,44 @@
+package com.shifterwebapp.shifter.meeting;
+import org.springframework.stereotype.Component;
+import java.time.*;
+
+@Component
+public class MeetingUtils {
+
+    public static final ZoneId EXPERT_ZONE = ZoneId.of("Europe/Skopje");
+
+    public ZonedDateTime[] getExpertStartEnd(String date, String time, String userTimeZone, int durationMinutes) {
+        ZoneId userZone = ZoneId.of(userTimeZone);
+
+        LocalDate localDate = LocalDate.parse(date);
+        LocalTime localTime = LocalTime.parse(time);
+        ZonedDateTime startUser = ZonedDateTime.of(localDate, localTime, userZone);
+
+        ZonedDateTime startExpert = startUser.withZoneSameInstant(EXPERT_ZONE);
+        ZonedDateTime endExpert = startExpert.plusMinutes(durationMinutes);
+
+        return new ZonedDateTime[]{startExpert, endExpert};
+    }
+
+    public ZoneId parseZone(String zone) {
+        try {
+            return ZoneId.of(zone);
+        } catch (DateTimeException e) {
+            throw new IllegalArgumentException("Invalid time zone: " + zone, e);
+        }
+    }
+
+    public ZonedDateTime calculateScheduleStart(ZonedDateTime now) {
+        DayOfWeek dow = now.getDayOfWeek();
+        int daysToAdd = switch (dow) {
+            case THURSDAY, FRIDAY -> 4;
+            case SATURDAY -> 3;
+            default -> 2;
+        };
+        return now.withHour(8).withMinute(0).withSecond(0).withNano(0).plusDays(daysToAdd);
+    }
+
+    public ZonedDateTime calculateScheduleEnd(ZonedDateTime start) {
+        return start.withHour(16).withMinute(30).withSecond(0).withNano(0).plusDays(9);
+    }
+}
Index: backend/src/main/java/com/shifterwebapp/shifter/meeting/UserMeetingInfoRequest.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/meeting/UserMeetingInfoRequest.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
+++ backend/src/main/java/com/shifterwebapp/shifter/meeting/UserMeetingInfoRequest.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -0,0 +1,28 @@
+package com.shifterwebapp.shifter.meeting;
+
+import com.shifterwebapp.shifter.enums.CompanyType;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class UserMeetingInfoRequest {
+
+    private String name;
+
+    private String email;
+
+    private CompanyType companyType;
+
+    private String workPosition;
+
+    private String aboutCompany;
+
+    private String challenges;
+
+    private String expectations;
+
+    private String otherInfo;
+}
Index: backend/src/main/java/com/shifterwebapp/shifter/meeting/ZoomMeetingRequest.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/meeting/ZoomMeetingRequest.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
+++ backend/src/main/java/com/shifterwebapp/shifter/meeting/ZoomMeetingRequest.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -0,0 +1,33 @@
+package com.shifterwebapp.shifter.meeting;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@Builder
+public class ZoomMeetingRequest {
+
+    @JsonProperty("topic")
+    private String topic;
+
+    @JsonProperty("start_time")
+    private String startTime; // ISO-8601 format: 2025-08-14T15:00:00Z
+
+    @JsonProperty("duration")
+    private int duration; // in minutes
+
+    @JsonProperty("type")
+    private int type; // 2 = scheduled meeting
+
+    @JsonProperty("timezone")
+    private String timezone;
+
+    // Optional
+    @JsonProperty("agenda")
+    private String agenda;
+
+}
+
Index: backend/src/main/java/com/shifterwebapp/shifter/meeting/service/ImplMeetingService.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/meeting/service/ImplMeetingService.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
+++ backend/src/main/java/com/shifterwebapp/shifter/meeting/service/ImplMeetingService.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -0,0 +1,14 @@
+package com.shifterwebapp.shifter.meeting.service;
+
+import com.shifterwebapp.shifter.meeting.UserMeetingInfoRequest;
+
+import java.util.List;
+import java.util.Map;
+
+public interface ImplMeetingService {
+
+    Map<String, List<String>> getExpertFreeTimeSlots(String userTimeZone);
+
+    void scheduleMeeting(String userDate, String userTime, String userTimeZone, UserMeetingInfoRequest userMeetingInfoRequest);
+
+}
Index: backend/src/main/java/com/shifterwebapp/shifter/meeting/service/MeetingService.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/meeting/service/MeetingService.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
+++ backend/src/main/java/com/shifterwebapp/shifter/meeting/service/MeetingService.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -0,0 +1,180 @@
+package com.shifterwebapp.shifter.meeting.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.api.services.calendar.model.FreeBusyResponse;
+import com.shifterwebapp.shifter.exception.AccessDeniedException;
+import com.shifterwebapp.shifter.exception.TimeSlotUnavailableException;
+import com.shifterwebapp.shifter.external.email.EmailService;
+import com.shifterwebapp.shifter.external.ZoomService;
+import com.shifterwebapp.shifter.external.GoogleCalendarService;
+import com.shifterwebapp.shifter.meeting.MeetingUtils;
+import com.shifterwebapp.shifter.meeting.UserMeetingInfoRequest;
+import com.shifterwebapp.shifter.meeting.ZoomMeetingRequest;
+import com.shifterwebapp.shifter.scheduledemail.ScheduledEmail;
+import com.shifterwebapp.shifter.scheduledemail.ScheduledEmailService;
+import com.shifterwebapp.shifter.user.service.UserService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.Map;
+
+@Service
+@RequiredArgsConstructor
+public class MeetingService implements ImplMeetingService {
+
+    private final GoogleCalendarService googleCalendarService;
+    private final ScheduledEmailService scheduledEmailService;
+    private final UserService userService;
+    private final ZoomService zoomService;
+    private final EmailService emailService;
+    private final MeetingUtils meetingUtils;
+
+
+    @Override
+    public Map<String, List<String>> getExpertFreeTimeSlots(String userTimeZone) {
+        ZoneId expertZone = ZoneId.of("Europe/Skopje");
+        ZoneId userZone = meetingUtils.parseZone(userTimeZone);
+
+        ZonedDateTime nowExpert = ZonedDateTime.now(expertZone);
+        ZonedDateTime scheduleStart = meetingUtils.calculateScheduleStart(nowExpert);
+        ZonedDateTime scheduleEnd = meetingUtils.calculateScheduleEnd(scheduleStart);
+
+        FreeBusyResponse freeBusyResponse = googleCalendarService
+                .queryFreeBusy(scheduleStart.toInstant(), scheduleEnd.toInstant());
+
+        String calendarId = googleCalendarService.getExpertCalendarId();
+        if (freeBusyResponse == null
+                || freeBusyResponse.getCalendars() == null
+                || freeBusyResponse.getCalendars().get(calendarId) == null
+                || freeBusyResponse.getCalendars().get(calendarId).getBusy() == null) {
+            throw new IllegalStateException("Invalid FreeBusyResponse from Google API");
+        }
+
+        List<GoogleCalendarService.TimeInterval> busyIntervals = freeBusyResponse
+                .getCalendars()
+                .get(calendarId)
+                .getBusy()
+                .stream()
+                .map(i -> new GoogleCalendarService.TimeInterval(
+                        Instant.ofEpochMilli(i.getStart().getValue()),
+                        Instant.ofEpochMilli(i.getEnd().getValue())))
+                .toList();
+
+        return googleCalendarService.computeFreeSlotsByDate(
+                busyIntervals, scheduleStart, scheduleEnd, 30, userZone
+        );
+    }
+
+    @Override
+    public void scheduleMeeting(String userDate, String userTime, String userTimeZone, UserMeetingInfoRequest userMeetingInfoRequest) {
+        String userEmail = userMeetingInfoRequest.getEmail();
+
+        if (userService.getUserHasUsedFreeConsultation(userEmail)) {
+            throw new AccessDeniedException("User has already used free consultation");
+        }
+
+        ZonedDateTime[] expertTimes = meetingUtils.getExpertStartEnd(userDate, userTime, userTimeZone, 30);
+        ZonedDateTime startExpert = expertTimes[0];
+        ZonedDateTime endExpert = expertTimes[1];
+
+        boolean calendarCreated = false;
+        String calendarEventId = null;
+        String meetingLink = null;
+        String meetingId = null;
+
+        try {
+            FreeBusyResponse freeBusy = googleCalendarService.queryFreeBusy(
+                    startExpert.toInstant(),
+                    endExpert.toInstant()
+            );
+            if (!googleCalendarService.isSlotFree(freeBusy, startExpert, endExpert)) {
+                throw new TimeSlotUnavailableException("Slot is no longer available");
+            }
+
+            calendarEventId = googleCalendarService.createEvent(
+                    "Free Consultation",
+                    startExpert.toInstant(),
+                    endExpert.toInstant()
+            );
+            calendarCreated = true;
+
+            String zoomStartTime = startExpert
+                    .withZoneSameInstant(ZoneId.of("UTC"))
+                    .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+            JsonNode jsonNode = zoomService.createMeeting(ZoomMeetingRequest.builder()
+                    .topic("Free Consultation Session")
+                    .startTime(zoomStartTime)
+                    .duration(30)
+                    .timezone(MeetingUtils.EXPERT_ZONE.toString())
+                    .agenda("""
+                        This session is designed to understand your current challenges, goals, and preferences.
+                        During this session, our expert will provide valuable insights based on your situation.
+                        After the session, you will receive a personalized program recommendation tailored to your needs.
+                        """)
+                            .type(2)    // Scheduled meeting
+                    .build());
+            meetingLink = jsonNode.get("join_url").asText();
+            meetingId = jsonNode.get("id").asText();
+
+            emailService.sendFreeConsultationConfirmation(userEmail, userDate, userTime, meetingLink);
+            emailService.sendExpertMeetingInformation(userMeetingInfoRequest, startExpert.toLocalTime().toString(), startExpert.toLocalDate().toString(), userTimeZone, meetingLink);
+
+            LocalDateTime scheduledDayBefore = startExpert
+                    .minusDays(1) // 1 day before
+                    .withHour(8)  // 8 AM
+                    .withMinute(0)
+                    .withSecond(0)
+                    .toLocalDateTime();
+            scheduledEmailService.saveScheduledEmail(
+                    ScheduledEmail.builder()
+                            .recipientEmail(userEmail)
+                            .meetingDateTime(LocalDateTime.parse(userDate + "T" + userTime))
+                            .scheduledDateTime(scheduledDayBefore)
+                            .zoomLink(meetingLink)
+                            .sent(false)
+                            .build()
+            );
+
+            LocalDateTime scheduledOnDay = startExpert.minusHours(2).toLocalDateTime(); // 2 hours before
+            scheduledEmailService.saveScheduledEmail(
+                    ScheduledEmail.builder()
+                            .recipientEmail(userEmail)
+                            .meetingDateTime(LocalDateTime.parse(userDate + "T" + userTime))
+                            .scheduledDateTime(scheduledOnDay)
+                            .zoomLink(meetingLink)
+                            .sent(false)
+                            .build()
+            );
+
+            userService.markUserAsUsedFreeConsultation(userEmail);
+
+        } catch (Exception e) {
+            // Rollback calendar if Zoom or email fails
+            if (calendarCreated && calendarEventId != null) {
+                try {
+                    googleCalendarService.deleteEvent(calendarEventId);
+                } catch (Exception ex) {
+                    // log failure to delete calendar event
+                    System.err.println("Failed to delete calendar event: " + ex.getMessage());
+                }
+            }
+
+            if (meetingId != null) {
+                try {
+                    zoomService.deleteMeeting(meetingId);
+                } catch (Exception ex) {
+                    // log failure to delete Zoom meeting
+                    System.err.println("Failed to delete Zoom meeting: " + ex.getMessage());
+                }
+            }
+
+            // Optionally log or rethrow the exception
+            throw new RuntimeException("Failed to schedule meeting: " + e.getMessage(), e);
+        }
+    }
+
+
+}
Index: backend/src/main/java/com/shifterwebapp/shifter/scheduledemail/ScheduledEmail.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/scheduledemail/ScheduledEmail.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
+++ backend/src/main/java/com/shifterwebapp/shifter/scheduledemail/ScheduledEmail.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -0,0 +1,34 @@
+package com.shifterwebapp.shifter.scheduledemail;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@Entity
+@ToString
+public class ScheduledEmail {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    private String recipientEmail;
+
+    private LocalDateTime meetingDateTime;
+
+    private LocalDateTime scheduledDateTime;
+
+    private Boolean sent;
+
+    private String zoomLink;
+
+}
Index: backend/src/main/java/com/shifterwebapp/shifter/scheduledemail/ScheduledEmailRepository.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/scheduledemail/ScheduledEmailRepository.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
+++ backend/src/main/java/com/shifterwebapp/shifter/scheduledemail/ScheduledEmailRepository.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -0,0 +1,12 @@
+package com.shifterwebapp.shifter.scheduledemail;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+import java.util.List;
+
+public interface ScheduledEmailRepository extends JpaRepository<ScheduledEmail, Long> {
+
+    @Query("select se from ScheduledEmail se where se.sent = false and se.scheduledDateTime <= CURRENT_TIMESTAMP")
+    List<ScheduledEmail> findPendingEmails();
+}
Index: backend/src/main/java/com/shifterwebapp/shifter/scheduledemail/ScheduledEmailService.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/scheduledemail/ScheduledEmailService.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
+++ backend/src/main/java/com/shifterwebapp/shifter/scheduledemail/ScheduledEmailService.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -0,0 +1,22 @@
+package com.shifterwebapp.shifter.scheduledemail;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class ScheduledEmailService {
+
+    private final ScheduledEmailRepository scheduledEmailRepository;
+
+    public List<ScheduledEmail> getPendingEmails() {
+        return scheduledEmailRepository.findPendingEmails();
+    }
+
+    public void saveScheduledEmail(ScheduledEmail email) {
+        scheduledEmailRepository.save(email);
+    }
+
+}
Index: backend/src/main/java/com/shifterwebapp/shifter/user/UserRepository.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/user/UserRepository.java	(revision bee3afb8cd1e741c5eff5e30a0d90f874ad19d83)
+++ backend/src/main/java/com/shifterwebapp/shifter/user/UserRepository.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -4,4 +4,5 @@
 import org.springframework.data.jpa.repository.Query;
 import org.springframework.data.repository.query.Param;
+import org.springframework.security.core.parameters.P;
 
 import java.util.Optional;
@@ -10,4 +11,7 @@
 
     boolean existsUserByEmail(String email);
+
+    @Query("select u.email from User u where u.id = :userId")
+    String getUserEmailById(@Param("userId") Long userId);
 
     Optional<User> findByEmail(String email);
Index: backend/src/main/java/com/shifterwebapp/shifter/user/service/ImplUserService.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/user/service/ImplUserService.java	(revision bee3afb8cd1e741c5eff5e30a0d90f874ad19d83)
+++ backend/src/main/java/com/shifterwebapp/shifter/user/service/ImplUserService.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -14,4 +14,7 @@
     User getUserEntityById(Long userId);
     User getUserEntityByEmail(String email);
+    String getUserEmailById(Long userId);
+    Boolean getUserHasUsedFreeConsultation(String userEmail);
+
     Boolean existsUserByEmail(String email);
 
@@ -36,3 +39,4 @@
     UserDto removePayment(Long id, Payment removePayment);
 
+    void markUserAsUsedFreeConsultation(String userEmail);
 }
Index: backend/src/main/java/com/shifterwebapp/shifter/user/service/UserService.java
===================================================================
--- backend/src/main/java/com/shifterwebapp/shifter/user/service/UserService.java	(revision bee3afb8cd1e741c5eff5e30a0d90f874ad19d83)
+++ backend/src/main/java/com/shifterwebapp/shifter/user/service/UserService.java	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -44,4 +44,17 @@
         validate.validateUserExists(email);
         return userRepository.findByEmail(email).orElseThrow();
+    }
+
+    @Override
+    public String getUserEmailById(Long userId) {
+        validate.validateUserExists(userId);
+        return userRepository.getUserEmailById(userId);
+    }
+
+    @Override
+    public Boolean getUserHasUsedFreeConsultation(String userEmail) {
+        validate.validateUserExists(userEmail);
+        User user = userRepository.findByEmail(userEmail).orElseThrow();
+        return user.getHasUsedFreeConsultation();
     }
 
@@ -243,3 +256,11 @@
         return userMapper.toDto(user);
     }
+
+    @Override
+    public void markUserAsUsedFreeConsultation(String email) {
+        validate.validateUserExists(email);
+        User user = userRepository.findByEmail(email).orElseThrow();
+        user.setHasUsedFreeConsultation(true);
+        userRepository.save(user);
+    }
 }
Index: backend/src/main/resources/application.properties
===================================================================
--- backend/src/main/resources/application.properties	(revision bee3afb8cd1e741c5eff5e30a0d90f874ad19d83)
+++ backend/src/main/resources/application.properties	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -25,5 +25,5 @@
 api.admin.path=/api/admin
 
-spring.profiles.active=dev
+#spring.profiles.active=dev
 
 # Max file size
@@ -35,8 +35,26 @@
 
 
+# PostgreSQL connection
+spring.datasource.driver-class-name=org.postgresql.Driver
+spring.datasource.url=${SPRING_DATASOURCE_URL}
+spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
+spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
+
 # Amazon S3 configuration
-# aws.region=REMOVED
-# aws.s3.bucket-name=REMOVED
-# aws.access-key=REMOVED
-# aws.secret-key=REMOVED
+aws.region=${AWS_S3_REGION}
+aws.s3.bucket-name=${AWS_S3_BUCKET_NAME}
+aws.access-key=${AWS_S3_ACCESS_KEY}
+aws.secret-key=${AWS_S3_SECRET_KEY}
 
+
+# JWT Configuration Key
+jwt.secret=${JWT_CONFIG_SECRET}
+
+# Email configuration
+spring.mail.host=${EMAIL_HOST}
+spring.mail.port=${EMAIL_PORT}
+spring.mail.username=${EMAIL_USERNAME}
+spring.mail.password=${EMAIL_PASSWORD}
+spring.mail.properties.mail.smtp.auth=true
+spring.mail.properties.mail.smtp.starttls.enable=true
+
Index: frontend/src/api/meetingApi.ts
===================================================================
--- frontend/src/api/meetingApi.ts	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
+++ frontend/src/api/meetingApi.ts	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -0,0 +1,33 @@
+import axios from "axios";
+import type {UserMeetingInfoRequest} from "../models/UserMeetingInfoRequest.tsx";
+
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-expect-error
+const backendUrl = import.meta.env.VITE_BACKEND_URL;
+
+export const fetchExpertFreeTimeSlotsApi = async (accessToken: string): Promise<Record<string, string[]>> => {
+    const userTimeZone = encodeURIComponent(Intl.DateTimeFormat().resolvedOptions().timeZone);
+    const res = await axios.get(
+        `${backendUrl}/api/meetings/free-time-slots?userTimeZone=${userTimeZone}`,
+        {
+            headers: {
+                Authorization: `Bearer ${accessToken}`
+            }
+        }
+    );
+
+    return res.data;
+}
+
+export const scheduleMeetingApi = async (accessToken: string, startTime: string, date: string, userMeetingInfoRequest: UserMeetingInfoRequest): Promise<void> => {
+    const userTimeZone = encodeURIComponent(Intl.DateTimeFormat().resolvedOptions().timeZone);
+    await axios.post(
+        `${backendUrl}/api/meetings/schedule-free-consultation?startTime=${startTime}&date=${date}&userTimeZone=${userTimeZone}`,
+        userMeetingInfoRequest,
+        {
+            headers: {
+                Authorization: `Bearer ${accessToken}`
+            }
+        }
+    );
+}
Index: frontend/src/context/AuthContext.tsx
===================================================================
--- frontend/src/context/AuthContext.tsx	(revision bee3afb8cd1e741c5eff5e30a0d90f874ad19d83)
+++ frontend/src/context/AuthContext.tsx	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -84,4 +84,6 @@
         return refreshAccessTokenApi()
             .then(data => {
+                console.log(data.accessToken)
+                console.log(encodeURIComponent(Intl.DateTimeFormat().resolvedOptions().timeZone))
                 setAccessToken(data.accessToken);
                 setUser(data.user);
Index: frontend/src/global.css
===================================================================
--- frontend/src/global.css	(revision bee3afb8cd1e741c5eff5e30a0d90f874ad19d83)
+++ frontend/src/global.css	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -188,5 +188,5 @@
 
 /* HTML: <div class="loader"></div> */
-.loader {
+.loader, .loader-white {
     /*margin-left: 20px;*/
     /*width: 40px;*/
@@ -195,9 +195,16 @@
     border: 4px solid #0000;
     border-radius: 50%;
+    animation: l15 1s infinite linear;
+}
+.loader-white {
+    border-right-color: var(--color-white);
+}
+.loader {
     border-right-color: var(--color-shifter);
-    animation: l15 1s infinite linear;
 }
 .loader::before,
-.loader::after {
+.loader::after,
+.loader-white::before,
+.loader-white::after {
     content: "";
     grid-area: 1/1;
@@ -207,5 +214,5 @@
     animation: l15 2s infinite;
 }
-.loader::after {
+.loader::after, .loader-white::after {
     margin: 8px;
     animation-duration: 3s;
Index: frontend/src/models/UserMeetingInfoRequest.tsx
===================================================================
--- frontend/src/models/UserMeetingInfoRequest.tsx	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
+++ frontend/src/models/UserMeetingInfoRequest.tsx	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -0,0 +1,6 @@
+export interface UserMeetingInfoRequest {
+    aboutCompany: string;
+    challenges: string;
+    expectations: string;
+    otherInfo: string;
+}
Index: frontend/src/models/javaObjects/User.tsx
===================================================================
--- frontend/src/models/javaObjects/User.tsx	(revision bee3afb8cd1e741c5eff5e30a0d90f874ad19d83)
+++ frontend/src/models/javaObjects/User.tsx	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -3,4 +3,5 @@
     email: string;
     name: string;
+    hasUsedFreeConsultation: boolean;
     companyType: string;
     workPosition: string;
Index: frontend/src/pages/FreeConsultation.tsx
===================================================================
--- frontend/src/pages/FreeConsultation.tsx	(revision bee3afb8cd1e741c5eff5e30a0d90f874ad19d83)
+++ frontend/src/pages/FreeConsultation.tsx	(revision fcb98a188079a5fc4d0f566d2251f6416711f591)
@@ -1,6 +1,61 @@
 import {useAuthContext} from "../context/AuthContext.tsx";
+import React, {useEffect, useState} from "react";
+import {fetchExpertFreeTimeSlotsApi, scheduleMeetingApi} from "../api/meetingApi.ts";
+import type {UserMeetingInfoRequest} from "../models/UserMeetingInfoRequest.tsx";
 
 function FreeConsultation() {
-    const {user} = useAuthContext();
+    const {user, setUser, accessToken, authChecked} = useAuthContext();
+    const [loadingDateTime, setLoadingDateTime] = useState(true);
+    const [loadingSubmitForm, setLoadingSubmitForm] = useState(false);
+    const [error, setError] = useState<string>("");
+    const [selectedDate, setSelectedDate] = useState<string>("");
+    const [selectedTime, setSelectedTime] = useState<string>("");
+    const [freeSlots, setFreeSlots] = useState<Record<string, string[]>>({"": [""]});
+    const [userMeetingInfo, setUserMeetingInfo] = useState<UserMeetingInfoRequest>({
+        aboutCompany: "",
+        challenges: "",
+        expectations: "",
+        otherInfo: ""
+    });
+    const [meetingScheduled, setMeetingScheduled] = useState<boolean>(false);
+
+    const handleScheduleMeeting = (e: React.FormEvent<HTMLFormElement>) => {
+        e.preventDefault();
+        if (!selectedDate || !selectedTime) {
+            setError("Please select both date and time.");
+            return;
+        }
+
+        setLoadingSubmitForm(true);
+        scheduleMeetingApi(accessToken || "", selectedTime, selectedDate, userMeetingInfo)
+            .then(() => {
+                setMeetingScheduled(true);
+                setError("");
+
+                if (user)
+                    setUser({...user, hasUsedFreeConsultation: true})
+            })
+            .catch(error => {
+                console.error("Error scheduling meeting:", error);
+                setError("Failed to schedule the meeting. Please try again later or contact support.");
+            })
+            .finally(() => setLoadingSubmitForm(false));
+    }
+
+    useEffect(() => {
+        if (!authChecked || !accessToken)
+            return;
+
+        setLoadingDateTime(true);
+        fetchExpertFreeTimeSlotsApi(accessToken)
+            .then(data => {
+                console.log("Available time slots:", data);
+                setFreeSlots(data)
+            })
+            .catch(error => {
+                console.error("Error fetching time slots:", error);
+            })
+            .finally(() => setLoadingDateTime(false));
+    }, []);
 
     return (
@@ -13,6 +68,6 @@
                     <h1 className="text-5xl font-bold">Book Your Free Expert Session</h1>
                     <p className="text-xl font-light ">
-                        Talk to an expert about your business goals and challenges. Get a personalized mentorship
-                        plan or course recommendation that fits your unique situation.
+                        Talk to an expert about your business goals and challenges.
+                        Get a personalized program recommendation tailored to your needs.
                     </p>
                 </div>
@@ -26,15 +81,19 @@
                     <div>
                         <ol className="flex flex-col">
+                            {/*<Step*/}
+                            {/*    title={"Submit Your Request"}*/}
+                            {/*    description={"Share your business challenges and objectives by completing the form. This helps us understand your needs and prepare a personalized session."}*/}
+                            {/*/>*/}
                             <Step
-                                title={"Expert Prepares and Sends Invite"}
-                                description={"Our expert reviews your request and sends a meeting invitation to your email."}
+                                title={"Your Expert is Assigned"}
+                                description={"We schedule an expert for your session and send you a confirmation email with all the meeting details."}
                             />
                             <Step
-                                title={"You Confirm the Invite"}
-                                description={"You’ll receive an email with the meeting details — simply confirm the invite to secure your spot."}
+                                title={"Expert Prepares for Your Session"}
+                                description={"Your expert reviews the information you provided to create valuable insights and a strategy tailored to your situation."}
                             />
                             <Step
-                                title={"Join Your Online Session"}
-                                description={"Attend the session and walk away with a tailored plan built around your goals — designed to help you overcome challenges, grow, and move forward with clarity."}
+                                title={"Attend Your Personalized Session"}
+                                description={"Join the session and receive actionable guidance along with a program or mentorship recommendation designed specifically for your goals."}
                                 isLast={true}
                             />
@@ -46,5 +105,9 @@
                 <div className="flex flex-col gap-4 bg-white rounded-xl w-2/3 px-horizontal-sm py-vertical-md ">
                     {/*Automatically populated*/}
-                    <div className="grid grid-cols-2 gap-y-6">
+                    <div className="grid grid-cols-2 gap-y-4">
+                        <p className="text-black/40 col-span-2">
+                            These values are automatically populated from your profile.
+                            If any of them are incorrect, please update them in the Profile page.
+                        </p>
                         <p className="font-light text-black/60 text-lg ">Name: <span
                             className="text-black font-medium">{user?.name}</span></p>
@@ -56,8 +119,4 @@
                         <p className="font-light text-black/60 text-lg ">Work Position: <span
                             className="text-black font-medium">{user?.workPosition}</span></p>
-                        <p className="text-black/40 col-span-2">
-                            <sup>*</sup> These values are automatically populated from your profile.
-                            If any of them are incorrect, please update them in the Profile page.
-                        </p>
                     </div>
 
@@ -66,26 +125,102 @@
                     {/*Form*/}
                     <form
+                        onSubmit={handleScheduleMeeting}
                         className="flex flex-col gap-6 w-full">
-                        <LabelInput
+                        <p className="text-black/40 col-span-2">
+                            <sup>*</sup> These fields are optional. Filling them out helps us better understand your
+                            challenges and objectives, so we can prepare a more personalized session.
+                        </p>
+                        <TextInput
                             label="About your business"
                             name="intro"
                             placeholder="What you do, industry, customers"
-                        />
-                        <LabelInput
+                            onChange={(e) => setUserMeetingInfo({...userMeetingInfo, aboutCompany: e.target.value})}
+                        />
+                        <TextInput
                             label="Your current challenges"
                             name="challenges"
                             placeholder="E.g. sales, growth, team issues"
-                        />
-                        <LabelInput
+                            onChange={(e) => setUserMeetingInfo({...userMeetingInfo, challenges: e.target.value})}
+                        />
+                        <TextInput
                             label="What you want from the session"
                             name="expectations"
                             placeholder="Advice, strategy, solutions"
-                        />
-                        <LabelInput
-                            label="Anything else? (optional)"
+                            onChange={(e) => setUserMeetingInfo({...userMeetingInfo, expectations: e.target.value})}
+                        />
+                        <TextInput
+                            label="Anything else"
                             name="additional"
                             placeholder="Extra context or details"
-                        />
-
+                            onChange={(e) => setUserMeetingInfo({...userMeetingInfo, otherInfo: e.target.value})}
+                        />
+
+                        <hr className="border-t-2 border-black/20"/>
+
+                        <div className="flex justify-between items-center">
+                            {
+                                loadingDateTime ? (
+                                    <>
+                                        <div className="bg-gray-300 animate-pulse py-2 px-8 rounded-sm h-10 w-50"></div>
+                                        <div className="bg-gray-300 animate-pulse py-2 px-8 rounded-sm h-10 w-50"></div>
+                                    </>
+                                ) : (
+                                    <>
+                                        <SelectInput
+                                            value={selectedDate}
+                                            onChange={(e) => setSelectedDate(e.target.value)}
+                                            firstOption={"Select a date"}
+                                            options={Object.keys(freeSlots)}
+                                        />
+                                        <SelectInput
+                                            value={selectedTime}
+                                            onChange={(e) => setSelectedTime(e.target.value)}
+                                            firstOption={"Select a time"}
+                                            options={freeSlots[selectedDate]}
+                                        />
+                                    </>
+                                )
+                            }
+                        </div>
+
+                        {
+                            error && (
+                                <p className="text-red-500 text-md font-medium text-center">
+                                    {error}
+                                </p>
+                            )
+                        }
+                        {
+                            meetingScheduled ? (
+                                <div className="my-12 text-center">
+                                    <h2 className="text-2xl font-bold text-dark-blue mb-4">
+                                        Your free consultation is scheduled! 🎉
+                                    </h2>
+                                    <p className="text-lg font-medium text-black/80 max-w-xl mx-auto">
+                                        Completing the form was the <span className="font-semibold text-dark-blue">first step toward progress and growth</span>.
+                                        Check your <span className="font-semibold text-dark-blue">email for the Zoom link</span> to continue your journey.
+                                    </p>
+                                </div>
+
+                            ) : (
+                                <button
+                                    type="submit"
+                                    disabled={loadingSubmitForm}
+                                    className="disabled:cursor-not-allowed disabled:opacity-60 disabled:shadow-none
+                                    hover:shadow-md hover:shadow-dark-blue/60 transition-all duration-300 ease-in-out
+                                    shadow-sm shadow-dark-blue/30 border-2 border-white/20
+                                    w-full py-2 bg-dark-blue text-white text-lg font-semibold rounded-sm cursor-pointer"
+                                >
+                                    {
+                                        loadingSubmitForm ? (
+                                            <div className="flex justify-center gap-6 items-center">
+                                                Booking...
+                                                <div className="loader-white w-8 h-8"/>
+                                            </div>
+                                        ) : "Book Session"
+                                    }
+                                </button>
+                            )
+                        }
                     </form>
                 </div>
@@ -95,13 +230,15 @@
 }
 
-function LabelInput({label, name, placeholder}: {
+function TextInput({label, name, placeholder, onChange}: {
     label: string;
     name: string;
     placeholder: string;
+    onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
 }) {
     return (
         <label className="flex flex-col gap-2">
-            <span className="text-black font-semibold text-xl">{label}</span>
+            <span className="text-black font-semibold text-xl"><sup>*</sup> {label}</span>
             <textarea
+                onChange={onChange}
                 rows={2}
                 name={name}
@@ -112,4 +249,25 @@
         </label>
     )
+}
+
+function SelectInput({value, onChange, firstOption, options}: {
+    value: string;
+    onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
+    firstOption: string;
+    options: string[];
+}) {
+    return (
+        <select
+            className="bg-dark-blue/5 border-1 border-black/10 py-2 px-8 rounded-sm
+                font-medium resize-none overflow-hidden min-h-fit cursor-pointer"
+            value={value} onChange={onChange}>
+            <option value="">{firstOption}</option>
+            {options?.map((option) => (
+                <option key={option} value={option}>
+                    {option}
+                </option>
+            ))}
+        </select>
+    );
 }
 
