diff --git a/src/metabase/query_processor/card.clj b/src/metabase/query_processor/card.clj
index b1f81f7d090577aa5d8e26591e4a77eb2227f12a..3cff5cc33f4359ccc9ed4d70d29690462b7e1425 100644
--- a/src/metabase/query_processor/card.clj
+++ b/src/metabase/query_processor/card.clj
@@ -71,17 +71,38 @@
   type of the parameter has to agree with the type of the template tag as well. This variable controls whether or not
   this constraint is enforced.
 
+  In 0.44.0+ explicit parameters can be defined on MBQL cards as well. Parameters supplied in a request must correspond
+  by ID to parameters defined explicitly on the card and have an appropriate type, unless this variable is `true`.
+
   Normally, when running a query in the context of a /Card/, this is `false`, and the constraint is enforced. By
   binding this to a truthy value you can disable the checks. Currently this is only done
   by [[metabase.query-processor.dashboard]], which does its own parameter validation before handing off to the code
   here."
   false)
 
-(defn- card-template-tag-parameters
-  "Template tag parameters that have been specified for the query for Card with `card-id`, if any, returned as a map in
+(defn- card-parameters
+  "Parameters on provided Card, returned as a map with the format
+
+    {\"parameter_id\" :parameter-type ...}
+
+  This allows the expected type of a request parameter targeting a field on an MBQL query to be quickly looked up by ID.
+  Excludes parameters that do not have IDs."
+  [{parameters :parameters}]
+  (into {} (comp
+            (map (juxt :id :type))
+            (remove (comp nil? first)))
+          parameters))
+
+(defn- card-template-tags
+  "Template tags that have been specified for the query in the provided Card, if any, returned as a map in
   the format
 
-    {\"template_tag_parameter_name\" :parameter-type, ...}
+    {\"template_tag_name\" :parameter-type,
+     \"template_tag_id\"   :parameter-type
+     ...}
+
+  This allows the expected type of a request parameter targeting a template tag to be quickly looked up by *either*
+  name or ID.
 
   Template tag parameter name is the name of the parameter as it appears in the query, e.g. `{{id}}` has the `:name`
   `\"id\"`.
@@ -89,23 +110,22 @@
   Parameter type in this case is something like `:string` or `:number` or `:date/month-year`; parameters passed in as
   parameters to the API request must be allowed for this type (i.e. `:string/=` is allowed for a `:string` parameter,
   but `:number/=` is not)."
-  [card-id]
-  (let [query (api/check-404 (db/select-one-field :dataset_query Card :id card-id))]
-    (into
-     {}
-     (comp
-      (map (fn [[param-name {widget-type :widget-type, tag-type :type}]]
-             ;; Field Filter parameters have a `:type` of `:dimension` and the widget type that should be used is
-             ;; specified by `:widget-type`. Non-Field-filter parameters just have `:type`. So prefer
-             ;; `:widget-type` if available but fall back to `:type` if not.
-             (cond
-               (= tag-type :dimension)
-               [param-name widget-type]
-
-               (contains? mbql.s/raw-value-template-tag-types tag-type)
-               [param-name tag-type])))
-      (filter some?))
-     (get-in query [:native :template-tags]))))
+  [{query :dataset_query}]
+  (into
+   {}
+   (comp
+    (mapcat
+     (fn [[param-name {widget-type :widget-type, tag-type :type, id :id}]]
+       ;; Field Filter parameters have a `:type` of `:dimension` and the widget type that should be used is
+       ;; specified by `:widget-type`. Non-Field-filter parameters just have `:type`. So prefer
+       ;; `:widget-type` if available but fall back to `:type` if not.
+       (let [param-type (cond
+                          (= tag-type :dimension) widget-type
+                          (contains? mbql.s/raw-value-template-tag-types tag-type) tag-type)]
+         [[id param-type]
+          [param-name param-type]])))
+    (filter some?))
+   (get-in query [:native :template-tags])))
 
 (defn- allowed-parameter-type-for-template-tag-widget-type? [parameter-type widget-type]
   (when-let [allowed-template-tag-types (get-in mbql.s/parameter-types [parameter-type :allowed-for])]
@@ -150,19 +170,41 @@
 
 (s/defn ^:private validate-card-parameters
   "Unless [[*allow-arbitrary-mbql-parameters*]] is truthy, check to make all supplied `parameters` actually match up
-  with template tags in the query for Card with `card-id`."
-  [card-id :- su/IntGreaterThanZero parameters :- mbql.s/ParameterList]
+  with `parameters` defined on the Card with `card-id`, or template tags on the query."
+  [card-id :- su/IntGreaterThanZero request-parameters :- mbql.s/ParameterList]
   (when-not *allow-arbitrary-mbql-parameters*
-    (let [template-tags (card-template-tag-parameters card-id)]
-      (doseq [request-parameter parameters
-              :let              [parameter-name (infer-parameter-name request-parameter)]]
-        (let [matching-widget-type (or (get template-tags parameter-name)
-                                       (throw (ex-info (tru "Invalid parameter: Card {0} does not have a template tag named {1}."
-                                                            card-id
-                                                            (pr-str parameter-name))
-                                                       {:type               qp.error-type/invalid-parameter
-                                                        :invalid-parameter  request-parameter
-                                                        :allowed-parameters (keys template-tags)})))]
+    (let [card                   (api/check-404 (db/select-one [Card :dataset_query :parameters] :id card-id))
+          types-on-template-tags (card-template-tags card)
+          types-on-parameters    (card-parameters card)]
+      (doseq [request-parameter request-parameters
+              :let              [parameter-id   (:id request-parameter)
+                                 parameter-name (infer-parameter-name request-parameter)]]
+        (let [matching-widget-type
+              (if (seq types-on-parameters)
+                ;; If (non-template tag) parameters are defined on the card, request parameters should target them by
+                ;; ID, rather than targeting template tags directly (even if they also exist)
+                (or (get types-on-parameters parameter-id)
+                    (throw (ex-info (if parameter-id
+                                     (tru "Invalid parameter: Card {0} does not have a parameter with the ID {1}."
+                                          card-id
+                                          (pr-str parameter-id))
+                                     (tru "Invalid parameter: missing id"))
+                                    {:type               qp.error-type/invalid-parameter
+                                     :invalid-parameter  request-parameter
+                                     :allowed-parameters (keys types-on-parameters)})))
+                (or
+                 ;; Use ID preferentially, but fallback to name if ID is not present, as a safety net in case request
+                 ;; parameters are malformed. Only need to do this for parameters targeting template tags since card
+                 ;; parameters should always be targeted by ID.
+                 (get types-on-template-tags parameter-id)
+                 (get types-on-template-tags parameter-name)
+                 (throw (ex-info (tru "Invalid parameter: Card {0} does not have a template tag with the ID {1} or name {2}."
+                                      card-id
+                                      (pr-str parameter-id)
+                                      (pr-str parameter-name))
+                                 {:type               qp.error-type/invalid-parameter
+                                  :invalid-parameter  request-parameter
+                                  :allowed-parameters (keys types-on-template-tags)}))))]
           ;; now make sure the type agrees as well
           (check-allowed-parameter-value-type parameter-name matching-widget-type (:type request-parameter)))))))
 
diff --git a/test/metabase/query_processor/card_test.clj b/test/metabase/query_processor/card_test.clj
index 0b7ccf186581dc80b966ee81472266aaafe9c9b8..0df68c1a2d6817f42fa81e07b9f5b0ae1b37937b 100644
--- a/test/metabase/query_processor/card_test.clj
+++ b/test/metabase/query_processor/card_test.clj
@@ -74,33 +74,33 @@
                                :default      "1"}}
               :query         "SELECT *\nFROM ORDERS\nWHERE id = {{id}}"}})
 
-(deftest card-template-tag-parameters-test
+(deftest card-template-tags-test
   (testing "Card with a Field filter parameter"
-    (mt/with-temp Card [{card-id :id} {:dataset_query (field-filter-query)}]
-      (is (= {"date" :date/all-options}
-             (#'qp.card/card-template-tag-parameters card-id)))))
+    (mt/with-temp Card [card {:dataset_query (field-filter-query)}]
+      (is (= {"date" :date/all-options, "_DATE_" :date/all-options}
+             (#'qp.card/card-template-tags card)))))
   (testing "Card with a non-Field-filter parameter"
-    (mt/with-temp Card [{card-id :id} {:dataset_query (non-field-filter-query)}]
-      (is (= {"id" :number}
-             (#'qp.card/card-template-tag-parameters card-id)))))
+    (mt/with-temp Card [card {:dataset_query (non-field-filter-query)}]
+      (is (= {"id" :number, "_ID_" :number}
+             (#'qp.card/card-template-tags card)))))
   (testing "Should ignore native query snippets and source card IDs"
-    (mt/with-temp Card [{card-id :id} {:dataset_query (assoc (non-field-filter-query)
-                                                             "abcdef"
-                                                             {:id           "abcdef"
-                                                              :name         "#1234"
-                                                              :display-name "#1234"
-                                                              :type         :card
-                                                              :card-id      1234}
+    (mt/with-temp Card [card {:dataset_query (assoc (non-field-filter-query)
+                                                    "abcdef"
+                                                    {:id           "abcdef"
+                                                     :name         "#1234"
+                                                     :display-name "#1234"
+                                                     :type         :card
+                                                     :card-id      1234}
 
-                                                             "xyz"
-                                                             {:id           "xyz"
-                                                              :name         "snippet: My Snippet"
-                                                              :display-name "Snippet: My Snippet"
-                                                              :type         :snippet
-                                                              :snippet-name "My Snippet"
-                                                              :snippet-id   1})}]
-      (is (= {"id" :number}
-             (#'qp.card/card-template-tag-parameters card-id))))))
+                                                    "xyz"
+                                                    {:id           "xyz"
+                                                     :name         "snippet: My Snippet"
+                                                     :display-name "Snippet: My Snippet"
+                                                     :type         :snippet
+                                                     :snippet-name "My Snippet"
+                                                     :snippet-id   1})}]
+      (is (= {"id" :number, "_ID_" :number}
+             (#'qp.card/card-template-tags card))))))
 
 (deftest infer-parameter-name-test
   (is (= "my_param"
@@ -110,20 +110,20 @@
   (is (= nil
          (#'qp.card/infer-parameter-name {:target [:field 1000 nil]}))))
 
-(deftest validate-card-parameters-test
+(deftest validate-card-template-tag-test
   (mt/with-temp Card [{card-id :id} {:dataset_query (field-filter-query)}]
     (testing "Should disallow parameters that aren't actually part of the Card"
       (is (thrown-with-msg?
            clojure.lang.ExceptionInfo
-           #"Invalid parameter: Card [\d,]+ does not have a template tag named \"fake\""
+           #"Invalid parameter: Card [\d,]+ does not have a template tag with the ID \"_FAKE_\" or name \"fake\""
            (#'qp.card/validate-card-parameters card-id [{:id    "_FAKE_"
                                                          :name  "fake"
                                                          :type  :date/single
                                                          :value "2016-01-01"}])))
       (testing "As an API request"
-        (is (schema= {:message            #"Invalid parameter: Card [\d,]+ does not have a template tag named \"fake\".+"
+        (is (schema= {:message            #"Invalid parameter: Card [\d,]+ does not have a template tag with the ID \"_FAKE_\" or name \"fake\""
                       :invalid-parameter  (s/eq {:id "_FAKE_", :name "fake", :type "date/single", :value "2016-01-01"})
-                      :allowed-parameters (s/eq ["date"])
+                      :allowed-parameters (s/eq ["_DATE_" "date"])
                       s/Keyword           s/Any}
                      (mt/user-http-request :rasta :post (format "card/%d/query" card-id)
                                            {:parameters [{:id    "_FAKE_"
@@ -154,10 +154,50 @@
                   (is (= nil
                          (validate disallowed-type))))))))))
 
-    (testing "Happy path -- API request should succeed if parameter is valid"
+    (testing "Happy path -- API request should succeed if parameter correlates to a template tag by ID"
+      (is (= [1000]
+             (mt/first-row (mt/user-http-request :rasta :post (format "card/%d/query" card-id)
+                                                 {:parameters [{:id    "_DATE_"
+                                                                :type  :date/single
+                                                                :value "2016-01-01"}]})))))
+
+    (testing "Happy path -- API request should succeed if parameter correlates to a template tag by name"
+      (is (= [1000]
+             (mt/first-row (mt/user-http-request :rasta :post (format "card/%d/query" card-id)
+                                                 {:parameters [{:name  "date"
+                                                                :type  :date/single
+                                                                :value "2016-01-01"}]})))))))
+
+(deftest validate-card-parameters-test
+  (mt/with-temp Card [{card-id :id} {:dataset_query (mt/mbql-query checkins {:aggregation [[:count]]})
+                                     :parameters [{:id   "_DATE_"
+                                                   :type :date/single
+                                                   :name "Date"
+                                                   :slug "DATE"}]}]
+    (testing "API request should fail if request parameter does not contain ID"
+      (is (schema= {:message            #"Invalid parameter: missing id"
+                    :invalid-parameter  (s/eq {:name "date", :type "date/single", :value "2016-01-01"})
+                    :allowed-parameters (s/eq ["_DATE_"])
+                    s/Keyword           s/Any}
+                   (mt/user-http-request :rasta :post (format "card/%d/query" card-id)
+                                         {:parameters [{:name  "date"
+                                                        :type  :date/single
+                                                        :value "2016-01-01"}]}))))
+
+    (testing "API request should fail if request parameter ID does not exist on the card"
+      (is (schema= {:message            #"Invalid parameter: Card [\d,]+ does not have a parameter with the ID \"_FAKE_\"."
+                    :invalid-parameter  (s/eq {:name "date", :id "_FAKE_" :type "date/single", :value "2016-01-01"})
+                    :allowed-parameters (s/eq ["_DATE_"])
+                    s/Keyword           s/Any}
+                   (mt/user-http-request :rasta :post (format "card/%d/query" card-id)
+                                         {:parameters [{:id    "_FAKE_"
+                                                        :name  "date"
+                                                        :type  :date/single
+                                                        :value "2016-01-01"}]}))))
+
+    (testing "Happy path -- API request should succeed if request parameter correlates to a card parameter by ID"
       (is (= [1000]
              (mt/first-row (mt/user-http-request :rasta :post (format "card/%d/query" card-id)
                                                  {:parameters [{:id    "_DATE_"
-                                                                :name  "date"
                                                                 :type  :date/single
                                                                 :value "2016-01-01"}]})))))))