diff --git a/src/models/note.ts b/src/models/note.ts
index f2fb39051..8ca65bb42 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -38,11 +38,7 @@ export type INote = {
 	fileIds: mongo.ObjectID[];
 	replyId: mongo.ObjectID;
 	renoteId: mongo.ObjectID;
-	poll: {
-		choices: Array<{
-			id: number;
-		}>
-	};
+	poll: IPoll;
 	text: string;
 	tags: string[];
 	tagsLower: string[];
@@ -102,6 +98,16 @@ export type INote = {
 	_files?: IDriveFile[];
 };
 
+export type IPoll = {
+	choices: IChoice[]
+};
+
+export type IChoice = {
+	id: number;
+	text: string;
+	votes: number;
+};
+
 export const hideNote = async (packedNote: any, meId: mongo.ObjectID) => {
 	let hide = false;
 
diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts
index bee2d943a..dd0083340 100644
--- a/src/remote/activitypub/models/note.ts
+++ b/src/remote/activitypub/models/note.ts
@@ -14,6 +14,8 @@ import Emoji, { IEmoji } from '../../../models/emoji';
 import { ITag } from './tag';
 import { toUnicode } from 'punycode';
 import { unique, concat, difference } from '../../../prelude/array';
+import { extractPollFromQuestion } from './question';
+import vote from '../../../services/note/polls/vote';
 
 const log = debug('misskey:activitypub');
 
@@ -110,6 +112,16 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
 	// テキストのパース
 	const text = note._misskey_content ? note._misskey_content : htmlToMFM(note.content);
 
+	// vote
+	if (reply && reply.poll && text != null) {
+		const m = text.match(/([0-9])$/);
+		if (m) {
+			log(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${m[0]}`);
+			await vote(actor, reply, Number(m[1]));
+			return null;
+		}
+	}
+
 	const emojis = await extractEmojis(note.tag, actor.host).catch(e => {
 		console.log(`extractEmojis: ${e}`);
 		return [] as IEmoji[];
@@ -117,6 +129,9 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
 
 	const apEmojis = emojis.map(emoji => emoji.name);
 
+	const questionUri = note._misskey_question;
+	const poll = questionUri ? await extractPollFromQuestion(questionUri).catch(() => undefined) : undefined;
+
 	// ユーザーの情報が古かったらついでに更新しておく
 	if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
 		updatePerson(note.attributedTo);
@@ -137,6 +152,8 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
 		apMentions,
 		apHashtags,
 		apEmojis,
+		questionUri,
+		poll,
 		uri: note.id
 	}, silent);
 }
diff --git a/src/remote/activitypub/models/question.ts b/src/remote/activitypub/models/question.ts
new file mode 100644
index 000000000..53892a409
--- /dev/null
+++ b/src/remote/activitypub/models/question.ts
@@ -0,0 +1,19 @@
+import { IChoice, IPoll } from '../../../models/note';
+import Resolver from '../resolver';
+
+export async function extractPollFromQuestion(questionUri: string): Promise<IPoll> {
+	const resolver = new Resolver();
+	const question = await resolver.resolve(questionUri) as any;
+
+	const choices: IChoice[] = question.oneOf.map((x: any, i: number) => {
+			return {
+				id: i,
+				text: x.name,
+				votes: x._misskey_votes || 0,
+			} as IChoice;
+	});
+
+	return {
+		choices
+	};
+}
diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
index 4bb2281da..190e01838 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -93,17 +93,27 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
 
 	let text = note.text;
 
+	let question: string;
 	if (note.poll != null) {
 		if (text == null) text = '';
 		const url = `${config.url}/notes/${note._id}`;
 		// TODO: i18n
-		text += `\n\n[投票を見る](${url})`;
+		text += `\n\n[リモートで投票を見る](${url})`;
+
+		question = `${config.url}/questions/${note._id}`;
 	}
 
 	let apText = text;
+	if (apText == null) apText = '';
+
+	// Provides choices as text for AP
+	if (note.poll != null) {
+		const cs = note.poll.choices.map(c => `${c.id}: ${c.text}`);
+		apText += '\n';
+		apText += cs.join('\n');
+	}
 
 	if (quote) {
-		if (apText == null) apText = '';
 		apText += `\n\nRE: ${quote}`;
 	}
 
@@ -130,6 +140,7 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
 		content,
 		_misskey_content: text,
 		_misskey_quote: quote,
+		_misskey_question: question,
 		published: note.createdAt.toISOString(),
 		to,
 		cc,
diff --git a/src/remote/activitypub/renderer/question.ts b/src/remote/activitypub/renderer/question.ts
new file mode 100644
index 000000000..9df4daca3
--- /dev/null
+++ b/src/remote/activitypub/renderer/question.ts
@@ -0,0 +1,20 @@
+import config from '../../../config';
+import { ILocalUser } from '../../../models/user';
+import { INote } from '../../../models/note';
+
+export default async function renderQuestion(user: ILocalUser, note: INote) {
+	const question =  {
+		type: 'Question',
+		id: `${config.url}/questions/${note._id}`,
+		actor: `${config.url}/users/${user._id}`,
+		content:  note.text != null ? note.text : '',
+		oneOf: note.poll.choices.map(c => {
+			return {
+				name: c.text,
+				_misskey_votes: c.votes,
+			};
+		}),
+	};
+
+	return question;
+}
diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts
index 9ffe73a67..b902abea2 100644
--- a/src/remote/activitypub/type.ts
+++ b/src/remote/activitypub/type.ts
@@ -42,6 +42,7 @@ export interface INote extends IObject {
 	type: 'Note';
 	_misskey_content: string;
 	_misskey_quote: string;
+	_misskey_question: string;
 }
 
 export interface IPerson extends IObject {
diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
index 9adc3dd94..ac8d3d4e2 100644
--- a/src/server/activitypub.ts
+++ b/src/server/activitypub.ts
@@ -16,6 +16,7 @@ import Outbox, { packActivity } from './activitypub/outbox';
 import Followers from './activitypub/followers';
 import Following from './activitypub/following';
 import Featured from './activitypub/featured';
+import renderQuestion from '../remote/activitypub/renderer/question';
 
 // Init router
 const router = new Router();
@@ -110,6 +111,36 @@ router.get('/notes/:note/activity', async ctx => {
 	setResponseType(ctx);
 });
 
+// question
+router.get('/questions/:question', async (ctx, next) => {
+	if (!ObjectID.isValid(ctx.params.question)) {
+		ctx.status = 404;
+		return;
+	}
+
+	const poll = await Note.findOne({
+		_id: new ObjectID(ctx.params.question),
+		visibility: { $in: ['public', 'home'] },
+		localOnly: { $ne: true },
+		poll: {
+			$exists: true,
+			$ne: null
+		},
+	});
+
+	if (poll === null) {
+		ctx.status = 404;
+		return;
+	}
+
+	const user = await User.findOne({
+			_id: poll.userId
+	});
+
+	ctx.body = pack(await renderQuestion(user as ILocalUser, poll));
+	setResponseType(ctx);
+});
+
 // outbox
 router.get('/users/:user/outbox', Outbox);
 
diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts
index 8de0eb420..f99fb099c 100644
--- a/src/server/api/endpoints/notes/polls/vote.ts
+++ b/src/server/api/endpoints/notes/polls/vote.ts
@@ -6,6 +6,8 @@ import watch from '../../../../../services/note/watch';
 import { publishNoteStream } from '../../../../../stream';
 import notify from '../../../../../notify';
 import define from '../../../define';
+import createNote from '../../../../../services/note/create';
+import User from '../../../../../models/user';
 
 export const meta = {
 	desc: {
@@ -114,4 +116,19 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
 	if (user.settings.autoWatch !== false) {
 		watch(user._id, note);
 	}
+
+	// リモート投票の場合リプライ送信
+	if (note._user.host != null) {
+		const pollOwner = await User.findOne({
+			_id: note.userId
+		});
+
+		createNote(user, {
+			createdAt: new Date(),
+			text: ps.choice.toString(),
+			reply: note,
+			visibility: 'specified',
+			visibleUsers: [ pollOwner ],
+		});
+	}
 }));
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index fbafe2910..8d1ab181b 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -103,6 +103,7 @@ type Option = {
 	apMentions?: IUser[];
 	apHashtags?: string[];
 	apEmojis?: string[];
+	questionUri?: string;
 	uri?: string;
 	app?: IApp;
 };
diff --git a/src/services/note/polls/vote.ts b/src/services/note/polls/vote.ts
new file mode 100644
index 000000000..ee657f19c
--- /dev/null
+++ b/src/services/note/polls/vote.ts
@@ -0,0 +1,78 @@
+import Vote from '../../../models/poll-vote';
+import Note, { INote } from '../../../models/note';
+import Watching from '../../../models/note-watching';
+import watch from '../../../services/note/watch';
+import { publishNoteStream } from '../../../stream';
+import notify from '../../../notify';
+import createNote from '../../../services/note/create';
+import { isLocalUser, IUser } from '../../../models/user';
+
+export default (user: IUser, note: INote, choice: number) => new Promise(async (res, rej) => {
+	if (!note.poll.choices.some(x => x.id == choice)) return rej('invalid choice param');
+
+	// if already voted
+	const exist = await Vote.findOne({
+		noteId: note._id,
+		userId: user._id
+	});
+
+	if (exist !== null) {
+		return rej('already voted');
+	}
+
+	// Create vote
+	await Vote.insert({
+		createdAt: new Date(),
+		noteId: note._id,
+		userId: user._id,
+		choice: choice
+	});
+
+	// Send response
+	res();
+
+	const inc: any = {};
+	inc[`poll.choices.${note.poll.choices.findIndex(c => c.id == choice)}.votes`] = 1;
+
+	// Increment votes count
+	await Note.update({ _id: note._id }, {
+		$inc: inc
+	});
+
+	publishNoteStream(note._id, 'pollVoted', {
+		choice: choice,
+		userId: user._id.toHexString()
+	});
+
+	// Notify
+	notify(note.userId, user._id, 'poll_vote', {
+		noteId: note._id,
+		choice: choice
+	});
+
+	// Fetch watchers
+	Watching
+		.find({
+			noteId: note._id,
+			userId: { $ne: user._id },
+			// 削除されたドキュメントは除く
+			deletedAt: { $exists: false }
+		}, {
+			fields: {
+				userId: true
+			}
+		})
+		.then(watchers => {
+			for (const watcher of watchers) {
+				notify(watcher.userId, user._id, 'poll_vote', {
+					noteId: note._id,
+					choice: choice
+				});
+			}
+		});
+
+	// ローカルユーザーが投票した場合この投稿をWatchする
+	if (isLocalUser(user) && user.settings.autoWatch !== false) {
+		watch(user._id, note);
+	}
+});