Q:

Ruby: any difference between == elsif != VS. == else?

Is there any practical difference in the results of these two little chunks of logic, if you are just checking for one specific condition and everything else is NOT X?
def country
    if params[:ip_country_code] == "X"
        {:api_key => 1}
    else
        {:api_key => 2}
    end
end

vs.
def country
    if params[:ip_country_code] == "X"
        {:api_key => 1}
    elsif params[:ip_country_code] != "X"
        {:api_key => 2}
    else    
    end
end

A:

if is good or two mutually exclusive choices.
elsif is usually used for multiple (or two not mutually exclusive) choices for different conditions.
multiple choices for the same condition are usually covered with with clause. 

While there is no logical difference between those, the proper way to write this would be:
def country
  value =
    case params[:ip_country_code]
    when "X" then 1
    else 2
    end

  # or:
  value =
    if params[:ip_country_code] == 'X'
      1
    else
      2
    end

  # or even with ternary:
  value = params[:ip_country_code] == 'X' ? 1 : 2

  {api_key: value}
end

A:

The difference if that using elsif generates one more comparison, therefore requiring a bit more work from the processor. You can disassemble it using following script:
code = <<~CODE
  a = 4
  if a == 4
    puts "equal"
  else
    puts "not equal"
  end
CODE

code2 = <<~CODE
  a = 4
  if a == 4
    puts "equal"
  elseif a != 4
    puts "not equal"
  end
CODE

puts RubyVM::InstructionSequence.compile(code).disasm
puts "\n\n----------------------\n\n"
puts RubyVM::InstructionSequence.compile(code2).disasm

Result for first one (if-else):
== disasm: #<ISeq:<compiled>@<compiled>>================================
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] a
0000 trace            1                                               (   1)
0002 putobject        4
0004 setlocal_OP__WC__0 2
0006 trace            1                                               (   2)
0008 getlocal_OP__WC__0 2
0010 putobject        4
0012 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0015 branchunless     26
0017 trace            1                                               (   3)
0019 putself
0020 putstring        "equal"
0022 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0025 leave                                                            (   2)
0026 trace            1                                               (   5)
0028 putself
0029 putstring        "not equal"
0031 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0034 leave

While for the second (if-elsif):
== disasm: #<ISeq:<compiled>@<compiled>>================================
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] a
0000 trace            1                                               (   1)
0002 putobject        4
0004 setlocal_OP__WC__0 2
0006 trace            1                                               (   2)
0008 getlocal_OP__WC__0 2
0010 putobject        4
0012 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0015 branchunless     51
0017 trace            1                                               (   3)
0019 putself
0020 putstring        "equal"
0022 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0025 pop
0026 trace            1                                               (   4)
0028 putself
0029 getlocal_OP__WC__0 2
0031 putobject        4
0033 opt_neq          <callinfo!mid:!=, argc:1, ARGS_SIMPLE>, <callcache>, <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0038 opt_send_without_block <callinfo!mid:elseif, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0041 pop
0042 trace            1                                               (   5)
0044 putself
0045 putstring        "not equal"
0047 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0050 leave                                                            (   2)
0051 putnil                                                           (   5)
0052 leave

As you see, second one generates more instructions so if done billions time, it will be slightly slower. I don't know if that's the "practicality" you meant.
In your case there will be even more overhead with accessing values from the hash. If you perform some really "heavy" operations in condition, it will be repeated twice and can have real impact on performance.
Case case
As asked in the comments, here's a version for case-based solution:
code3 = <<~CODE
  a = 4
  case a
  when 4 then puts "equal"
  else puts "not equal"
  end
CODE

Result:
== disasm: #<ISeq:<compiled>@<compiled>>================================
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] a
0000 trace            1                                               (   1)
0002 putobject        4
0004 setlocal_OP__WC__0 2
0006 trace            1                                               (   2)
0008 getlocal_OP__WC__0 2
0010 dup
0011 opt_case_dispatch <cdhash>, 21
0014 dup                                                              (   3)
0015 putobject        4
0017 checkmatch       2
0019 branchif         31
0021 pop                                                              (   4)
0022 trace            1
0024 putself
0025 putstring        "not equal"
0027 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0030 leave
0031 pop                                                              (   5)
0032 trace            1                                               (   3)
0034 putself
0035 putstring        "equal"
0037 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0040 leave

Conclusion: this is only a bit longer that normal if-else and still much better that if-ifelse.
EDIT: I also noticed mistake in disassembled if-else, updated the answer.

