diff --git a/data/database.py b/data/database.py
index 3e98b83be..a659e7d50 100644
--- a/data/database.py
+++ b/data/database.py
@@ -113,6 +113,7 @@ class TeamMemberInvite(BaseModel):
   user = ForeignKeyField(User, index=True, null=True)
   email = CharField(null=True)
   team = ForeignKeyField(Team, index=True)
+  inviter = ForeignKeyField(User, related_name='inviter')
   invite_token = CharField(default=uuid_generator)
 
 
diff --git a/data/model/legacy.py b/data/model/legacy.py
index 64bf8d4f8..52248958c 100644
--- a/data/model/legacy.py
+++ b/data/model/legacy.py
@@ -71,6 +71,10 @@ class TooManyUsersException(DataModelException):
   pass
 
 
+class UserAlreadyInTeam(DataModelException):
+  pass
+
+
 def is_create_user_allowed():
   return get_active_user_count() < config.app_config['LICENSE_USER_LIMIT']
 
@@ -294,7 +298,7 @@ def remove_team(org_name, team_name, removed_by_username):
   team.delete_instance(recursive=True, delete_nullable=True)
 
 
-def add_or_invite_to_team(team, user=None, email=None, adder=None):
+def add_or_invite_to_team(inviter, team, user=None, email=None):
   # If the user is a member of the organization, then we simply add the
   # user directly to the team. Otherwise, an invite is created for the user/email.
   # We return None if the user was directly added and the invite object if the user was invited.
@@ -326,15 +330,16 @@ def add_or_invite_to_team(team, user=None, email=None, adder=None):
     add_user_to_team(user, team)
     return None
 
-  return TeamMemberInvite.create(user=user, email=email if not user else None, team=team)
+  return TeamMemberInvite.create(user=user, email=email if not user else None, team=team,
+                                 inviter=inviter)
 
 
 def add_user_to_team(user, team):
   try:
     return TeamMember.create(user=user, team=team)
   except Exception:
-    raise DataModelException('User \'%s\' is already a member of team \'%s\'' %
-                             (user.username, team.name))
+    raise UserAlreadyInTeam('User \'%s\' is already a member of team \'%s\'' %
+                            (user.username, team.name))
 
 
 def remove_user_from_team(org_name, team_name, username, removed_by_username):
@@ -1766,6 +1771,32 @@ def delete_notifications_by_kind(target, kind_name):
   Notification.delete().where(Notification.target == target,
                               Notification.kind == kind_ref).execute()
 
+def delete_matching_notifications(target, kind_name, **kwargs):
+  kind_ref = NotificationKind.get(name=kind_name)
+
+  # Load all notifications for the user with the given kind.
+  notifications = list(Notification.select().where(
+      Notification.target == target,
+      Notification.kind == kind_ref))
+
+  # For each, match the metadata to the specified values.
+  for notification in notifications:
+    matches = True
+    try:
+      metadata = json.loads(notification.metadata_json)
+    except:
+      continue
+
+    for (key, value) in kwargs.iteritems():
+      if not key in metadata or metadata[key] != value:
+        matches = False
+        break
+
+    if not matches:
+      continue
+
+    notification.delete_instance()
+
 
 def get_active_users():
   return User.select().where(User.organization == False, User.robot == False)
@@ -1821,3 +1852,51 @@ def confirm_email_authorization_for_repo(code):
   found.save()
 
   return found
+
+
+def lookup_team_invites(user):
+  return TeamMemberInvite.select().where(TeamMemberInvite.user == user)
+
+def lookup_team_invite(code, user):
+ # Lookup the invite code.
+  try:
+    found = TeamMemberInvite.get(TeamMemberInvite.invite_token == code)
+  except TeamMemberInvite.DoesNotExist:
+    raise DataModelException('Invalid confirmation code.')
+
+  # Verify the code applies to the current user.
+  if found.user:
+    if found.user != user:
+      raise DataModelException('Invalid confirmation code.')
+  else:
+    if found.email != user.email:
+      raise DataModelException('Invalid confirmation code.')
+    
+  return found
+
+
+def delete_team_invite(code, user):
+  found = lookup_team_invite(code, user)
+
+  team = found.team
+  inviter = found.inviter
+
+  found.delete_instance()
+
+  return (team, inviter)
+
+def confirm_team_invite(code, user):
+  found = lookup_team_invite(code, user)
+
+  # Add the user to the team.
+  try:
+    add_user_to_team(user, found.team)
+  except UserAlreadyInTeam:
+    # Ignore.
+    pass
+  
+  # Delete the invite and return the team.
+  team = found.team
+  inviter = found.inviter
+  found.delete_instance()
+  return (team, inviter)
diff --git a/endpoints/api/team.py b/endpoints/api/team.py
index 3c0751a56..37efd44f2 100644
--- a/endpoints/api/team.py
+++ b/endpoints/api/team.py
@@ -2,7 +2,7 @@ from flask import request
 
 from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
                            log_action, Unauthorized, NotFound, internal_only, require_scope,
-                           query_param, truthy_bool, parse_args)
+                           query_param, truthy_bool, parse_args, require_user_admin)
 from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission
 from auth.auth_context import get_authenticated_user
 from auth import scopes
@@ -10,8 +10,8 @@ from data import model
 from util.useremails import send_org_invite_email
 from util.gravatar import compute_hash
 
-def add_or_invite_to_team(team, user=None, email=None, adder=None):
-  invite = model.add_or_invite_to_team(team, user, email, adder)
+def add_or_invite_to_team(inviter, team, user=None, email=None):
+  invite = model.add_or_invite_to_team(inviter, team, user, email)
   if not invite:
     # User was added to the team directly.
     return
@@ -20,13 +20,13 @@ def add_or_invite_to_team(team, user=None, email=None, adder=None):
   if user:
     model.create_notification('org_team_invite', user, metadata = {
       'code': invite.invite_token,
-      'adder': adder,
+      'inviter': inviter.username,
       'org': orgname,
       'team': team.name
     })
 
   send_org_invite_email(user.username if user else email, user.email if user else email,
-                        orgname, team.name, adder, invite.invite_token)
+                        orgname, team.name, inviter.username, invite.invite_token)
   return invite
 
 def team_view(orgname, team):
@@ -204,11 +204,8 @@ class TeamMember(ApiResource):
         raise request_error(message='Unknown user')
         
       # Add or invite the user to the team.
-      adder = None
-      if get_authenticated_user():
-        adder = get_authenticated_user().username
-
-      invite = add_or_invite_to_team(team, user=user, adder=adder)
+      inviter = get_authenticated_user()
+      invite = add_or_invite_to_team(inviter, team, user=user)
       if not invite:
         log_action('org_add_team_member', orgname, {'member': membername, 'team': teamname})
         return member_view(user, invited=False)
@@ -232,3 +229,52 @@ class TeamMember(ApiResource):
       return 'Deleted', 204
 
     raise Unauthorized()
+
+
+@resource('/v1/teaminvite/<code>')
+@internal_only
+class TeamMemberInvite(ApiResource):
+  """ Resource for managing invites to jon a team. """
+  @require_user_admin
+  @nickname('acceptOrganizationTeamInvite')
+  def put(self, code):
+    """ Accepts an invite to join a team in an organization. """
+    # Accept the invite for the current user.
+    try:
+      (team, inviter) = model.confirm_team_invite(code, get_authenticated_user())
+    except model.DataModelException:
+      raise NotFound()
+
+    model.delete_matching_notifications(get_authenticated_user(), 'org_team_invite', code=code)
+
+    orgname = team.organization.username
+    log_action('org_team_member_invite_accepted', orgname, {
+        'member': get_authenticated_user().username,
+        'team': team.name,
+        'inviter': inviter.username
+    })
+
+    return {
+      'org': orgname,
+      'team': team.name
+    }
+
+  @nickname('declineOrganizationTeamInvite')
+  @require_user_admin
+  def delete(self, code):
+    """ Delete an existing member of a team. """
+    try:
+      (team, inviter) = model.delete_team_invite(code, get_authenticated_user())
+    except model.DataModelException:
+      raise NotFound()
+
+    model.delete_matching_notifications(get_authenticated_user(), 'org_team_invite', code=code)
+
+    orgname = team.organization.username
+    log_action('org_team_member_invite_declined', orgname, {
+        'member': get_authenticated_user().username,
+        'team': team.name,
+        'inviter': inviter.username
+    })
+
+    return 'Deleted', 204
diff --git a/endpoints/web.py b/endpoints/web.py
index 19f9bb7f1..b1e69bc5e 100644
--- a/endpoints/web.py
+++ b/endpoints/web.py
@@ -32,8 +32,8 @@ STATUS_TAGS = app.config['STATUS_TAGS']
 @web.route('/', methods=['GET'], defaults={'path': ''})
 @web.route('/organization/<path:path>', methods=['GET'])
 @no_cache
-def index(path):
-  return render_page_template('index.html')
+def index(path, **kwargs):
+  return render_page_template('index.html', **kwargs)
 
 
 @web.route('/500', methods=['GET'])
@@ -101,7 +101,7 @@ def superuser():
 
 @web.route('/signin/')
 @no_cache
-def signin():
+def signin(redirect=None):
   return index('')
 
 
@@ -123,6 +123,13 @@ def new():
   return index('')
 
 
+@web.route('/confirminvite')
+@no_cache
+def confirm_invite():
+  code = request.values['code']
+  return index('', code=code)
+
+
 @web.route('/repository/', defaults={'path': ''})
 @web.route('/repository/<path:path>', methods=['GET'])
 @no_cache
diff --git a/initdb.py b/initdb.py
index 21485d5a4..860b4e135 100644
--- a/initdb.py
+++ b/initdb.py
@@ -214,6 +214,8 @@ def initialize_database():
   LogEntryKind.create(name='org_delete_team')
   LogEntryKind.create(name='org_invite_team_member')
   LogEntryKind.create(name='org_add_team_member')
+  LogEntryKind.create(name='org_team_member_invite_accepted')
+  LogEntryKind.create(name='org_team_member_invite_declined')
   LogEntryKind.create(name='org_remove_team_member')
   LogEntryKind.create(name='org_set_team_description')
   LogEntryKind.create(name='org_set_team_role')
diff --git a/static/js/app.js b/static/js/app.js
index 74a7b3454..e17e36e6f 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -535,7 +535,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
 
       stringBuilderService.buildString = function(value_or_func, metadata) {
          var fieldIcons = {
-          'adder': 'user',
+          'inviter': 'user',
           'username': 'user',
           'activating_username': 'user',
           'delegate_user': 'user',
@@ -1115,8 +1115,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
     return externalNotificationData;
   }]);
 
-  $provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService', 'Config',
-    function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService, Config) {
+  $provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService', 'Config', '$location',
+                                           function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService, Config, $location) {
       var notificationService = {
         'user': null,
         'notifications': [],
@@ -1135,15 +1135,24 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
         },
         'org_team_invite': {
           'level': 'primary',
-          'message': '{adder} is inviting you to join team {team} under organization {org}',
+          'message': '{inviter} is inviting you to join team {team} under organization {org}',
           'actions': [
             {
               'title': 'Join team',
               'kind': 'primary',
               'handler': function(notification) {
+                window.location = '/confirminvite?code=' + notification.metadata['code'];
               }
             },
-            {'title': 'Decline', 'kind': 'default'}
+            {
+              'title': 'Decline',
+              'kind': 'default',
+              'handler': function(notification) {
+                ApiService.declineOrganizationTeamInvite(null, {'code': notification.metadata['code']}).then(function() {
+                  notificationService.update();
+                });
+              }
+            }
           ]
         },
         'password_required': {
@@ -1725,7 +1734,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
                        templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
       when('/security/', {title: 'Security', description: 'Security features used when transmitting and storing data',
                        templateUrl: '/static/partials/security.html'}).
-      when('/signin/', {title: 'Sign In', description: 'Sign into ' + title, templateUrl: '/static/partials/signin.html'}).
+      when('/signin/', {title: 'Sign In', description: 'Sign into ' + title, templateUrl: '/static/partials/signin.html', controller: SignInCtrl, reloadOnSearch: false}).
       when('/new/', {title: 'Create new repository', description: 'Create a new public or private docker repository, optionally constructing from a dockerfile',
                      templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}).
       when('/organizations/', {title: 'Organizations', description: 'Private docker repository hosting for businesses and organizations',
@@ -1746,6 +1755,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
       when('/tour/features', {title: title + ' Features', templateUrl: '/static/partials/tour.html', controller: TourCtrl}).
       when('/tour/enterprise', {title: 'Enterprise Edition', templateUrl: '/static/partials/tour.html', controller: TourCtrl}).
 
+      when('/confirminvite', {title: 'Confirm Team Invite', templateUrl: '/static/partials/confirm-team-invite.html', controller: ConfirmInviteCtrl, reloadOnSearch: false}).
+
       when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl,
                  pageClass: 'landing-page'}).
       otherwise({redirectTo: '/'});
@@ -2244,6 +2255,10 @@ quayApp.directive('signinForm', function () {
       'signedIn': '&signedIn'
     },
     controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, CookieService, Features, Config) {
+      var getRedirectUrl = function() {
+        return $scope.redirectUrl;
+      };
+
       $scope.showGithub = function() {
         if (!Features.GITHUB_LOGIN) { return; }
 
@@ -2255,7 +2270,7 @@ quayApp.directive('signinForm', function () {
         }
 
         // Save the redirect URL in a cookie so that we can redirect back after GitHub returns to us.
-        var redirectURL = $scope.redirectUrl || window.location.toString();
+        var redirectURL = getRedirectUrl() || window.location.toString();
         CookieService.putPermanent('quay.redirectAfterLoad', redirectURL);
         
         // Needed to ensure that UI work done by the started callback is finished before the location
@@ -2283,17 +2298,19 @@ quayApp.directive('signinForm', function () {
           if ($scope.signedIn != null) {
             $scope.signedIn();
           }
-            
+
+          // Load the newly created user.
           UserService.load();
 
           // Redirect to the specified page or the landing page
           // Note: The timeout of 500ms is needed to ensure dialogs containing sign in
           // forms get removed before the location changes.
           $timeout(function() {
-           if ($scope.redirectUrl == $location.path()) {
-             return;
-           }
-           $location.path($scope.redirectUrl ? $scope.redirectUrl : '/');
+            var redirectUrl = getRedirectUrl();
+            if (redirectUrl == $location.path()) {
+              return;
+            }
+            window.location = (redirectUrl ? redirectUrl : '/');
           }, 500);
         }, function(result) {
           $scope.needsEmailVerification = result.data.needsEmailVerification;
@@ -2629,8 +2646,12 @@ quayApp.directive('logsView', function () {
           'org_create_team': 'Create team: {team}',
           'org_delete_team': 'Delete team: {team}',
           'org_add_team_member': 'Add member {member} to team {team}',
-          'org_invite_team_member': 'Invite user {member} to team {team}',
           'org_remove_team_member': 'Remove member {member} from team {team}',
+          'org_invite_team_member': 'Invite user {member} to team {team}',
+
+          'org_team_member_invite_accepted': 'User {member}, invited by {inviter}, accepted to join team {team}',
+          'org_team_member_invite_declined': 'User {member}, invited by {inviter}, declined to join team {team}',
+
           'org_set_team_description': 'Change description of team {team}: {description}',
           'org_set_team_role': 'Change permission of team {team} to {role}',
           'create_prototype_permission': function(metadata) {
@@ -2711,6 +2732,8 @@ quayApp.directive('logsView', function () {
         'org_add_team_member': 'Add team member',
         'org_invite_team_member': 'Invite team member',
         'org_remove_team_member': 'Remove team member',
+        'org_team_member_invite_accepted': 'Team invite accepted',
+        'org_team_member_invite_declined': 'Team invite declined',
         'org_set_team_description': 'Change team description',
         'org_set_team_role': 'Change team permission',
         'create_prototype_permission': 'Create default permission',
@@ -5346,7 +5369,7 @@ quayApp.directive('dockerfileBuildForm', function () {
         var data = {
           'mimeType': mimeType
         };
-        
+      
         var getUploadUrl = ApiService.getFiledropUrl(data).then(function(resp) {
           conductUpload(file, resp.url, resp.file_id, mimeType);
         }, function() {
diff --git a/static/js/controllers.js b/static/js/controllers.js
index 56689e80a..82ed5c684 100644
--- a/static/js/controllers.js
+++ b/static/js/controllers.js
@@ -20,6 +20,17 @@ $.fn.clipboardCopy = function() {
   });
 };
 
+function SignInCtrl($scope, $location) {
+  var redirect = $location.search()['redirect'];
+  if (redirect && redirect.indexOf('/') < 0) {
+    delete $location.search()['redirect'];
+    $scope.redirectUrl = '/' + redirect;
+    return;
+  }
+
+  $scope.redirectUrl = '/';
+}
+
 function GuideCtrl() {
 }
 
@@ -2855,3 +2866,28 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
 function TourCtrl($scope, $location) {
   $scope.kind = $location.path().substring('/tour/'.length);
 }
+
+function ConfirmInviteCtrl($scope, $location, UserService, ApiService, NotificationService) {
+  // Monitor any user changes and place the current user into the scope.
+  $scope.loading = false;
+
+  UserService.updateUserIn($scope, function(user) {
+    if (!user.anonymous && !$scope.loading) {
+      $scope.loading = true;
+
+      var params = {
+        'code': $location.search()['code']
+      };
+
+      ApiService.acceptOrganizationTeamInvite(null, params).then(function(resp) {
+        NotificationService.update();
+        $location.path('/organization/' + resp.org + '/teams/' + resp.team);
+      }, function() {
+        $scope.loading = false;
+        $scope.invalid = true;
+      });
+    }
+  });
+  
+  $scope.redirectUrl = 'confirminvite?code=' + $location.search()['code'];
+}
\ No newline at end of file
diff --git a/static/partials/confirm-team-invite.html b/static/partials/confirm-team-invite.html
new file mode 100644
index 000000000..625e9e262
--- /dev/null
+++ b/static/partials/confirm-team-invite.html
@@ -0,0 +1,13 @@
+<div class="confirm-team-invite">
+  <div class="container signin-container">
+    <div class="row">
+      <div class="col-sm-6 col-sm-offset-3">
+        <div class="user-setup" ng-show="user.anonymous" redirect-url="redirectUrl"></div>        
+        <div class="quay-spinner" ng-show="!user.anonymous && loading"></div>
+        <div class="alert alert-danger" ng-show="!user.anonymous && invalid">
+          Invalid confirmation code
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
diff --git a/static/partials/signin.html b/static/partials/signin.html
index 4aac6cb7e..2a1a9563d 100644
--- a/static/partials/signin.html
+++ b/static/partials/signin.html
@@ -1,7 +1,7 @@
 <div class="container signin-container">
   <div class="row">
     <div class="col-sm-6 col-sm-offset-3">
-      <div class="user-setup" redirect-url="'/'"></div>
+      <div class="user-setup" redirect-url="redirectUrl"></div>
     </div>
   </div>
 </div>
diff --git a/test/test_api_security.py b/test/test_api_security.py
index 5b3e5612d..364432d4b 100644
--- a/test/test_api_security.py
+++ b/test/test_api_security.py
@@ -8,7 +8,7 @@ from app import app
 from initdb import setup_database_for_testing, finished_database_for_testing
 from endpoints.api import api_bp, api
 
-from endpoints.api.team import TeamMember, TeamMemberList, OrganizationTeam
+from endpoints.api.team import TeamMember, TeamMemberList, OrganizationTeam, TeamMemberInvite
 from endpoints.api.tag import RepositoryTagImages, RepositoryTag
 from endpoints.api.search import FindRepositories, EntitySearch
 from endpoints.api.image import RepositoryImageChanges, RepositoryImage, RepositoryImageList
@@ -3424,6 +3424,36 @@ class TestSuperUserLogs(ApiTestCase):
     self._run_test('GET', 200, 'devtable', None)
 
 
+class TestTeamMemberInvite(ApiTestCase):
+  def setUp(self):
+    ApiTestCase.setUp(self)
+    self._set_url(TeamMemberInvite, code='foobarbaz')
+
+  def test_put_anonymous(self):
+    self._run_test('PUT', 401, None, None)
+
+  def test_put_freshuser(self):
+    self._run_test('PUT', 404, 'freshuser', None)
+
+  def test_put_reader(self):
+    self._run_test('PUT', 404, 'reader', None)
+
+  def test_put_devtable(self):
+    self._run_test('PUT', 404, 'devtable', None)
+
+  def test_delete_anonymous(self):
+    self._run_test('DELETE', 401, None, None)
+
+  def test_delete_freshuser(self):
+    self._run_test('DELETE', 404, 'freshuser', None)
+
+  def test_delete_reader(self):
+    self._run_test('DELETE', 404, 'reader', None)
+
+  def test_delete_devtable(self):
+    self._run_test('DELETE', 404, 'devtable', None)
+
+
 class TestSuperUserList(ApiTestCase):
   def setUp(self):
     ApiTestCase.setUp(self)
@@ -3442,7 +3472,6 @@ class TestSuperUserList(ApiTestCase):
     self._run_test('GET', 200, 'devtable', None)
 
 
-
 class TestSuperUserManagement(ApiTestCase):
   def setUp(self):
     ApiTestCase.setUp(self)
diff --git a/test/test_api_usage.py b/test/test_api_usage.py
index c91005c5c..99086e45b 100644
--- a/test/test_api_usage.py
+++ b/test/test_api_usage.py
@@ -11,7 +11,7 @@ from app import app
 from initdb import setup_database_for_testing, finished_database_for_testing
 from data import model, database
 
-from endpoints.api.team import TeamMember, TeamMemberList, OrganizationTeam
+from endpoints.api.team import TeamMember, TeamMemberList, TeamMemberInvite, OrganizationTeam
 from endpoints.api.tag import RepositoryTagImages, RepositoryTag
 from endpoints.api.search import FindRepositories, EntitySearch
 from endpoints.api.image import RepositoryImage, RepositoryImageList
@@ -734,16 +734,50 @@ class TestGetOrganizationTeamMembers(ApiTestCase):
                                 params=dict(orgname=ORGANIZATION,
                                             teamname='readers'))
 
-    assert READ_ACCESS_USER in json['members']
+    self.assertEquals(READ_ACCESS_USER, json['members'][1]['name'])
 
 
 class TestUpdateOrganizationTeamMember(ApiTestCase):
-  def test_addmember(self):
+  def assertInTeam(self, data, membername):
+    for memberData in data['members']:
+      if memberData['name'] == membername:
+        return
+
+    self.fail(membername + ' not found in team: ' + json.dumps(data))
+
+  def test_addmember_alreadyteammember(self):
     self.login(ADMIN_ACCESS_USER)
 
+    membername = READ_ACCESS_USER
+    self.putResponse(TeamMember,
+                     params=dict(orgname=ORGANIZATION, teamname='readers',
+                                 membername=membername),
+                     expected_code=400)
+     
+
+  def test_addmember_orgmember(self):
+    self.login(ADMIN_ACCESS_USER)
+
+    membername = READ_ACCESS_USER
+    self.putJsonResponse(TeamMember,
+                         params=dict(orgname=ORGANIZATION, teamname='owners',
+                                     membername=membername))
+     
+    # Verify the user was added to the team.
+    json = self.getJsonResponse(TeamMemberList,
+                                params=dict(orgname=ORGANIZATION,
+                                            teamname='owners'))
+
+    self.assertInTeam(json, membername)
+
+
+  def test_addmember_robot(self):
+    self.login(ADMIN_ACCESS_USER)
+
+    membername = ORGANIZATION + '+coolrobot'
     self.putJsonResponse(TeamMember,
                          params=dict(orgname=ORGANIZATION, teamname='readers',
-                                     membername=NO_ACCESS_USER))
+                                     membername=membername))
      
 
     # Verify the user was added to the team.
@@ -751,7 +785,152 @@ class TestUpdateOrganizationTeamMember(ApiTestCase):
                                 params=dict(orgname=ORGANIZATION,
                                             teamname='readers'))
 
-    assert NO_ACCESS_USER in json['members']
+    self.assertInTeam(json, membername)
+
+
+  def test_addmember_invalidrobot(self):
+    self.login(ADMIN_ACCESS_USER)
+
+    membername = 'freshuser+anotherrobot'
+    self.putResponse(TeamMember,
+                     params=dict(orgname=ORGANIZATION, teamname='readers',
+                                 membername=membername),
+                     expected_code=400)
+  
+   
+  def test_addmember_nonorgmember(self):
+    self.login(ADMIN_ACCESS_USER)
+
+    membername = NO_ACCESS_USER
+    response = self.putJsonResponse(TeamMember,
+                                    params=dict(orgname=ORGANIZATION, teamname='owners',
+                                                membername=membername))
+     
+
+    self.assertEquals(True, response['invited'])
+
+    # Make sure the user is not (yet) part of the team.
+    json = self.getJsonResponse(TeamMemberList,
+                                params=dict(orgname=ORGANIZATION,
+                                            teamname='readers'))
+
+    for member in json['members']:
+      self.assertNotEqual(membername, member['name'])
+
+
+class TestAcceptTeamMemberInvite(ApiTestCase):
+  def assertInTeam(self, data, membername):
+    for memberData in data['members']:
+      if memberData['name'] == membername:
+        return
+
+    self.fail(membername + ' not found in team: ' + json.dumps(data))
+
+  def test_accept_wronguser(self):
+    self.login(ADMIN_ACCESS_USER)
+
+    # Create the invite.
+    membername = NO_ACCESS_USER
+    response = self.putJsonResponse(TeamMember,
+                                    params=dict(orgname=ORGANIZATION, teamname='owners',
+                                                membername=membername))
+    
+    self.assertEquals(True, response['invited'])
+
+    # Try to accept the invite.
+    user = model.get_user(membername)
+    invites = list(model.lookup_team_invites(user))
+    self.assertEquals(1, len(invites))
+
+    self.putResponse(TeamMemberInvite,
+                     params=dict(code=invites[0].invite_token),
+                     expected_code=404)
+
+
+  def test_accept(self):
+    self.login(ADMIN_ACCESS_USER)
+
+    # Create the invite.
+    membername = NO_ACCESS_USER
+    response = self.putJsonResponse(TeamMember,
+                                    params=dict(orgname=ORGANIZATION, teamname='owners',
+                                                membername=membername))
+    
+    self.assertEquals(True, response['invited'])
+
+    # Login as the user.
+    self.login(membername)
+
+    # Accept the invite.
+    user = model.get_user(membername)
+    invites = list(model.lookup_team_invites(user))
+    self.assertEquals(1, len(invites))
+
+    self.putJsonResponse(TeamMemberInvite,
+                         params=dict(code=invites[0].invite_token))
+
+    # Verify the user is now on the team.
+    json = self.getJsonResponse(TeamMemberList,
+                                params=dict(orgname=ORGANIZATION,
+                                            teamname='owners'))
+
+    self.assertInTeam(json, membername)
+    
+    # Verify the accept now fails.
+    self.putResponse(TeamMemberInvite,
+                     params=dict(code=invites[0].invite_token),
+                     expected_code=404)
+
+
+
+class TestDeclineTeamMemberInvite(ApiTestCase):
+  def test_decline_wronguser(self):
+    self.login(ADMIN_ACCESS_USER)
+
+    # Create the invite.
+    membername = NO_ACCESS_USER
+    response = self.putJsonResponse(TeamMember,
+                                    params=dict(orgname=ORGANIZATION, teamname='owners',
+                                                membername=membername))
+    
+    self.assertEquals(True, response['invited'])
+
+    # Try to decline the invite.
+    user = model.get_user(membername)
+    invites = list(model.lookup_team_invites(user))
+    self.assertEquals(1, len(invites))
+
+    self.deleteResponse(TeamMemberInvite,
+                        params=dict(code=invites[0].invite_token),
+                        expected_code=404)
+
+
+  def test_decline(self):
+    self.login(ADMIN_ACCESS_USER)
+
+    # Create the invite.
+    membername = NO_ACCESS_USER
+    response = self.putJsonResponse(TeamMember,
+                                    params=dict(orgname=ORGANIZATION, teamname='owners',
+                                                membername=membername))
+    
+    self.assertEquals(True, response['invited'])
+
+    # Login as the user.
+    self.login(membername)
+
+    # Decline the invite.
+    user = model.get_user(membername)
+    invites = list(model.lookup_team_invites(user))
+    self.assertEquals(1, len(invites))
+
+    self.deleteResponse(TeamMemberInvite,
+                        params=dict(code=invites[0].invite_token))
+
+    # Make sure the invite was deleted.
+    self.deleteResponse(TeamMemberInvite,
+                        params=dict(code=invites[0].invite_token),
+                        expected_code=404)
 
 
 class TestDeleteOrganizationTeamMember(ApiTestCase):
@@ -768,7 +947,7 @@ class TestDeleteOrganizationTeamMember(ApiTestCase):
                                 params=dict(orgname=ORGANIZATION,
                                             teamname='readers'))
 
-    assert not READ_ACCESS_USER in json['members']
+    assert len(json['members']) == 1
 
 
 class TestCreateRepo(ApiTestCase):
@@ -2064,7 +2243,7 @@ class TestSuperUserManagement(ApiTestCase):
 
     json = self.getJsonResponse(SuperUserManagement, params=dict(username = 'freshuser'))
     self.assertEquals('freshuser', json['username'])
-    self.assertEquals('no@thanks.com', json['email'])
+    self.assertEquals('jschorr+test@devtable.com', json['email'])
     self.assertEquals(False, json['super_user'])    
 
   def test_delete_user(self):
@@ -2087,7 +2266,7 @@ class TestSuperUserManagement(ApiTestCase):
     # Verify the user exists.
     json = self.getJsonResponse(SuperUserManagement, params=dict(username = 'freshuser'))
     self.assertEquals('freshuser', json['username'])
-    self.assertEquals('no@thanks.com', json['email'])
+    self.assertEquals('jschorr+test@devtable.com', json['email'])
 
     # Update the user.
     self.putJsonResponse(SuperUserManagement, params=dict(username='freshuser'), data=dict(email='foo@bar.com'))
diff --git a/util/useremails.py b/util/useremails.py
index 33307623c..7e727cb40 100644
--- a/util/useremails.py
+++ b/util/useremails.py
@@ -67,7 +67,7 @@ To confirm this email address, please click the following link:<br>
 
 INVITE_TO_ORG_TEAM_MESSAGE = """
 Hi {0},<br>
-{1} has invited you to join the team {2} under organization {3} on <a href="{4}">{5}</a>.
+{1} has invited you to join the team <b>{2}</b> under organization <b>{3}</b> on <a href="{4}">{5}</a>.
 <br><br>
 To join the team, please click the following link:<br>
 <a href="{4}/confirminvite?code={6}">{4}/confirminvite?code={6}</a>