How do I query posts by a sub value with the API?

To access posts via meta key you need to hook into the [rest_query_vary filter].1
The example I was thinking of when I read your question is the same one found on the post @WebElaine commented. I’ve merely added it below:

function my_allow_meta_query( $valid_vars ) {

    $valid_vars = array_merge( $valid_vars, array( 'meta_key', 'meta_value' ) );
    return $valid_vars;
}
add_filter( 'rest_query_vars', 'my_allow_meta_query' );

Calling like so:
wp-json/wp/v2/posts?filter[meta_key]=MY-KEY&filter[meta_value]=MY-VALUE


The filter is applied as part of WP_REST_Posts_Controller::prepare_items_query, shown in context here in case that helps:

protected function prepare_items_query( $prepared_args = array(), $request = null ) {
    $query_args = array();

    foreach ( $prepared_args as $key => $value ) {
        /**
         * Filters the query_vars used in get_items() for the constructed query.
         *
         * The dynamic portion of the hook name, `$key`, refers to the query_var key.
         *
         * @since 4.7.0
         *
         * @param string $value The query_var value.
         */
        $query_args[ $key ] = apply_filters( "rest_query_var-{$key}", $value );
    }

    if ( 'post' !== $this->post_type || ! isset( $query_args['ignore_sticky_posts'] ) ) {
        $query_args['ignore_sticky_posts'] = true;
    }

    // Map to proper WP_Query orderby param.
    if ( isset( $query_args['orderby'] ) && isset( $request['orderby'] ) ) {
        $orderby_mappings = array(
            'id'      => 'ID',
            'include' => 'post__in',
            'slug'    => 'post_name',
        );

        if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
            $query_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
        }
    }

    return $query_args;
}