diff --git a/app/db/repositories/invite.js b/app/db/repositories/invite.js
index bfc9ac915366aec298c02f8000bfc5063faaa6cd..a0e0854cfc1441f18bf742194ddcf2594a47f854 100644
--- a/app/db/repositories/invite.js
+++ b/app/db/repositories/invite.js
@@ -1,22 +1,46 @@
+import * as channelRepository from './membership'
+
 const membershipRepository = require('./membership')
 const { loggerOf } = require('../../services/util')
 const logger = loggerOf('db|inviteRepository')
 
-// (Database, string, string, string) -> boolean
+// (Database, string, string, string) -> Promise<boolean>
 const issue = async (db, channelPhoneNumber, inviterPhoneNumber, inviteePhoneNumber) => {
   // issues invite IFF invitee is not already member of channel or invited by same person
   // returns true if invite issued, false otherwise
   try {
     if (await membershipRepository.isMember(db, channelPhoneNumber, inviteePhoneNumber))
       return false
-    const [, wasInviteCreated] = await db.invite.findOrCreate({
+    return (await db.invite.findOrCreate({
       where: { channelPhoneNumber, inviterPhoneNumber, inviteePhoneNumber },
-    })
-    return wasInviteCreated
+    }))[1]
+  } catch (e) {
+    logger.error(e)
+    return false
+  }
+}
+
+// (Database, string, string, string) -> Promise<boolean>
+const accept = async (db, channelPhoneNumber, inviteePhoneNumber, language) => {
+  try {
+    await channelRepository.addSubscriber(db, channelPhoneNumber, inviteePhoneNumber, language)
+    await db.invite.destroy({ where: { channelPhoneNumber, inviteePhoneNumber } })
+    return true
+  } catch (e) {
+    logger.error(e)
+    return false
+  }
+}
+
+// (Database, string, string) -> Promise<boolean>
+const decline = async (db, channelPhoneNumber, inviteePhoneNumber) => {
+  try {
+    await db.invite.destroy({ where: { channelPhoneNumber, inviteePhoneNumber } })
+    return true
   } catch (e) {
     logger.error(e)
     return false
   }
 }
 
-module.exports = { issue }
+module.exports = { issue, accept, decline }
diff --git a/app/db/repositories/membership.js b/app/db/repositories/membership.js
index a2c9d396061321e6a3785513774252c6ba9999c8..f966487ae080ffac1da0d768cdc792cad8e77a52 100644
--- a/app/db/repositories/membership.js
+++ b/app/db/repositories/membership.js
@@ -42,6 +42,7 @@ const addSubscriber = async (
   memberPhoneNumber,
   language = defaultLanguage,
 ) =>
+  // TODO: use findOrCreate here to make this idempotent!
   performOpIfChannelExists(db, channelPhoneNumber, 'subscribe member to', () =>
     db.membership.create({
       type: memberTypes.SUBSCRIBER,
diff --git a/app/services/dispatcher/commands/execute.js b/app/services/dispatcher/commands/execute.js
index ab53c24b01ed0607c9265b833df930f142643744..6031b21ba171046a253858f70126fc322aa0b9a9 100644
--- a/app/services/dispatcher/commands/execute.js
+++ b/app/services/dispatcher/commands/execute.js
@@ -34,7 +34,9 @@ const execute = async (executable, dispatchable) => {
   // don't allow command execution on the signup channel for non-admins
   if (channel.phoneNumber === signupPhoneNumber && sender.type !== ADMIN) return noop()
   const result = await ({
+    [commands.ACCEPT]: () => maybeAccept(db, channel, sender, language),
     [commands.ADD]: () => maybeAddAdmin(db, channel, sender, payload),
+    [commands.DECLINE]: () => decline(db, channel, sender),
     [commands.HELP]: () => showHelp(db, channel, sender),
     [commands.INFO]: () => showInfo(db, channel, sender),
     [commands.INVITE]: () => maybeInvite(db, channel, sender, payload),
@@ -55,6 +57,27 @@ const execute = async (executable, dispatchable) => {
  * COMMAND EXECUTION
  ********************/
 
+// ACCEPT
+
+const maybeAccept = async (db, channel, sender, language) => {
+  const cr = messagesIn(language).commandResponses.accept
+  return (await membershipRepository.isMember(db, channel.phoneNumber, inviteePhoneNumber))
+    ? { status: statuses.ERROR, message: cr.alreadyMember }
+    : accept(db, channel, sender, language, cr)
+}
+
+const accept = async (db, channel, sender, language, cr) =>
+  (await accept(db, channel.phoneNumber, sender.phoneNumber, language))
+    ? { status: statuses.SUCCESS, message: cr.success }
+    : { status: statuses.ERROR, message: cr.dbError }
+
+// DECLINE
+
+const decline = async (db, channel, sender, inviteePhoneNumber, language, cr) =>
+  (await decline(db, channel.phoneNumber, sender.phoneNumber))
+    ? { status: statuses.SUCCESS, message: cr.success }
+    : { status: statuses.ERROR, message: cr.dbError }
+
 // ADD
 
 const maybeAddAdmin = async (db, channel, sender, phoneNumberInput) => {
diff --git a/app/services/dispatcher/strings/messages/EN.js b/app/services/dispatcher/strings/messages/EN.js
index 939b7e690ac16adda33aa8eac2c9aba7677cbd06..02fd11c752c9fbd7eae9fad6cf341fa9551b8052 100644
--- a/app/services/dispatcher/strings/messages/EN.js
+++ b/app/services/dispatcher/strings/messages/EN.js
@@ -76,6 +76,14 @@ Reply with HELP for more info.`,
 }
 
 const commandResponses = {
+  // ACCEPT
+
+  accept: {
+    success: 'foo',
+    alreadyMember: 'bar',
+    dbError: 'lala',
+  },
+
   // ADD
 
   add: {