diff --git a/L10n/en.template b/L10n/en.template
index 9fef51c7..765908a2 100644
--- a/L10n/en.template
+++ b/L10n/en.template
@@ -157,6 +157,9 @@
 /* [edsm.py] */
 "Error: Can't connect to EDSM" = "Error: Can't connect to EDSM";
 
+/* [inara.py] */
+"Error: Can't connect to Inara" = "Error: Can't connect to Inara";
+
 /* [edsm.py] */
 "Error: EDSM {MSG}" = "Error: EDSM {MSG}";
 
@@ -169,6 +172,9 @@
 /* Raised when the Companion API server thinks that the user has not purchased E:D. i.e. doesn't have the correct 'SKU'. [companion.py] */
 "Error: Frontier server SKU problem" = "Error: Frontier server SKU problem";
 
+/* [inara.py] */
+"Error: Inara {MSG}" = "Error: Inara {MSG}";
+
 /* [companion.py] */
 "Error: Invalid Credentials" = "Error: Invalid Credentials";
 
@@ -235,6 +241,9 @@
 /* Tab heading in settings. [prefs.py] */
 "Identity" = "Identity";
 
+/* Section heading in settings. [inara.py] */
+"Inara credentials" = "Inara credentials";
+
 /* Hotkey/Shortcut settings prompt on OSX. [prefs.py] */
 "Keyboard shortcut" = "Keyboard shortcut";
 
@@ -431,7 +440,10 @@
 "Semi Professional" = "Semi Professional";
 
 /* [edsm.py] */
-"Send flight log to Elite Dangerous Star Map" = "Send flight log to Elite Dangerous Star Map";
+"Send flight log and Cmdr status to EDSM" = "Send flight log and Cmdr status to EDSM";
+
+/* [inara.py] */
+"Send flight log and Cmdr status to Inara" = "Send flight log and Cmdr status to Inara";
 
 /* Output setting. [prefs.py] */
 "Send station data to the Elite Dangerous Data Network" = "Send station data to the Elite Dangerous Data Network";
diff --git a/README.md b/README.md
index b16ea059..c7bd5b95 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,7 @@ This app downloads your Cmdr's details and system, faction, scan and station dat
 * saves station commodity market prices to files on your computer that you can load into trading tools such as [Trade Dangerous](https://bitbucket.org/kfsone/tradedangerous/wiki/Home), [Inara](http://inara.cz), [mEDI's Elite Tools](https://github.com/mEDI-S/mEDI_s-Elite-Tools), etc.
 * saves a record of your ship loadout to files on your computer that you can load into outfitting tools such as [E:D Shipyard](http://www.edshipyard.com), [Coriolis](http://coriolis.io) or [Elite Trade Net](http://etn.io/).
 * sends your Cmdr's details, ship details, materials and flight log to [Elite: Dangerous Star Map](http://www.edsm.net/).
+* sends your Cmdr's details, ship details, materials and flight log to [Inara](https://inara.cz).
 
 You can run the app on the same machine on which you're running Elite: Dangerous or on another machine connected via a network share.
 
@@ -85,7 +86,11 @@ Some options work by reading the Elite: Dangerous game's log files. If you're ru
 
 ### EDSM
 
-You can send a record of your Cmdr's details, ship details, materials and flight log to [Elite: Dangerous Star Map](http://www.edsm.net/). You will need to register for an account and then follow the “[Elite Dangerous Star Map credentials](http://www.edsm.net/settings/api)” link to obtain your API key.
+You can send a record of your Cmdr's details, ship details, cargo, materials and flight log to [Elite: Dangerous Star Map](http://www.edsm.net/). You will need to register for an account and then follow the “[Elite Dangerous Star Map credentials](http://www.edsm.net/settings/api)” link to obtain your API key.
+
+### Inara
+
+You can send a record of your Cmdr's details, ship details, cargo, materials and flight log to [Inara](https://inara.cz/). You will need to register for an account and then follow the “[Inara credentials](https://inara.cz/settings-api/)” link to obtain your API key.
 
 
 Uninstall
diff --git a/plugins/edsm.py b/plugins/edsm.py
index 37ca0cb5..46e028f3 100644
--- a/plugins/edsm.py
+++ b/plugins/edsm.py
@@ -93,7 +93,7 @@ def plugin_prefs(parent, cmdr, is_beta):
 
     HyperlinkLabel(frame, text='Elite Dangerous Star Map', background=nb.Label().cget('background'), url='https://www.edsm.net/', underline=True).grid(columnspan=2, padx=PADX, sticky=tk.W)	# Don't translate
     this.log = tk.IntVar(value = config.getint('edsm_out') and 1)
-    this.log_button = nb.Checkbutton(frame, text=_('Send flight log to Elite Dangerous Star Map'), variable=this.log, command=prefsvarchanged)
+    this.log_button = nb.Checkbutton(frame, text=_('Send flight log and Cmdr status to EDSM'), variable=this.log, command=prefsvarchanged)
     this.log_button.grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W)
 
     nb.Label(frame).grid(sticky=tk.W)	# big spacer
diff --git a/plugins/inara.py b/plugins/inara.py
new file mode 100644
index 00000000..47e62af2
--- /dev/null
+++ b/plugins/inara.py
@@ -0,0 +1,349 @@
+#
+# Inara sync
+#
+
+from collections import OrderedDict
+import json
+import requests
+import sys
+import time
+import urllib2
+from calendar import timegm
+from Queue import Queue
+from threading import Thread
+
+import Tkinter as tk
+from ttkHyperlinkLabel import HyperlinkLabel
+import myNotebook as nb
+
+from config import appname, applongname, appversion, config
+import companion
+import coriolis
+import edshipyard
+import outfitting
+import plug
+
+if __debug__:
+    from traceback import print_exc
+
+_TIMEOUT = 20
+FAKE = ['CQC', 'Training', 'Destination']	# Fake systems that shouldn't be sent to Inara
+
+
+this = sys.modules[__name__]	# For holding module globals
+this.session = requests.Session()
+this.queue = Queue()	# Items to be sent to Inara by worker thread
+
+# Game state
+this.multicrew = False	# don't send captain's ship info to Inara while on a crew
+
+# Cached Cmdr state
+this.location = None
+this.cargo = None
+this.materials = None
+
+def plugin_start():
+
+    # Migrate old settings
+    if not config.get('inara_cmdrs'):
+        if isinstance(config.get('cmdrs'), list) and config.get('inara_usernames') and config.get('inara_apikeys'):
+            # Migrate <= 2.34 settings
+            config.set('inara_cmdrs', config.get('cmdrs'))
+        elif config.get('inara_cmdrname'):
+            # Migrate <= 2.25 settings. inara_cmdrs is unknown at this time
+            config.set('inara_usernames', [config.get('inara_cmdrname') or ''])
+            config.set('inara_apikeys',   [config.get('inara_apikey') or ''])
+        config.delete('inara_cmdrname')
+        config.delete('inara_apikey')
+    if config.getint('output') & 256:
+        # Migrate <= 2.34 setting
+        config.set('inara_out', 1)
+    config.delete('inara_autoopen')
+    config.delete('inara_historical')
+
+    this.thread = Thread(target = worker, name = 'Inara worker')
+    this.thread.daemon = True
+    this.thread.start()
+
+    return 'Inara'
+
+def plugin_close():
+    # Signal thread to close and wait for it
+    this.queue.put(None)
+    this.thread.join()
+    this.thread = None
+
+def plugin_prefs(parent, cmdr, is_beta):
+
+    PADX = 10
+    BUTTONX = 12	# indent Checkbuttons and Radiobuttons
+    PADY = 2		# close spacing
+
+    frame = nb.Frame(parent)
+    frame.columnconfigure(1, weight=1)
+
+    HyperlinkLabel(frame, text='Inara', background=nb.Label().cget('background'), url='https://inara.cz/', underline=True).grid(columnspan=2, padx=PADX, sticky=tk.W)	# Don't translate
+    this.log = tk.IntVar(value = config.getint('inara_out') and 1)
+    this.log_button = nb.Checkbutton(frame, text=_('Send flight log and Cmdr status to Inara'), variable=this.log, command=prefsvarchanged)
+    this.log_button.grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W)
+
+    nb.Label(frame).grid(sticky=tk.W)	# big spacer
+    this.label = HyperlinkLabel(frame, text=_('Inara credentials'), background=nb.Label().cget('background'), url='https://inara.cz/settings-api', underline=True)	# Section heading in settings
+    this.label.grid(columnspan=2, padx=PADX, sticky=tk.W)
+
+    this.apikey_label = nb.Label(frame, text=_('API Key'))	# EDSM setting
+    this.apikey_label.grid(row=12, padx=PADX, sticky=tk.W)
+    this.apikey = nb.Entry(frame)
+    this.apikey.grid(row=12, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
+
+    prefs_cmdr_changed(cmdr, is_beta)
+
+    return frame
+
+def prefs_cmdr_changed(cmdr, is_beta):
+    this.log_button['state'] = cmdr and not is_beta and tk.NORMAL or tk.DISABLED
+    this.apikey['state'] = tk.NORMAL
+    this.apikey.delete(0, tk.END)
+    if cmdr:
+        cred = credentials(cmdr)
+        if cred:
+            this.apikey.insert(0, cred)
+    this.label['state'] = this.apikey_label['state'] = this.apikey['state'] = cmdr and not is_beta and this.log.get() and tk.NORMAL or tk.DISABLED
+
+def prefsvarchanged():
+    this.label['state'] = this.apikey_label['state'] = this.apikey['state'] = this.log.get() and this.log_button['state'] or tk.DISABLED
+
+def prefs_changed(cmdr, is_beta):
+    config.set('inara_out', this.log.get())
+
+    print 'prefs_changed', cmdr, is_beta
+
+    if cmdr and not is_beta:
+        cmdrs = config.get('inara_cmdrs') or []
+        apikeys = config.get('inara_apikeys') or []
+        if cmdr in cmdrs:
+            idx = cmdrs.index(cmdr)
+            apikeys.extend([''] * (1 + idx - len(apikeys)))
+            apikeys[idx] = this.apikey.get().strip()
+        else:
+            config.set('inara_cmdrs', cmdrs + [cmdr])
+            apikeys.append(this.apikey.get().strip())
+        config.set('inara_apikeys', apikeys)
+    # TODO: schedule a call with callback if changed
+
+def credentials(cmdr):
+    # Credentials for cmdr
+    if not cmdr:
+        return None
+
+    cmdrs = config.get('inara_cmdrs') or []
+    if cmdr in cmdrs and config.get('inara_apikeys'):
+        return config.get('inara_apikeys')[cmdrs.index(cmdr)]
+    else:
+        return None
+
+
+def journal_entry(cmdr, is_beta, system, station, entry, state):
+
+    this.multicrew = bool(state['Role'])
+
+    if entry['event'] == 'LoadGame':
+        # clear cached state
+        this.location = None
+        this.cargo = None
+        this.materials = None
+
+    # Send location and status on new game or StartUp. Assumes Location is the last event on a new game (other than Docked).
+    # Always send an update on Docked, Undocked, FSDJump, Promotion and EngineerProgress.
+    # Also send material and cargo (if changed) whenever we send an update.
+
+    if config.getint('inara_out') and not is_beta and not multicrew and credentials(cmdr):
+        try:
+            events = []
+
+            # Send credits to Inara on new game (but not on startup - data might be old)
+            if entry['event'] == 'Location':
+                # TODO: 'commanderAssets'
+                add_event(events, 'setCommanderCredits', entry['timestamp'],
+                          OrderedDict([
+                              ('commanderCredits', state['Credits']),
+                              ('commanderLoan', state['Loan']),
+                          ]))
+
+            # Send rank info to Inara on startup or change
+            if entry['event'] in ['StartUp', 'Location'] and state['Rank']:
+                for k,v in state['Rank'].iteritems():
+                    if v is not None:
+                        add_event(events, 'setCommanderRankPilot', entry['timestamp'],
+                                  OrderedDict([
+                                      ('rankName', k.lower()),
+                                      ('rankValue', v[0]),
+                                      ('rankProgress', v[1] / 100.0),
+                                  ]))
+            elif entry['event'] == 'Promotion':
+                for k,v in state['Rank'].iteritems():
+                    if k in entry:
+                        add_event(events, 'setCommanderRankPilot', entry['timestamp'],
+                                  OrderedDict([
+                                      ('rankName', k.lower()),
+                                      ('rankValue', v[0]),
+                                      ('rankProgress', 0),
+                                  ]))
+
+            # Send engineer status to Inara on change (not available on startup)
+            if entry['event'] == 'EngineerProgress':
+                if 'Rank' in entry:
+                    add_event(events, 'setCommanderRankEngineer', entry['timestamp'],
+                              OrderedDict([
+                                  ('engineerName', entry['Engineer']),
+                                  ('rankValue', entry['Rank']),
+                              ]))
+                else:
+                    add_event(events, 'setCommanderRankEngineer', entry['timestamp'],
+                              OrderedDict([
+                                  ('engineerName', entry['Engineer']),
+                                  ('rankStage', entry['Progress']),
+                              ]))
+
+            # Send PowerPlay status to Inara on change (not available on startup, and promotion not available at all)
+            if entry['event'] == 'PowerplayJoin':
+                add_event(events, 'setCommanderRankPower', entry['timestamp'],
+                          OrderedDict([
+                              ('powerName', entry['Power']),
+                              ('rankValue', 1),
+                          ]))
+            elif entry['event'] == 'PowerplayLeave':
+                add_event(events, 'setCommanderRankPower', entry['timestamp'],
+                          OrderedDict([
+                              ('powerName', entry['Power']),
+                              ('rankValue', 0),
+                          ]))
+            elif entry['event'] == 'PowerplayDefect':
+                add_event(events, 'setCommanderRankPower', entry['timestamp'],
+                          OrderedDict([
+                              ('powerName', entry['ToPower']),
+                              ('rankValue', 1),
+                          ]))
+
+            # Update location
+            if entry['event'] == 'Location':
+                if entry.get('Docked'):
+                    add_event(events, 'setCommanderTravelLocation', entry['timestamp'],
+                              OrderedDict([
+                                  ('starsystemName', entry['StarSystem']),
+                                  ('stationName', entry['StationName']),
+                                  ('shipType', companion.ship_map.get(state['ShipType'], state['ShipType'])),
+                                  ('shipGameID', state['ShipID']),
+                              ]))
+                    this.location = (entry['StarSystem'], entry['StationName'])
+                else:
+                    add_event(events, 'setCommanderTravelLocation', entry['timestamp'],
+                              OrderedDict([
+                                  ('starsystemName', entry['StarSystem']),
+                                  ('shipType', companion.ship_map.get(state['ShipType'], state['ShipType'])),
+                                  ('shipGameID', state['ShipID']),
+                              ]))
+                    this.location = (entry['StarSystem'], None)
+
+            elif entry['event'] == 'Docked' and this.location != (entry['StarSystem'], entry['StationName']):
+                # Don't send docked event on new game - i.e. following 'Location' event
+                add_event(events, 'addCommanderTravelDock', entry['timestamp'],
+                          OrderedDict([
+                              ('starsystemName', entry['StarSystem']),
+                              ('stationName', entry['StationName']),
+                              ('shipType', companion.ship_map.get(state['ShipType'], state['ShipType'])),
+                              ('shipGameID', state['ShipID']),
+                          ]))
+                this.location = (entry['StarSystem'], entry['StationName'])
+
+            elif entry['event'] == 'Undocked' and this.location:
+                add_event(events, 'setCommanderTravelLocation', entry['timestamp'],
+                          OrderedDict([
+                              ('starsystemName', this.location[0]),
+                              ('shipType', companion.ship_map.get(state['ShipType'], state['ShipType'])),
+                              ('shipGameID', state['ShipID']),
+                          ]))
+                this.location = (this.location[0], None)
+
+            elif entry['event'] == 'FSDJump':
+                add_event(events, 'addCommanderTravelFSDJump', entry['timestamp'],
+                          OrderedDict([
+                              ('starsystemName', entry['StarSystem']),
+                              ('jumpDistance', entry['JumpDist']),
+                              ('shipType', companion.ship_map.get(state['ShipType'], state['ShipType'])),
+                              ('shipGameID', state['ShipID']),
+                          ]))
+                this.location = (entry['StarSystem'], None)
+
+            if events:
+                # Send cargo and materials to Inara if changed and if we're sending any other kind of update
+                cargo = [ OrderedDict([('itemName', k), ('itemCount', state['Cargo'][k])]) for k in sorted(state['Cargo']) ]
+                if this.cargo != cargo:
+                    add_event(events, 'setCommanderInventoryCargo', entry['timestamp'], cargo)
+                    this.cargo = cargo
+                materials = []
+                for category in ['Raw', 'Manufactured', 'Encoded']:
+                    materials.extend([ OrderedDict([('itemName', k), ('itemCount', state[category][k])]) for k in sorted(state[category]) ])
+                if this.materials != materials:
+                    add_event(events, 'setCommanderInventoryMaterials', entry['timestamp'],  materials)
+                    this.materials = materials
+
+                # Queue a call to Inara
+                call(cmdr, events)
+
+        except Exception as e:
+            if __debug__: print_exc()
+            return unicode(e)
+
+def add_event(events, name, timestamp, data):
+    events.append(OrderedDict([
+        ('eventName', name),
+        ('eventTimestamp', timestamp),
+        ('eventData', data),
+    ]))
+
+
+# Queue a call to Inara, handled in Worker thread
+def call(cmdr, events, callback=None):
+    args = OrderedDict([
+        ('header', OrderedDict([
+        ('appName', applongname),
+            ('appVersion', appversion),
+            ('isDeveloped', True),	# TODO: Remove before release
+            ('APIkey', credentials(cmdr)),
+            ('commanderName', cmdr.encode('utf-8')),
+        ])),
+        ('events', events),
+    ])
+    this.queue.put(('https://inara.cz/inapi/v1/', json.dumps(args, separators = (',', ':')), None))
+
+# Worker thread
+def worker():
+    while True:
+        item = this.queue.get()
+        if not item:
+            return	# Closing
+        else:
+            (url, data, callback) = item
+
+        retrying = 0
+        while retrying < 3:
+            try:
+                r = this.session.post(url, data=data, timeout=_TIMEOUT)
+                r.raise_for_status()
+                reply = r.json()
+                status = reply['header']['eventStatus']
+                if callback:
+                    callback(reply)
+                elif status // 100 != 2:	# 2xx == OK (maybe with warnings)
+                    plug.show_error(_('Error: Inara {MSG}').format(MSG = reply['header'].get('eventStatusText', status)))
+                    if __debug__: print r.content
+                break
+            except:
+                if __debug__: print_exc()
+                retrying += 1
+        else:
+            if callback:
+                callback(None)
+            else:
+                plug.show_error(_("Error: Can't connect to Inara"))