Conditionaly move certain posts at the bottom of the query result

As for fixing the issue in question, you should first always check that $apiresults[$bb24rrid] is set, i.e. exists, and secondly, you should create an array which stores the posts that should go at the bottom and then merge it with the $exp_query->posts array, instead of pushing items in the foreach loop.

// Array of posts (WP_Post objects) which should go at the bottom.
$bottom_posts = array();

foreach( $exp_query->posts as $key => $result ) {
    $bb24rrid = get_post_meta( $result->ID, '_b24_room_id', true);
    if ( isset( $apiresults[$bb24rrid] ) &&
        is_array( $apiresults[$bb24rrid] ) &&
        isset( $apiresults[$bb24rrid]['minstay'] )
    ) {
        $bottom_posts[] = $exp_query->posts[$key];
        unset($exp_query->posts[$key]);
    }
}

$exp_query->posts = array_merge( $exp_query->posts, $bottom_posts );

The other possible way to achieve your goal is by using 2 while ( $exp_query->have_posts() ) loops like so, where the first one excludes the posts that should go at the bottom, and the second loop displays only those excluded posts. This approach is best used with a template part, to avoid duplicating codes in the loops.

// Array of posts (IDs) which should go at the bottom.
$bottom_posts = array();

while ( $exp_query->have_posts() ) {
    $exp_query->the_post();

    $post_id  = get_the_ID();
    $bb24rrid = get_post_meta( $post_id, '_b24_room_id', true );

    if ( isset( $apiresults[$bb24rrid] ) &&
        is_array( $apiresults[$bb24rrid] ) &&
        isset( $apiresults[$bb24rrid]['minstay'] )
    ) {
        $bottom_posts[] = $post_id;
        continue;
    }

    get_template_part( 'your-template-part' );
}

while ( $exp_query->have_posts() ) {
    $exp_query->the_post();

    if ( ! in_array( get_the_ID(), $bottom_posts ) ) {
        continue;
    }

    get_template_part( 'your-template-part' );
}

Or you could instead use $bottom_posts[] = get_post(); and then replace the 2nd loop above with:

global $post;
foreach ( $bottom_posts as $post ) {
    setup_postdata( $post );
    get_template_part( 'your-template-part' );
}

Additional Notes

  1. I believe it’s more performant if you remove the 'orderby' => 'rand' and then randomize the posts via PHP, e.g. do a shuffle( $exp_query->posts ); right after the new WP_Query() call.

  2. 'posts_per_page' => -1 (which disables pagination or enables a “no LIMIT” query) should be avoided, and instead, use a number which you know would never be reached, e.g. if the query would only ever return at most 60 posts, then use 'posts_per_page' => 60. (If you’re thinking about using 999 or another high number, though, then you should better off use pagination, e.g. 30 posts per page.)

tech