diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/SchemaFactory.java b/application/src/main/java/org/opentripplanner/apis/gtfs/SchemaFactory.java
index 86561416006..f1476e7c441 100644
--- a/application/src/main/java/org/opentripplanner/apis/gtfs/SchemaFactory.java
+++ b/application/src/main/java/org/opentripplanner/apis/gtfs/SchemaFactory.java
@@ -55,6 +55,7 @@
 import org.opentripplanner.apis.gtfs.datafetchers.StopCallImpl;
 import org.opentripplanner.apis.gtfs.datafetchers.StopGeometriesImpl;
 import org.opentripplanner.apis.gtfs.datafetchers.StopImpl;
+import org.opentripplanner.apis.gtfs.datafetchers.StopInPatternImpl;
 import org.opentripplanner.apis.gtfs.datafetchers.StopOnRouteImpl;
 import org.opentripplanner.apis.gtfs.datafetchers.StopOnTripImpl;
 import org.opentripplanner.apis.gtfs.datafetchers.StopRelationshipImpl;
@@ -165,6 +166,7 @@ public static GraphQLSchema createSchema() {
         .type(typeWiring.build(stepImpl.class))
         .type(typeWiring.build(StopImpl.class))
         .type(typeWiring.build(stopAtDistanceImpl.class))
+        .type(typeWiring.build(StopInPatternImpl.class))
         .type(typeWiring.build(StoptimeImpl.class))
         .type(typeWiring.build(StoptimesInPatternImpl.class))
         .type(typeWiring.build(TicketTypeImpl.class))
diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PatternImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PatternImpl.java
index db0dae0fb26..f3aa8f281c7 100644
--- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PatternImpl.java
+++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PatternImpl.java
@@ -16,6 +16,7 @@
 import org.opentripplanner.apis.gtfs.GraphQLRequestContext;
 import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers;
 import org.opentripplanner.apis.gtfs.generated.GraphQLTypes;
+import org.opentripplanner.apis.gtfs.model.StopInPatternModel;
 import org.opentripplanner.apis.support.SemanticHash;
 import org.opentripplanner.framework.graphql.GraphQLUtils;
 import org.opentripplanner.routing.alertpatch.EntitySelector;
@@ -194,6 +195,19 @@ public DataFetcher<Iterable<Object>> stops() {
     return this::getStops;
   }
 
+  @Override
+  public DataFetcher<Iterable<Object>> stopsInPattern() {
+    return environment -> {
+      var pattern = getSource(environment);
+      var numberOfStops = pattern.numberOfStops();
+      var result = new StopInPatternModel[numberOfStops];
+      for (var i = 0; i < numberOfStops; i++) {
+        result[i] = StopInPatternModel.fromPatternAndIndex(pattern, i);
+      }
+      return List.of(result);
+    };
+  }
+
   @Override
   public DataFetcher<Iterable<Trip>> trips() {
     return this::getTrips;
diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopInPatternImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopInPatternImpl.java
new file mode 100644
index 00000000000..3e4b6bb4883
--- /dev/null
+++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopInPatternImpl.java
@@ -0,0 +1,35 @@
+package org.opentripplanner.apis.gtfs.datafetchers;
+
+import graphql.schema.DataFetcher;
+import graphql.schema.DataFetchingEnvironment;
+import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers;
+import org.opentripplanner.apis.gtfs.generated.GraphQLTypes;
+import org.opentripplanner.apis.gtfs.mapping.PickDropMapper;
+import org.opentripplanner.apis.gtfs.model.StopInPatternModel;
+
+public class StopInPatternImpl implements GraphQLDataFetchers.GraphQLStopInPattern {
+
+  @Override
+  public DataFetcher<GraphQLTypes.GraphQLPickupDropoffType> dropOffType() {
+    return environment -> PickDropMapper.map(getSource(environment).dropoffType());
+  }
+
+  @Override
+  public DataFetcher<Integer> indexInPattern() {
+    return environment -> getSource(environment).indexInPattern();
+  }
+
+  @Override
+  public DataFetcher<GraphQLTypes.GraphQLPickupDropoffType> pickupType() {
+    return environment -> PickDropMapper.map(getSource(environment).pickupType());
+  }
+
+  @Override
+  public DataFetcher<Object> stop() {
+    return environment -> getSource(environment).stop();
+  }
+
+  private StopInPatternModel getSource(DataFetchingEnvironment environment) {
+    return environment.getSource();
+  }
+}
diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java
index 5f7c72c1377..3e3d4671daf 100644
--- a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java
+++ b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java
@@ -673,6 +673,8 @@ public interface GraphQLPattern {
 
     public DataFetcher<Iterable<Object>> stops();
 
+    public DataFetcher<Iterable<Object>> stopsInPattern();
+
     public DataFetcher<Iterable<Trip>> trips();
 
     public DataFetcher<Iterable<Trip>> tripsForDate();
@@ -1138,6 +1140,16 @@ public interface GraphQLStopGeometries {
     public DataFetcher<Iterable<Geometry>> googleEncoded();
   }
 
+  public interface GraphQLStopInPattern {
+    public DataFetcher<GraphQLPickupDropoffType> dropOffType();
+
+    public DataFetcher<Integer> indexInPattern();
+
+    public DataFetcher<GraphQLPickupDropoffType> pickupType();
+
+    public DataFetcher<Object> stop();
+  }
+
   /** Stop that should (but not guaranteed) to exist on a route. */
   public interface GraphQLStopOnRoute {
     public DataFetcher<Route> route();
diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java
index 7973b909b3a..da8e135d4a2 100644
--- a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java
+++ b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java
@@ -1,6 +1,7 @@
 // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
 package org.opentripplanner.apis.gtfs.generated;
 
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/model/StopInPatternModel.java b/application/src/main/java/org/opentripplanner/apis/gtfs/model/StopInPatternModel.java
new file mode 100644
index 00000000000..afdd1c0c98e
--- /dev/null
+++ b/application/src/main/java/org/opentripplanner/apis/gtfs/model/StopInPatternModel.java
@@ -0,0 +1,21 @@
+package org.opentripplanner.apis.gtfs.model;
+
+import org.opentripplanner.model.PickDrop;
+import org.opentripplanner.transit.model.network.TripPattern;
+import org.opentripplanner.transit.model.site.StopLocation;
+
+public record StopInPatternModel(
+  StopLocation stop,
+  int indexInPattern,
+  PickDrop pickupType,
+  PickDrop dropoffType
+) {
+  public static StopInPatternModel fromPatternAndIndex(TripPattern pattern, int indexInPattern) {
+    return new StopInPatternModel(
+      pattern.getStop(indexInPattern),
+      indexInPattern,
+      pattern.getBoardType(indexInPattern),
+      pattern.getAlightType(indexInPattern)
+    );
+  }
+}
diff --git a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls
index 9b1628577b6..3c33b6b4d3a 100644
--- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls
+++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls
@@ -960,6 +960,8 @@ type Pattern implements Node {
   semanticHash: String
   "List of stops served by this pattern"
   stops: [Stop!]
+  "List of stops with pickup / dropoff type served by this pattern"
+  stopsInPattern: [StopInPattern!]!
   "Trips which run on this pattern"
   trips: [Trip!]
   "Trips which run on this pattern on the specified date"
@@ -2280,6 +2282,15 @@ type StopGeometries {
   googleEncoded: [Geometry]
 }
 
+type StopInPattern {
+  "NULL means that the stop is cancelled from the pattern."
+  dropOffType: PickupDropoffType
+  indexInPattern: Int!
+  "NULL means that the stop is cancelled from the pattern."
+  pickupType: PickupDropoffType
+  stop: Stop!
+}
+
 "Stop that should (but not guaranteed) to exist on a route."
 type StopOnRoute {
   "Route which contains the stop."
diff --git a/application/src/test/java/org/opentripplanner/apis/gtfs/model/StopInPatternModelTest.java b/application/src/test/java/org/opentripplanner/apis/gtfs/model/StopInPatternModelTest.java
new file mode 100644
index 00000000000..2f249024152
--- /dev/null
+++ b/application/src/test/java/org/opentripplanner/apis/gtfs/model/StopInPatternModelTest.java
@@ -0,0 +1,62 @@
+package org.opentripplanner.apis.gtfs.model;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;
+
+import org.junit.jupiter.api.Test;
+import org.opentripplanner.model.PickDrop;
+import org.opentripplanner.transit.model._data.TimetableRepositoryForTest;
+import org.opentripplanner.transit.model.network.Route;
+import org.opentripplanner.transit.model.network.StopPattern;
+import org.opentripplanner.transit.model.network.TripPattern;
+import org.opentripplanner.transit.model.site.RegularStop;
+
+public class StopInPatternModelTest {
+
+  private static final String ID = "1";
+  private static final String NAME = "short name";
+  private static final TimetableRepositoryForTest TEST_MODEL = TimetableRepositoryForTest.of();
+
+  private static final Route ROUTE = TimetableRepositoryForTest.route("routeId").build();
+  public static final RegularStop STOP_A = TEST_MODEL.stop("A").build();
+  public static final RegularStop STOP_B = TEST_MODEL.stop("B").build();
+  public static final RegularStop STOP_C = TEST_MODEL.stop("C").build();
+  private static final StopPattern STOP_PATTERN = getStopPattern();
+
+  private static StopPattern getStopPattern() {
+    var builder = StopPattern.create(3);
+
+    builder.stops.with(0, STOP_A);
+    builder.stops.with(1, STOP_B);
+    builder.stops.with(2, STOP_C);
+    builder.pickups.with(0, PickDrop.SCHEDULED);
+    builder.pickups.with(1, PickDrop.CALL_AGENCY);
+    builder.pickups.with(2, PickDrop.NONE);
+    builder.dropoffs.with(0, PickDrop.NONE);
+    builder.dropoffs.with(1, PickDrop.COORDINATE_WITH_DRIVER);
+    builder.dropoffs.with(2, PickDrop.SCHEDULED);
+    return builder.build();
+  }
+
+  public static final TripPattern PATTERN = TripPattern.of(id(ID))
+    .withName(NAME)
+    .withRoute(ROUTE)
+    .withStopPattern(STOP_PATTERN)
+    .build();
+
+  @Test
+  public void fromPatternAndIndex() {
+    assertEquals(
+      new StopInPatternModel(STOP_A, 0, PickDrop.SCHEDULED, PickDrop.NONE),
+      StopInPatternModel.fromPatternAndIndex(PATTERN, 0)
+    );
+    assertEquals(
+      new StopInPatternModel(STOP_B, 1, PickDrop.CALL_AGENCY, PickDrop.COORDINATE_WITH_DRIVER),
+      StopInPatternModel.fromPatternAndIndex(PATTERN, 1)
+    );
+    assertEquals(
+      new StopInPatternModel(STOP_C, 2, PickDrop.NONE, PickDrop.SCHEDULED),
+      StopInPatternModel.fromPatternAndIndex(PATTERN, 2)
+    );
+  }
+}