diff --git a/google_appliance.admin.inc b/google_appliance.admin.inc
index 6dc4c25..1bb174e 100644
--- a/google_appliance.admin.inc
+++ b/google_appliance.admin.inc
@@ -54,6 +54,33 @@ function google_appliance_admin_settings($form) {
     '#required' => TRUE,
   );
 
+  // Authentication for administrative API.
+  $form['authentication'] = array(
+    '#title' => t('Authentication (Administrative API)'),
+    '#type' => 'fieldset',
+    '#collapsible' => TRUE,
+    '#collapsed' => FALSE,
+    '#description' => t('You may provide credentials to administer the GSA from Drupal. Additional tabs will show up depending on your permissions.'),
+  );
+  $form['authentication']['username'] = array(
+    '#type' => 'textfield',
+    '#title' => t('User Name'),
+    '#description' => t('A user name that has an Admin Console administrator account on the GSA.'),
+    '#default_value' => $settings['username'],
+  );
+  $form['authentication']['password'] = array(
+    '#type' => 'password',
+    '#title' => t('Password'),
+    '#description' => t('The password for the Admin Console account.'),
+    '#default_value' => '', // Don't reveal the password in the form.
+  );
+  $form['authentication']['verify_ssl'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Verify SSL certificate on the GSA'),
+    '#description' => t('Uncheck this box if your GSA uses a self-signed certificate.'),
+    '#default_value' => $settings['verify_ssl'],
+  );
+
   // search query parameter configuration
   $form['query_param'] = array(
     "#title" => t("Search Query Parameter Setup"),
@@ -201,6 +228,8 @@ function google_appliance_admin_settings_submit($form, &$form_state) {
     'collection',
     'frontend',
     'timeout',
+    'username',
+    'verify_ssl',
     'autofilter',
     'query_inspection',
     'search_title',
@@ -214,6 +243,12 @@ function google_appliance_admin_settings_submit($form, &$form_state) {
     variable_set('google_appliance_' . $field, trim($form_state['values'][$field]));
   }
 
+  // The default value of 'password' is empty.
+  // Update password only if user has set a value.
+  if (!empty($form_state['values']['password'])) {
+    variable_set('google_appliance_password', trim($form_state['values']['password']));
+  }
+
   // refresh settings getter
   $settings = _google_appliance_get_settings(TRUE);
 
diff --git a/google_appliance.admin.keymatch.inc b/google_appliance.admin.keymatch.inc
new file mode 100644
index 0000000..e77052d
--- /dev/null
+++ b/google_appliance.admin.keymatch.inc
@@ -0,0 +1,531 @@
+<?php
+
+define('GSA_KEYMATCH_MAX', 100000); // Maximum number of keymatchs we may display (arbitrary).
+define('GSA_KEYMATCH_LIMIT', 50); // Number of keymatchs per page.
+
+/**
+ * Form callback - list keymatchs.
+ *
+ * @see google_appliance_menu().
+ */
+function google_appliance_admin_keymatch_view() {
+  $startLine = isset($_GET['page']) ? $_GET['page'] * GSA_KEYMATCH_LIMIT : 0;
+  $query = empty($_SESSION['google_appliance']['keymatch_query']) ? NULL : $_SESSION['google_appliance']['keymatch_query'];
+  $count = _google_appliance_admin_keymatchs_count($query);
+  pager_default_initialize($count, GSA_KEYMATCH_LIMIT, 0);
+
+  $form['filter'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Search for keymatches containing'),
+    '#attributes' => array('class' => array('container-inline')),
+  );
+  $form['filter']['query'] = array(
+    '#type' => 'textfield',
+    '#default_value' => $query,
+  );
+  $form['filter']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Filter'),
+  );
+
+  $form['pager_top'] = array('#markup' => theme('pager'));
+
+  $headers = array(
+    'terms' => array('data' => t('Search Terms')),
+    'occurrence' => array('data' => t('Terms Occur As')),
+    'url' => array('data' => t('URL for Match')),
+    'title' => array('data' => t('Title for Match')),
+  );
+  $options = _google_appliance_admin_keymatchs_retrieve($startLine, GSA_KEYMATCH_LIMIT, $query);
+
+  // Sanitize data.
+  foreach ($options as $key => $option) {
+    $options[$key] = array_map('check_plain', $option);
+  }
+
+  $table = array(
+    'header' => $headers,
+    'rows' => $options,
+    'empty' => t('There is no Keymatch.'),
+  );
+
+  $form['keymatch'] = array('#markup' => theme('table', $table));
+
+  $form['pager_bottom'] = array('#markup' => theme('pager'));
+
+  return $form;
+}
+
+/**
+ * Form submit handler - view keymatchs.
+ *
+ * Store search string in session.
+ *
+ * @see google_appliance_admin_keymatch_view().
+ */
+function google_appliance_admin_keymatch_view_submit($form, &$form_state) {
+  $_SESSION['google_appliance']['keymatch_query'] = empty($form_state['values']['query']) ? NULL : $form_state['values']['query'];
+}
+
+/**
+ * Form callback - edit keymatchs.
+ *
+ * @see google_appliance_menu().
+ */
+function google_appliance_admin_keymatch_edit() {
+  $startLine = isset($_GET['page']) ? $_GET['page'] * GSA_KEYMATCH_LIMIT : 0;
+  $count = _google_appliance_admin_keymatchs_count();
+  pager_default_initialize($count, GSA_KEYMATCH_LIMIT, 0);
+
+  $form['pager_top'] = array('#markup' => theme('pager'));
+
+  $form['keymatch'] = array(
+    '#tree' => TRUE,
+    '#theme' => 'google_appliance_form_table',
+    '#header' => array(
+      'delete' => t('Delete'),
+      'terms' => t('Search terms'),
+      'occurrence' => t('Terms occur as'),
+      'url' => t('URL for match'),
+      'title' => t('Title for match'),
+    ),
+    '#empty' => t('There is no Keymatch.'),
+  );
+
+  $options = _google_appliance_admin_keymatchs_retrieve($startLine, GSA_KEYMATCH_LIMIT, $query);
+  foreach ($options as $key => $option) {
+    $form['keymatch'][$key] = array(
+      'delete' => array(
+        '#type' => 'checkbox',
+        '#default_value' => FALSE,
+      ),
+      'terms' => array(
+        '#type' => 'textfield',
+        '#default_value' => $option['terms'],
+        '#size' => 20,
+      ),
+      'occurrence' => array(
+        '#type' => 'select',
+        '#default_value' => $option['occurrence'],
+        '#options' => drupal_map_assoc(array('KeywordMatch', 'PhraseMatch', 'ExactMatch')),
+      ),
+      'url' => array(
+        '#type' => 'textfield',
+        '#default_value' => $option['url'],
+        '#size' => 30,
+      ),
+      'title' => array(
+        '#type' => 'textfield',
+        '#default_value' => $option['title'],
+        '#size' => 40,
+      ),
+    );
+  }
+
+  $form['start_line'] = array(
+    '#type' => 'hidden',
+    '#value' => $startLine,
+  );
+
+  $form['pager_bottom'] = array('#markup' => theme('pager'));
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+/**
+ * Form validation handler - edit keymatchs.
+ *
+ *
+ * @see google_appliance_admin_keymatch_edit().
+ */
+function google_appliance_admin_keymatch_edit_validate($form, $form_state) {
+  foreach ($form_state['values']['keymatch'] as $name => $keymatch) {
+    // If keymatch set to be deleted then skip validation.
+    if ($keymatch['delete']) {
+      continue;
+    }
+    // Otherwise, validate data.
+    google_appliance_admin_keymatch_validate($name, $keymatch);
+
+  }
+}
+
+/**
+ * Form submit handler - edit keymatchs.
+ *
+ * @see google_appliance_admin_keymatch_edit().
+ */
+function google_appliance_admin_keymatch_edit_submit($form, &$form_state) {
+  $startLine = $form_state['values']['start_line'];
+  $oldKeymatchs = _google_appliance_admin_keymatchs_retrieve($startLine, GSA_KEYMATCH_LIMIT);
+  $newKeymatchs = $form_state['values']['keymatch'];
+
+  foreach ($newKeymatchs as $key => $newKeymatch) {
+    if ($newKeymatch['delete']) {
+      $newKeymatchs[$key] = array('terms' => '', 'occurrence' => '', 'url' => '', 'title' => '');
+    }
+  }
+
+  _google_appliance_admin_keymatchs_update($startLine, $oldKeymatchs, $newKeymatchs);
+}
+
+/**
+ * Form callback - add keymatch.
+ *
+ * @see google_appliance_menu().
+ */
+function google_appliance_admin_keymatch_add() {
+  $form['keymatch'] = array(
+    '#tree' => TRUE,
+    '#theme' => 'google_appliance_form_table',
+    '#header' => array(
+      'terms' => t('Search terms'),
+      'occurrence' => t('Terms occur as'),
+      'url' => t('URL for match'),
+      'title' => t('Title for match'),
+    ),
+  );
+  for ($i = 0; $i < 10; $i++) {
+    $form['keymatch'][] = array(
+      'terms' => array(
+        '#type' => 'textfield',
+        '#size' => 20,
+      ),
+      'occurrence' => array(
+        '#type' => 'select',
+        '#options' => drupal_map_assoc(array('KeywordMatch', 'PhraseMatch', 'ExactMatch')),
+      ),
+      'url' => array(
+        '#type' => 'textfield',
+        '#size' => 30,
+      ),
+      'title' => array(
+        '#type' => 'textfield',
+        '#size' => 40,
+      ),
+    );
+  }
+
+  $form['save'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+/**
+ * Form validation handler - add keymatchs.
+ *
+ * @see google_appliance_admin_keymatch_add().
+ */
+function google_appliance_admin_keymatch_add_validate($form, &$form_state) {
+  foreach ($form_state['values']['keymatch'] as $key => $keymatch) {
+    if (empty($keymatch['terms']) && empty($keymatch['url']) && empty($keymatch['title'])) {
+      // User left all fields empty: we won't insert it in the GSA.
+      unset($form_state['values']['keymatch'][$key]);
+    }
+    else {
+      // User filled in at least 1 field: validate data.
+      google_appliance_admin_keymatch_validate($key, $keymatch);
+    }
+  }
+}
+
+/**
+ * Form submit handler - add keymatch to the GSA.
+ *
+ * @see google_appliance_admin_keymatch_add().
+ */
+function google_appliance_admin_keymatch_add_submit($form, &$form_state) {
+  $form_state['redirect'] = 'admin/config/search/google_appliance/keymatch';
+  _google_appliance_admin_keymatchs_append($form_state['values']['keymatch']);
+}
+
+/**
+ * Validates a single keymatch.
+ *
+ * The GSA would not return the reason why a keymatch does not validate.
+ * We replicate the validation rules of the GSA here.
+ *
+ * @param $element
+ *   Form element.
+ * @param $keymatch
+ *   Array with keys 'terms', 'occurrence', 'url' and 'title'.
+ *
+ * @see google_appliance_admin_keymatch_add().
+ * @see google_appliance_admin_keymatch_edit().
+ */
+function google_appliance_admin_keymatch_validate($element, $keymatch) {
+  // 'Search terms' is mandatory.
+  if (empty($keymatch['terms'])) {
+    form_set_error('keymatch][' . $element . '][terms', t('%field cannot be left empty.', array('%field' => 'Search Terms')));
+  }
+
+  // 'URL for match' is mandatory.
+  if (empty($keymatch['url'])) {
+    form_set_error('keymatch][' . $element . '][url', t('%field cannot be left empty.', array('%field' => 'URL for Match')));
+  }
+
+  // 'URL for match' must be a valid URL.
+  if (!valid_url($keymatch['url'], TRUE)) {
+    form_set_error('keymatch][' . $element . '][url', t('Invalid URL %url.', array('%url' => $keymatch['url'])));
+  }
+
+  // 'URL for match' must contain a path.
+  // e.g. 'http://example.net/abcd' validates, 'http://example.net' does not.
+  $path = parse_url($keymatch['url'], PHP_URL_PATH);
+  if (empty($path)) {
+    form_set_error('keymatch][' . $element . '][url', t('URL %url contains no path.', array('%url' => $keymatch['url'])));
+  }
+}
+
+/**
+ * Send a request to the keymatchs endpoint on the GSA.
+ *
+ * @param $method
+ *   HTTP method: either GET (default) or PUT.
+ * @param $data
+ *   Data to send.
+ * @param $force_new_token
+ *   If TRUE, then get a fresh authentication token from the GSA.
+ *
+ * @return
+ *   XML returned by the GSA.
+ *
+ * @throws Exception
+ *   If the request failed.
+ */
+function _google_appliance_admin_keymatchs($method, $data = NULL, $force_new_token = FALSE) {
+  $token = _google_appliance_admin_token($force_new_token);
+
+  // Build keymatchs endpoint from the settings.
+  $settings = _google_appliance_get_settings();
+  $parts = parse_url($settings['hostname']);
+  $url = $parts['scheme'] . '://' . $parts['host'] . ':8000/feeds/keymatch/' . $settings['frontend'];
+
+  // Build headers specific to the Admin API.
+  $options = array(
+    CURLOPT_HTTPHEADER => array(
+      'Content-Type: application/atom+xml',
+      'Authorization: GoogleLogin auth=' . rawurlencode($token),
+    ),
+  );
+
+  // Trigger request to GSA.
+  switch ($method) {
+    case 'PUT':
+      $result = _curl_put($url, $data, $options);
+      break;
+    case 'GET':
+    default:
+      $result = _curl_get($url, $data, $options);
+  }
+
+  // Check if Curl returned an error.
+  if ($result['is_error']) {
+    watchdog('google_appliance', $result['response'], array(), WATCHDOG_ERROR);
+    throw new Exception(t($result['response']));
+  }
+
+  // Check if GSA returned an internal error.
+  if ($result['response'] == 'Internal Error') {
+    static $new_token = FALSE;
+    if (!$new_token) {
+      // Token may have expired: get a new one.
+      $new_token = TRUE;
+      return _google_appliance_admin_keymatchs($method, $data, TRUE);
+    }
+    else {
+      // Error persists with a fresh token: throw exception.
+      watchdog('google_appliance', 'GSA returned internal error. URL: @url. Data: @data.', array('@url' => $url, '@data' => print_r($data, TRUE)), WATCHDOG_ERROR);
+      throw new Exception(t('Google Search Appliance returned: Internal Error.'));
+    }
+  }
+
+  // Check if GSA returned valid XML.
+  $xml = simplexml_load_string($result['response']);
+  if (!$xml) {
+    watchdog('google_appliance', 'Could not parse reponse from GSA. URL @url. Data: @data. Response: @response.', array('@url' => $url, '@data' => print_r($data, TRUE), '@response' => $result['response']), WATCHDOG_ERROR);
+    throw new Exception(t('Could not parse response from Google Search Appliance.'));
+  }
+
+  // Check if XML returned by the GSA contains an error message.
+  // e.g. someone modified keymatchs on the GSA while the user was editing.
+  $xml->registerXPathNamespace('g', 'http://schemas.google.com/g/2005');
+  $errors = $xml->xpath('//g:error/g:internalReason');
+  if (!empty($errors)) {
+    // The GSA might return the same error message in multiple XML elements.
+    // Make sure an error message shows up only once.
+    $errors = array_unique($errors);
+    throw new Exception(t('Google Search Appliance returned: @errors', array('@errors' => implode(", ", $errors))));
+  }
+
+  return $xml;
+}
+
+/**
+ * Get list of keymatchs.
+ *
+ * @param $startLine
+ *   Start line of the configuration table.
+ * @param $maxLines
+ *   Maximum number of lines to return.
+ * @param $query
+ *   Query string to perform a full-text search. Defaults to NULL (no search).
+ *
+ * @return
+ *   Array of keymatchs, each keymatch being an array with keys 'terms', 'occurrence', 'url' and 'title'.
+ */
+function _google_appliance_admin_keymatchs_retrieve($startLine, $maxLines, $query = NULL) {
+  // Request XML from GSA.
+  $data = array(
+    'startLine' => $startLine,
+    'maxLines' => $maxLines,
+    'query' => $query,
+  );
+  try {
+    $xml = _google_appliance_admin_keymatchs('GET', $data);
+  }
+  catch (Exception $e) {
+    drupal_set_message($e->getMessage(), 'error');
+    return array();
+  }
+
+  // Parse XML response.
+  // Keymatchs are <gsa:content name="foobar"> elements where foobar is a number.
+  $keymatchs = array();
+  $keys = array('terms', 'occurrence', 'url', 'title');
+  foreach ($xml->xpath('//gsa:content') as $content) {
+    $name = (string) $content['name'];
+    if (is_numeric($name)) {
+      $values = str_getcsv((string) $content);
+      $keymatchs[$name] = array_combine($keys, $values);
+    }
+  }
+
+  // The GSA admin interface sorts keymatchs by name.
+  ksort($keymatchs);
+
+  return $keymatchs;
+}
+
+/**
+ * Count the total number of keymatchs stored on the GSA.
+ *
+ * The GSA API does not expose this value directly.
+ * As a workaround, we request all keymatchs and check how many records are returned.
+ *
+ * @param $query
+ *   Query string to perform a full-text search. Defaults to NULL (no search).
+ *
+ * @return
+ *   The number of keymatchs stored on the GSA, or FALSE if an error occurred.
+ */
+function _google_appliance_admin_keymatchs_count($query = NULL) {
+  $data = array(
+    'startLine' => 0,
+    'maxLines' => GSA_KEYMATCH_MAX,
+    'query' => $query,
+  );
+
+  try {
+    $xml = _google_appliance_admin_keymatchs('GET', $data);
+  }
+  catch (Exception $e) {
+    return FALSE;
+  }
+
+  $numLines =  (string) array_pop($xml->xpath("//gsa:content[@name='numLines']"));
+  return $numLines;
+}
+
+/**
+ * Append new keymatchs in the GSA config.
+ *
+ * @param $keymatchs
+ *   Array of keymatchs, each keymatch being an array with keys 'terms', 'occurrence', 'url' and 'title'.
+ *
+ * @see http://www.google.com/support/enterprise/static/gsa/docs/admin/70/gsa_doc_set/acapi_protocol/acapi_protocol.html#1089555
+ */
+function _google_appliance_admin_keymatchs_append($keymatchs) {
+  $lines = _google_appliance_keymatchs_to_csv($keymatchs);
+
+  $xml = new SimpleXMLElement('<entry></entry>');
+  $xml->addAttribute('xmlns', 'http://www.w3.org/2005/Atom');
+  $xml->addAttribute('xmlns:xmlns:gsa', 'http://schemas.google.com/gsa/2007');
+  $xml->addChild('xmlns:gsa:content', 'append')->addAttribute('name', 'updateMethod');
+  $xml->addChild('xmlns:gsa:content', $lines)->addAttribute('name', 'newLines');
+
+  try {
+    _google_appliance_admin_keymatchs('PUT', $xml->asXML());
+    drupal_set_message(t('Successfully added @keymatches.', array('@keymatches' => format_plural(count($keymatchs), '1 keymatch', '@count keymatches'))));
+  }
+  catch (Exception $e) {
+    drupal_set_message($e->getMessage(), 'error');
+  }
+}
+
+/**
+ * Update/delete keymatchs in the GSA config.
+ *
+ * @param $startLine
+ *   Start line of the configuration table.
+ * @param $oldKeymatchs
+ *   Array of original keymatchs, each keymatch being an array with keys 'terms', 'occurrence', 'url' and 'title'.
+ * @param $newKeymatchs
+ *   Array of new keymatchs.
+ *
+ * @see http://www.google.com/support/enterprise/static/gsa/docs/admin/70/gsa_doc_set/acapi_protocol/acapi_protocol.html#1089555
+ */
+function _google_appliance_admin_keymatchs_update($startLine, $oldKeymatchs, $newKeymatchs) {
+  $oldLines = _google_appliance_keymatchs_to_csv($oldKeymatchs);
+  $newLines = _google_appliance_keymatchs_to_csv($newKeymatchs);
+
+  $xml = new SimpleXMLElement('<entry></entry>');
+  $xml->addAttribute('xmlns', 'http://www.w3.org/2005/Atom');
+  $xml->addAttribute('xmlns:xmlns:gsa', 'http://schemas.google.com/gsa/2007');
+  $xml->addChild('xmlns:gsa:content', 'update')->addAttribute('name', 'updateMethod');
+  $xml->addChild('xmlns:gsa:content', $startLine)->addAttribute('name', 'startLine');
+  $xml->addChild('xmlns:gsa:content', $oldLines)->addAttribute('name', 'originalLines');
+  $xml->addChild('xmlns:gsa:content', $newLines)->addAttribute('name', 'newLines');
+
+  try {
+    _google_appliance_admin_keymatchs('PUT', $xml->asXML());
+    drupal_set_message(t('Successfully updated @keymatches.', array('@keymatches' => format_plural(count($oldKeymatchs), '1 keymatch', '@count keymatches'))));
+  }
+  catch (Exception $e) {
+    drupal_set_message($e->getMessage(), 'error');
+  }
+
+}
+
+/**
+ * Convert an array of keymatchs into a CSV string.
+ *
+ * @param $keymatchs
+ *   Array of keymatchs, each keymatch being an array with keys 'terms', 'occurrence', 'url' and 'title'.
+ * @return
+ *   CSV string.
+ */
+function _google_appliance_keymatchs_to_csv($keymatchs = array()) {
+  $keys = drupal_map_assoc(array('terms', 'occurrence', 'url', 'title'));
+  $lines = array();
+
+  foreach ($keymatchs as $keymatch) {
+    $keymatch = array_intersect_key($keymatch, $keys);
+    // Wrap values with quotes so users can insert commas and we follow the csv spec.
+    // @see http://www.csvreader.com/csv_format.php.
+    foreach ($keymatch as $key => $value) {
+      $keymatch[$key] = '"' . str_replace('"', '""', $value) . '"';
+    }
+    $lines[] = implode($keymatch, ',');
+  }
+
+  return implode($lines, "\n");
+}
diff --git a/google_appliance.admin.onebox.inc b/google_appliance.admin.onebox.inc
new file mode 100644
index 0000000..45098cf
--- /dev/null
+++ b/google_appliance.admin.onebox.inc
@@ -0,0 +1,130 @@
+<?php
+
+/**
+ * Form callback - list keymatchs.
+ *
+ * @see google_appliance_menu().
+ */
+function google_appliance_admin_onebox_view() {
+  $headers = array(
+    'name' => array('data' => t('Name')),
+  );
+  $options = _google_appliance_admin_onebox_retrieve();
+
+  // Sanitize data.
+  foreach ($options as $key => $option) {
+    $options[$key] = array_map('check_plain', $option);
+  }
+
+  $table = array(
+    'header' => $headers,
+    'rows' => $options,
+    'empty' => t('There is no Onebox module.'),
+  );
+
+  $form[] = array('#markup' => theme('table', $table));
+
+  return $form;
+}
+
+/**
+ * Send a request to the onebox endpoint on the GSA.
+ *
+ * @param $force_new_token
+ *   If TRUE, then get a fresh authentication token from the GSA.
+ *
+ * @return
+ *   XML returned by the GSA.
+ *
+ * @throws Exception
+ *   If the request failed.
+ */
+function _google_appliance_admin_onebox($force_new_token = FALSE) {
+  $token = _google_appliance_admin_token($force_new_token);
+
+  // Build onebox endpoint from the settings.
+  $settings = _google_appliance_get_settings();
+  $parts = parse_url($settings['hostname']);
+  $url = $parts['scheme'] . '://' . $parts['host'] . ':8000/feeds/onebox';
+
+  // Build headers specific to the Admin API.
+  $options = array(
+    CURLOPT_HTTPHEADER => array(
+      'Content-Type: application/atom+xml',
+      'Authorization: GoogleLogin auth=' . rawurlencode($token),
+    ),
+  );
+
+  // Trigger request to GSA.
+  $result = _curl_get($url, array(), $options);
+
+  // Check if Curl returned an error.
+  if ($result['is_error']) {
+    watchdog('google_appliance', $result['response'], array(), WATCHDOG_ERROR);
+    throw new Exception(t($result['response']));
+  }
+
+  // Check if GSA returned an internal error.
+  if ($result['response'] == 'Internal Error') {
+    static $new_token = FALSE;
+    if (!$new_token) {
+      // Token may have expired: get a new one.
+      $new_token = TRUE;
+      return _google_appliance_admin_onebox(TRUE);
+    }
+    else {
+      // Error persists with a fresh token: throw exception.
+      watchdog('google_appliance', 'GSA returned internal error. URL: @url. Data: @data.', array('@url' => $url, '@data' => print_r($data, TRUE)), WATCHDOG_ERROR);
+      throw new Exception(t('Google Search Appliance returned: Internal Error.'));
+    }
+  }
+
+  // Check if GSA returned valid XML.
+  $xml = simplexml_load_string($result['response']);
+  if (!$xml) {
+    watchdog('google_appliance', 'Could not parse reponse from GSA. URL @url. Data: @data. Response: @response.', array('@url' => $url, '@data' => print_r($data, TRUE), '@response' => $result['response']), WATCHDOG_ERROR);
+    throw new Exception(t('Could not parse response from Google Search Appliance.'));
+  }
+
+  // Check if XML returned by the GSA contains an error message.
+  // e.g. someone modified keymatchs on the GSA while the user was editing.
+  $xml->registerXPathNamespace('g', 'http://schemas.google.com/g/2005');
+  $errors = $xml->xpath('//g:error/g:internalReason');
+  if (!empty($errors)) {
+    // The GSA might return the same error message in multiple XML elements.
+    // Make sure an error message shows up only once.
+    $errors = array_unique($errors);
+    throw new Exception(t('Google Search Appliance returned: @errors', array('@errors' => implode(", ", $errors))));
+  }
+
+  return $xml;
+}
+
+/**
+ * Get list of onebox modules.
+ *
+ * @return
+ *   Array of onebox modules with key 'name'.
+ */
+function _google_appliance_admin_onebox_retrieve() {
+  // Request XML from GSA.
+  try {
+    $xml = _google_appliance_admin_onebox();
+  }
+  catch (Exception $e) {
+    drupal_set_message($e->getMessage(), 'error');
+    return array();
+  }
+
+  // Parse XML response.
+  // Onebox module names <gsa:content name="entryID">foobar</gsa:content> elements.
+  $modules = array();
+  $keys = array('terms', 'occurrence', 'url', 'title');
+  foreach ($xml->xpath('//gsa:content') as $content) {
+    $modules[] = array(
+      'name' => (string) $content,
+    );
+  }
+
+  return $modules;
+}
diff --git a/google_appliance.admin.synonym.inc b/google_appliance.admin.synonym.inc
new file mode 100644
index 0000000..891ed67
--- /dev/null
+++ b/google_appliance.admin.synonym.inc
@@ -0,0 +1,495 @@
+<?php
+
+define('GSA_SYNONYM_MAX', 100000); // Maximum number of synonyms we may display (arbitrary).
+define('GSA_SYNONYM_LIMIT', 50); // Number of synonyms per page.
+
+/**
+ * Form callback - list synonyms.
+ *
+ * @see google_appliance_menu().
+ */
+function google_appliance_admin_synonym_view() {
+  $startLine = isset($_GET['page']) ? $_GET['page'] * GSA_SYNONYM_LIMIT : 0;
+  $query = empty($_SESSION['google_appliance']['synonym_query']) ? NULL : $_SESSION['google_appliance']['synonym_query'];
+  $count = _google_appliance_admin_synonyms_count($query);
+  pager_default_initialize($count, GSA_SYNONYM_LIMIT, 0);
+
+  $form['filter'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Search for Related Queries containing'),
+    '#attributes' => array('class' => array('container-inline')),
+  );
+  $form['filter']['query'] = array(
+    '#type' => 'textfield',
+    '#default_value' => $query,
+  );
+  $form['filter']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Filter'),
+  );
+
+  $form['pager_top'] = array('#markup' => theme('pager'));
+
+  $headers = array(
+    'search' => array('data' => t('Search Query')),
+    'related' => array('data' => t('Related Query')),
+  );
+  $options = _google_appliance_admin_synonyms_retrieve($startLine, GSA_SYNONYM_LIMIT, $query);
+
+  // Sanitize data.
+  foreach ($options as $key => $option) {
+    $options[$key] = array_map('check_plain', $option);
+  }
+
+  $table = array(
+    'header' => $headers,
+    'rows' => $options,
+    'empty' => t('There is no Related Query.'),
+  );
+
+  $form['synonym'] = array('#markup' => theme('table', $table));
+
+  $form['pager_bottom'] = array('#markup' => theme('pager'));
+
+  return $form;
+}
+
+/**
+ * Form submit handler - view synonyms.
+ *
+ * Store search string in session.
+ *
+ * @see google_appliance_admin_synonym_view().
+ */
+function google_appliance_admin_synonym_view_submit($form, &$form_state) {
+  $_SESSION['google_appliance']['synonym_query'] = empty($form_state['values']['query']) ? NULL : $form_state['values']['query'];
+}
+
+/**
+ * Form callback - edit synonyms.
+ *
+ * @see google_appliance_menu().
+ */
+function google_appliance_admin_synonym_edit() {
+  $startLine = isset($_GET['page']) ? $_GET['page'] * GSA_SYNONYM_LIMIT : 0;
+  $count = _google_appliance_admin_synonyms_count();
+  pager_default_initialize($count, GSA_SYNONYM_LIMIT, 0);
+
+  $form['pager_top'] = array('#markup' => theme('pager'));
+
+  $form['synonym'] = array(
+    '#tree' => TRUE,
+    '#theme' => 'google_appliance_form_table',
+    '#header' => array(
+      'delete' => t('Delete'),
+      'search' => t('Search Query'),
+      'related' => t('Related Query'),
+    ),
+    '#empty' => t('There is no Related Query.'),
+  );
+
+  $options = _google_appliance_admin_synonyms_retrieve($startLine, GSA_SYNONYM_LIMIT, $query);
+  foreach ($options as $key => $option) {
+    $form['synonym'][$key] = array(
+      'delete' => array(
+        '#type' => 'checkbox',
+        '#default_value' => FALSE,
+      ),
+      'search' => array(
+        '#type' => 'textfield',
+        '#default_value' => $option['search'],
+        '#size' => 20,
+      ),
+      'related' => array(
+        '#type' => 'textfield',
+        '#default_value' => $option['related'],
+        '#size' => 20,
+      ),
+    );
+  }
+
+  $form['start_line'] = array(
+    '#type' => 'hidden',
+    '#value' => $startLine,
+  );
+
+  $form['pager_bottom'] = array('#markup' => theme('pager'));
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+/**
+ * Form validation handler - edit synonyms.
+ *
+ *
+ * @see google_appliance_admin_synonym_edit().
+ */
+function google_appliance_admin_synonym_edit_validate($form, $form_state) {
+  foreach ($form_state['values']['synonym'] as $name => $synonym) {
+    // If synonym set to be deleted then skip validation.
+    if ($synonym['delete']) {
+      continue;
+    }
+    // Otherwise, validate data.
+    google_appliance_admin_synonym_validate($name, $synonym);
+
+  }
+}
+
+/**
+ * Form submit handler - edit synonyms.
+ *
+ * @see google_appliance_admin_synonym_edit().
+ */
+function google_appliance_admin_synonym_edit_submit($form, &$form_state) {
+  $startLine = $form_state['values']['start_line'];
+  $oldSynonyms = _google_appliance_admin_synonyms_retrieve($startLine, GSA_SYNONYM_LIMIT);
+  $newSynonyms = $form_state['values']['synonym'];
+
+  foreach ($newSynonyms as $key => $newSynonym) {
+    if ($newSynonym['delete']) {
+      $newSynonyms[$key] = array('search' => '', 'related' => '');
+    }
+  }
+
+  _google_appliance_admin_synonyms_update($startLine, $oldSynonyms, $newSynonyms);
+}
+
+/**
+ * Form callback - add synonym.
+ *
+ * @see google_appliance_menu().
+ */
+function google_appliance_admin_synonym_add() {
+  $form['synonym'] = array(
+    '#tree' => TRUE,
+    '#theme' => 'google_appliance_form_table',
+    '#header' => array(
+      'search' => t('Search Query'),
+      'related' => t('Related Query'),
+    ),
+  );
+  for ($i = 0; $i < 10; $i++) {
+    $form['synonym'][] = array(
+      'search' => array(
+        '#type' => 'textfield',
+        '#size' => 20,
+      ),
+      'related' => array(
+        '#type' => 'textfield',
+        '#size' => 20,
+      ),
+    );
+  }
+
+  $form['save'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+/**
+ * Form validation handler - add synonyms.
+ *
+ * @see google_appliance_admin_synonym_add().
+ */
+function google_appliance_admin_synonym_add_validate($form, &$form_state) {
+  foreach ($form_state['values']['synonym'] as $key => $synonym) {
+    if (empty($synonym['search']) && empty($synonym['related'])) {
+      // User left all fields empty: we won't insert it in the GSA.
+      unset($form_state['values']['synonym'][$key]);
+    }
+    else {
+      // User filled in at least 1 field: validate data.
+      google_appliance_admin_synonym_validate($key, $synonym);
+    }
+  }
+}
+
+/**
+ * Form submit handler - add synonym to the GSA.
+ *
+ * @see google_appliance_admin_synonym_add().
+ */
+function google_appliance_admin_synonym_add_submit($form, &$form_state) {
+  $form_state['redirect'] = 'admin/config/search/google_appliance/synonym';
+  _google_appliance_admin_synonyms_append($form_state['values']['synonym']);
+}
+
+/**
+ * Validates a single synonym.
+ *
+ * The GSA would not return the reason why a synonym does not validate.
+ * We replicate the validation rules of the GSA here.
+ *
+ * @param $element
+ *   Form element.
+ * @param $synonym
+ *   Array with keys 'search' and 'related'.
+ *
+ * @see google_appliance_admin_synonym_add().
+ * @see google_appliance_admin_synonym_edit().
+ */
+function google_appliance_admin_synonym_validate($element, $synonym) {
+  // 'Search Query' is mandatory.
+  if (empty($synonym['search'])) {
+    form_set_error('synonym][' . $element . '][search', t('%field cannot be left empty.', array('%field' => 'Search Query')));
+  }
+
+  // 'Related Query' is mandatory.
+  if (empty($synonym['related'])) {
+    form_set_error('synonym][' . $element . '][related', t('%field cannot be left empty.', array('%field' => 'Related Query')));
+  }
+}
+
+/**
+ * Send a request to the synonyms endpoint on the GSA.
+ *
+ * @param $method
+ *   HTTP method: either GET (default) or PUT.
+ * @param $data
+ *   Data to send.
+ * @param $force_new_token
+ *   If TRUE, then get a fresh authentication token from the GSA.
+ *
+ * @return
+ *   XML returned by the GSA.
+ *
+ * @throws Exception
+ *   If the request failed.
+ */
+function _google_appliance_admin_synonyms($method, $data = NULL, $force_new_token = FALSE) {
+  $token = _google_appliance_admin_token($force_new_token);
+
+  // Build synonyms endpoint from the settings.
+  $settings = _google_appliance_get_settings();
+  $parts = parse_url($settings['hostname']);
+  $url = $parts['scheme'] . '://' . $parts['host'] . ':8000/feeds/synonym/' . $settings['frontend'];
+
+  // Build headers specific to the Admin API.
+  $options = array(
+    CURLOPT_HTTPHEADER => array(
+      'Content-Type: application/atom+xml',
+      'Authorization: GoogleLogin auth=' . rawurlencode($token),
+    ),
+  );
+
+  // Trigger request to GSA.
+  switch ($method) {
+    case 'PUT':
+      $result = _curl_put($url, $data, $options);
+      break;
+    case 'GET':
+    default:
+      $result = _curl_get($url, $data, $options);
+  }
+
+  // Check if Curl returned an error.
+  if ($result['is_error']) {
+    watchdog('google_appliance', $result['response'], array(), WATCHDOG_ERROR);
+    throw new Exception(t($result['response']));
+  }
+
+  // Check if GSA returned an internal error.
+  if ($result['response'] == 'Internal Error') {
+    static $new_token = FALSE;
+    if (!$new_token) {
+      // Token may have expired: get a new one.
+      $new_token = TRUE;
+      return _google_appliance_admin_synonyms($method, $data, TRUE);
+    }
+    else {
+      // Error persists with a fresh token: throw exception.
+      watchdog('google_appliance', 'GSA returned internal error. URL: @url. Data: @data.', array('@url' => $url, '@data' => print_r($data, TRUE)), WATCHDOG_ERROR);
+      throw new Exception(t('Google Search Appliance returned: Internal Error.'));
+    }
+  }
+
+  // Check if GSA returned valid XML.
+  $xml = simplexml_load_string($result['response']);
+  if (!$xml) {
+    watchdog('google_appliance', 'Could not parse reponse from GSA. URL @url. Data: @data. Response: @response.', array('@url' => $url, '@data' => print_r($data, TRUE), '@response' => $result['response']), WATCHDOG_ERROR);
+    throw new Exception(t('Could not parse response from Google Search Appliance.'));
+  }
+
+  // Check if XML returned by the GSA contains an error message.
+  // e.g. someone modified synonyms on the GSA while the user was editing.
+  $xml->registerXPathNamespace('g', 'http://schemas.google.com/g/2005');
+  $errors = $xml->xpath('//g:error/g:internalReason');
+  if (!empty($errors)) {
+    // The GSA might return the same error message in multiple XML elements.
+    // Make sure an error message shows up only once.
+    $errors = array_unique($errors);
+    throw new Exception(t('Google Search Appliance returned: @errors', array('@errors' => implode(", ", $errors))));
+  }
+
+  return $xml;
+}
+
+/**
+ * Get list of synonyms.
+ *
+ * @param $startLine
+ *   Start line of the configuration table.
+ * @param $maxLines
+ *   Maximum number of lines to return.
+ * @param $query
+ *   Query string to perform a full-text search. Defaults to NULL (no search).
+ *
+ * @return
+ *   Array of synonyms, each synonym being an array with keys 'search' and 'related'.
+ */
+function _google_appliance_admin_synonyms_retrieve($startLine, $maxLines, $query = NULL) {
+  // Request XML from GSA.
+  $data = array(
+    'startLine' => $startLine,
+    'maxLines' => $maxLines,
+    'query' => $query,
+  );
+  try {
+    $xml = _google_appliance_admin_synonyms('GET', $data);
+  }
+  catch (Exception $e) {
+    drupal_set_message($e->getMessage(), 'error');
+    return array();
+  }
+
+  // Parse XML response.
+  // Synonyms are <gsa:content name="foobar"> elements where foobar is a number.
+  $synonyms = array();
+  $keys = array('search', 'related');
+  foreach ($xml->xpath('//gsa:content') as $content) {
+    $name = (string) $content['name'];
+    if (is_numeric($name)) {
+      $values = str_getcsv((string) $content);
+      $synonyms[$name] = array_combine($keys, $values);
+    }
+  }
+
+  // The GSA admin interface sorts synonyms by name.
+  ksort($synonyms);
+
+  return $synonyms;
+}
+
+/**
+ * Count the total number of synonyms stored on the GSA.
+ *
+ * The GSA API does not expose this value directly.
+ * As a workaround, we request all synonyms and check how many records are returned.
+ *
+ * @param $query
+ *   Query string to perform a full-text search. Defaults to NULL (no search).
+ *
+ * @return
+ *   The number of synonyms stored on the GSA, or FALSE if an error occurred.
+ */
+function _google_appliance_admin_synonyms_count($query = NULL) {
+  $data = array(
+    'startLine' => 0,
+    'maxLines' => GSA_SYNONYM_MAX,
+    'query' => $query,
+  );
+
+  try {
+    $xml = _google_appliance_admin_synonyms('GET', $data);
+  }
+  catch (Exception $e) {
+    return FALSE;
+  }
+
+  $numLines =  (string) array_pop($xml->xpath("//gsa:content[@name='numLines']"));
+  return $numLines;
+}
+
+/**
+ * Append new synonyms in the GSA config.
+ *
+ * @param $synonyms
+ *   Array of synonyms, each synonym being an array with keys 'search', 'related'.
+ *
+ * @see http://www.google.com/support/enterprise/static/gsa/docs/admin/70/gsa_doc_set/acapi_protocol/acapi_protocol.html#1089555
+ */
+function _google_appliance_admin_synonyms_append($synonyms) {
+  $lines = _google_appliance_synonyms_to_csv($synonyms);
+
+  $xml = new SimpleXMLElement('<entry></entry>');
+  $xml->addAttribute('xmlns', 'http://www.w3.org/2005/Atom');
+  $xml->addAttribute('xmlns:xmlns:gsa', 'http://schemas.google.com/gsa/2007');
+  $xml->addChild('xmlns:gsa:content', 'append')->addAttribute('name', 'updateMethod');
+  $xml->addChild('xmlns:gsa:content', $lines)->addAttribute('name', 'newLines');
+
+  try {
+    _google_appliance_admin_synonyms('PUT', $xml->asXML());
+    drupal_set_message(t('Successfully added @synonyms.', array('@synonyms' => format_plural(count($synonyms), '1 synonym', '@count synonyms'))));
+  }
+  catch (Exception $e) {
+    drupal_set_message($e->getMessage(), 'error');
+  }
+}
+
+/**
+ * Update/delete synonyms in the GSA config.
+ *
+ * @param $startLine
+ *   Start line of the configuration table.
+ * @param $oldSynonyms
+ *   Array of original synonyms, each synonym being an array with keys 'search', 'related'.
+ * @param $newSynonyms
+ *   Array of new synonyms.
+ *
+ * @see http://www.google.com/support/enterprise/static/gsa/docs/admin/70/gsa_doc_set/acapi_protocol/acapi_protocol.html#1089555
+ */
+function _google_appliance_admin_synonyms_update($startLine, $oldSynonyms, $newSynonyms) {
+  $oldLines = _google_appliance_synonyms_to_csv($oldSynonyms);
+  $newLines = _google_appliance_synonyms_to_csv($newSynonyms);
+
+  $xml = new SimpleXMLElement('<entry></entry>');
+  $xml->addAttribute('xmlns', 'http://www.w3.org/2005/Atom');
+  $xml->addAttribute('xmlns:xmlns:gsa', 'http://schemas.google.com/gsa/2007');
+  $xml->addChild('xmlns:gsa:content', 'update')->addAttribute('name', 'updateMethod');
+  $xml->addChild('xmlns:gsa:content', $startLine)->addAttribute('name', 'startLine');
+  $xml->addChild('xmlns:gsa:content', $oldLines)->addAttribute('name', 'originalLines');
+  $xml->addChild('xmlns:gsa:content', $newLines)->addAttribute('name', 'newLines');
+
+  try {
+    _google_appliance_admin_synonyms('PUT', $xml->asXML());
+    drupal_set_message(t('Successfully updated @synonyms.', array('@synonyms' => format_plural(count($oldSynonyms), '1 synonym', '@count synonyms'))));
+  }
+  catch (Exception $e) {
+    drupal_set_message($e->getMessage(), 'error');
+  }
+
+}
+
+/**
+ * Convert an array of synonyms into a CSV string.
+ *
+ * @param $synonyms
+ *   Array of synonyms, each synonym being an array with keys 'search', 'related'.
+ * @return
+ *   CSV string.
+ */
+function _google_appliance_synonyms_to_csv($synonyms = array()) {
+  $keys = drupal_map_assoc(array('search', 'related'));
+  $lines = array();
+
+  foreach ($synonyms as $synonym) {
+    $synonym = array_intersect_key($synonym, $keys);
+    // Wrap values with quotes so users can insert commas and we follow the csv spec.
+    // @see http://www.csvreader.com/csv_format.php.
+    foreach ($synonym as $key => $value) {
+      $synonym[$key] = '"' . str_replace('"', '""', $value) . '"';
+    }
+    $lines[] = implode($synonym, ',');
+  }
+
+  return implode($lines, "\n");
+}
diff --git a/google_appliance.helpers.inc b/google_appliance.helpers.inc
index 2fd2115..383822f 100644
--- a/google_appliance.helpers.inc
+++ b/google_appliance.helpers.inc
@@ -13,6 +13,9 @@ define('SGA_DEFAULT_HOSTNAME', '');
 define('SGA_DEFAULT_COLLECTION', 'default_collection');
 define('SGA_DEFAULT_FRONTEND', 'default_frontend');
 define('SGA_DEFAULT_TIMEOUT', 10);
+define('SGA_DEFAULT_USERNAME', '');
+define('SGA_DEFAULT_PASSWORD', '');
+define('SGA_DEFAULT_VERIFY_SSL', TRUE);
 define('SGA_DEFAULT_AUTOFILTER', 1);
 define('SGA_DEFAULT_LANGUAGE_FILTER_TOGGLE', FALSE);
 define('SGA_DEFAULT_QUERY_INSPECTION', 0);
@@ -44,6 +47,9 @@ function _google_appliance_get_settings($refresh = FALSE) {
     'collection',
     'frontend',
     'timeout',
+    'username',
+    'password',
+    'verify_ssl',
     'autofilter',
     'language_filter_toggle',
     'query_inspection',
@@ -123,6 +129,40 @@ function _curl_post($url, array $post = NULL, array $options = array()) {
 }
 
 /**
+ * Send a PUT request using cURL
+ * @param string $url to request
+ * @param string $data send
+ * @param array $options for cURL
+ * @return string
+ */
+function _curl_put($url, $data = NULL, array $options = array()) {
+  $defaults = array(
+    CURLOPT_CUSTOMREQUEST => 'PUT',
+    CURLOPT_HEADER => 0,
+    CURLOPT_URL => $url,
+    CURLOPT_FRESH_CONNECT => 1,
+    CURLOPT_RETURNTRANSFER => 1,
+    CURLOPT_FORBID_REUSE => 1,
+    CURLOPT_TIMEOUT => 4,
+    CURLOPT_POSTFIELDS => $data,
+  );
+
+  $ch = curl_init();
+  curl_setopt_array($ch, ($options + $defaults));
+  $result = array(
+    'is_error' => FALSE,
+    'response' => curl_exec($ch),
+  );
+
+  if ($result['response'] === FALSE) {
+    $result['is_error'] = TRUE;
+    $result['response'] = curl_error($ch);
+  }
+  curl_close($ch);
+  return $result;
+}
+
+/**
  * Send a GET requst using cURL
  * @param string $url to request
  * @param array $get values to send
@@ -221,3 +261,67 @@ function _google_appliance_get_lr($options) {
   }
   return implode('|', $langcodes);
 }
+
+/**
+ * Get a valid authentication token - either a new one from the GSA or an old one stored in the session.
+ *
+ * @param $force_new
+ *   If TRUE, then a fresh token will be retrieved from the GSA.
+ *
+ * @return
+ *   Authentication token.
+ *
+ * @throws Exception
+ *   If a token could not be retrieved.
+ *
+ * @see http://www.google.com/support/enterprise/static/gsa/docs/admin/70/gsa_doc_set/acapi_protocol/acapi_protocol.html#1078166
+ */
+function _google_appliance_admin_token($force_new = FALSE) {
+  if (!isset($_SESSION['google_appliance']['token']) || $force_new) {
+
+    // Build URL for authentication.
+    // Speaking to port 8443 of the GSA requires to use the https scheme.
+    $settings = _google_appliance_get_settings();
+    $host = parse_url($settings['hostname'], PHP_URL_HOST);
+    $url = 'https://' . $host . ':8443/accounts/ClientLogin';
+
+    $params = array(
+      'Email' => rawurlencode($settings['username']),
+      'Passwd' => rawurlencode($settings['password']),
+    );
+    $options = array(
+      CURLOPT_HTTPHEADER => array('Content-Type: application/x-www-form-urlencoded'),
+    );
+    if (!$settings['verify_ssl']) {
+      $options += array(
+        CURLOPT_SSL_VERIFYHOST => 0,
+        CURLOPT_SSL_VERIFYPEER => 0,
+      );
+    }
+    $response = _curl_post($url, $params, $options);
+
+    if ($response['is_error']) {
+      watchdog('google_appliance', $response['response'], array(), WATCHDOG_ERROR);
+      throw new Exception(t($response['response']));
+    }
+
+    // Extract token from response.
+    $token = '';
+    $lines = preg_split("/\r\n|\n|\r/", $response['response']);
+    foreach ($lines as $line) {
+      list($name, $token) = explode('=', $line, 2);
+      if ($name == 'Auth') {
+        break;
+      }
+    }
+
+    if (empty($token)) {
+      watchdog('google_appliance', 'Could not get authentication token. GSA replied: @response.', array('@response' => $response['response']), WATCHDOG_ERROR);
+      throw new Exception(t('Could not authenticate to Google Search Appliance. Did you set a valid username/password in the <a href="@url">settings</a> ?', array('@url' => url('admin/config/search/google_appliance'))));
+    }
+
+    $_SESSION['google_appliance']['token'] = $token;
+  }
+
+  return $_SESSION['google_appliance']['token'];
+}
diff --git a/google_appliance.module b/google_appliance.module
index 5eddb6b..2d1f39f 100644
--- a/google_appliance.module
+++ b/google_appliance.module
@@ -8,6 +8,11 @@
 // helpers
 require_once(drupal_get_path('module', 'google_appliance') . '/google_appliance.helpers.inc');
 
+// Admin API.
+require_once(drupal_get_path('module', 'google_appliance') . '/google_appliance.admin.keymatch.inc');
+require_once(drupal_get_path('module', 'google_appliance') . '/google_appliance.admin.onebox.inc');
+require_once(drupal_get_path('module', 'google_appliance') . '/google_appliance.admin.synonym.inc');
+
 // default theme impementations
 require_once(drupal_get_path('module', 'google_appliance') . '/theme/google_appliance.theme.inc');
 
@@ -79,6 +84,15 @@ function google_appliance_permission() {
   $perms['administer_google_appliance'] = array(
     'title' => t('Administer Google Appliance settings'),
   );
+  $perms['administer_google_appliance_keymatchs'] = array(
+    'title' => t('Administer Keymatchs'),
+  );
+  $perms['administer_google_appliance_synonyms'] = array(
+    'title' => t('Administer Related Queries'),
+  );
+  $perms['administer_google_appliance_onebox'] = array(
+    'title' => t('Administer Onebox modules'),
+  );
   $perms['access_google_appliance_content'] = array(
     'title' => t('Use Google Appliance searches'),
   );
@@ -104,7 +118,7 @@ function google_appliance_menu() {
   );
 
   // admin page
-  $items['admin/config/search/google_appliance/settings'] = array(
+  $items['admin/config/search/google_appliance'] = array(
     'title' => 'Google Appliance',
     'description' => 'Configure options for search via Google Search Appliance.',
     'page callback' => 'drupal_get_form',
@@ -112,6 +126,87 @@ function google_appliance_menu() {
     'file' => 'google_appliance.admin.inc',
     'access arguments' => array('administer_google_appliance'),
     'weight' => -30,
+    'type' => MENU_NORMAL_ITEM,
+  );
+  $items['admin/config/search/google_appliance/settings'] = array(
+    'title' => 'Settings',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+  );
+
+  // Admin keymatch.
+  $items['admin/config/search/google_appliance/keymatch'] = array(
+    'title' => 'Keymatchs',
+    'type' => MENU_LOCAL_TASK,
+    'page arguments' => array('google_appliance_admin_keymatch_view'),
+    'access arguments' => array('administer_google_appliance_keymatchs'),
+  );
+  $items['admin/config/search/google_appliance/keymatch/view'] = array(
+    'title' => 'View',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+  );
+  $items['admin/config/search/google_appliance/keymatch/edit'] = array(
+    'title' => 'Edit',
+    'type' => MENU_LOCAL_TASK,
+    'page arguments' => array('google_appliance_admin_keymatch_edit'),
+    'access arguments' => array('administer_google_appliance_keymatchs'),
+  );
+  $items['admin/config/search/google_appliance/keymatch/view/add'] = array(
+    'title' => 'Add Keymatch',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('google_appliance_admin_keymatch_add'),
+    'file' => 'google_appliance.admin.keymatch.inc',
+    'access arguments' => array('administer_google_appliance_keymatchs'),
+    'type' => MENU_LOCAL_ACTION,
+  );
+  $items['admin/config/search/google_appliance/keymatch/edit/add'] = array(
+    'title' => 'Add Keymatch',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('google_appliance_admin_keymatch_add'),
+    'file' => 'google_appliance.admin.keymatch.inc',
+    'access arguments' => array('administer_google_appliance_keymatchs'),
+    'type' => MENU_LOCAL_ACTION,
+  );
+
+  // Admin related queries (a.k.a. synonyms).
+  $items['admin/config/search/google_appliance/synonym'] = array(
+    'title' => 'Related queries',
+    'type' => MENU_LOCAL_TASK,
+    'page arguments' => array('google_appliance_admin_synonym_view'),
+    'access arguments' => array('administer_google_appliance_synonyms'),
+  );
+  $items['admin/config/search/google_appliance/synonym/view'] = array(
+    'title' => 'View',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+  );
+  $items['admin/config/search/google_appliance/synonym/edit'] = array(
+    'title' => 'Edit',
+    'type' => MENU_LOCAL_TASK,
+    'page arguments' => array('google_appliance_admin_synonym_edit'),
+    'access arguments' => array('administer_google_appliance_synonyms'),
+  );
+  $items['admin/config/search/google_appliance/synonym/view/add'] = array(
+    'title' => 'Add Related Query',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('google_appliance_admin_synonym_add'),
+    'file' => 'google_appliance.admin.synonym.inc',
+    'access arguments' => array('administer_google_appliance_synonyms'),
+    'type' => MENU_LOCAL_ACTION,
+  );
+  $items['admin/config/search/google_appliance/synonym/edit/add'] = array(
+    'title' => 'Add Related Query',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('google_appliance_admin_synonym_add'),
+    'file' => 'google_appliance.admin.synonym.inc',
+    'access arguments' => array('administer_google_appliance_synonyms'),
+    'type' => MENU_LOCAL_ACTION,
+  );
+
+  // Admin Onebox modules.
+  $items['admin/config/search/google_appliance/onebox'] = array(
+    'title' => 'Onebox modules',
+    'type' => MENU_LOCAL_TASK,
+    'page arguments' => array('google_appliance_admin_onebox_view'),
+    'access arguments' => array('administer_google_appliance_onebox'),
   );
 
   return $items;
diff --git a/theme/google_appliance.theme.inc b/theme/google_appliance.theme.inc
index c4853a5..acb95b0 100644
--- a/theme/google_appliance.theme.inc
+++ b/theme/google_appliance.theme.inc
@@ -95,6 +95,11 @@ function google_appliance_theme() {
     'path' => $ga_template_dir,
   );
 
+  // Table for forms.
+  $registry['google_appliance_form_table'] = array(
+    'render element' => 'element',
+  );
+
   return $registry;
 }
 
@@ -491,6 +496,26 @@ function theme_google_appliance_pager(&$vars) {
 }
 
 /**
+ * Theme a form in a table.
+ */
+function theme_google_appliance_form_table($variables) {
+  $element = $variables['element'];
+  $element += array('#empty' => '');
+  $header = $element['#header'];
+
+  $rows = array();
+  foreach (element_children($element) as $key) {
+    $row = array();
+    foreach ($header as $column => $label) {
+      $row[$column] = render($element[$key][$column]);
+    }
+    $rows[] = $row;
+  }
+
+  return theme('table', array('header' => $header, 'rows' => $rows, 'empty' => $element['#empty']));
+}
+
+/**
  * Preprocess google-appliance-sort-headers.tpl.php.
  */
 function template_preprocess_google_appliance_sort_headers(&$vars) {
