この知恵で考えられること
1. Zendesk Support 管理センター > Apps and Integrations > Zendesk API 2. 「Token Access」を ON → [Add API token] 3. メモ帳にコピー(例 → 4xDs8reJGxv...) ※トークンは一度しか表示されません。 4. 「Email + /token」の形で認証に使う 例)you@example.com/
通信を作るための初手
Zendesk API トークンを発行する
- Zendesk Support 管理センター > Apps and Integrations > Zendesk API
- 「Token Access」を ON → [Add API token]
- メモ帳にコピー(例 → 4xDs8reJGxv...) ※トークンは一度しか表示されません。
- 「Email + /token」の形で認証に使う 例)you@example.com/token
テスト疎通をしてtokenの有効性を確認する
- 1件のチケットコメントを取る
- 空のスプレッドシートを作成 > 拡張機能 > Apps Script
- 以下を貼り付け、冒頭を書き換え • ZD_SUBDOMAIN = ‘your_subdomain’ • ZD_EMAIL_TOKEN = ‘you@example.com/token:API_TOKEN’ • SHEET_NAME = ‘Comments’ (任意)
const ZD_SUBDOMAIN = 'your_subdomain'; // ★変更
const ZD_EMAIL_TOKEN = 'you@example.com/token:xxxx'; // ★変更
const SHEET_NAME = 'Comments'; // 出力先シート
/**
* 指定チケットIDのコメント一覧をシートへ書き込む
* =getTicketComments(123456)
*/
function getTicketComments(ticketId = 123) {
const url = `https://${ZD_SUBDOMAIN}.zendesk.com/api/v2/tickets/${ticketId}/comments.json`;
const options = {
method : 'get',
headers : { 'Authorization': 'Basic ' + Utilities.base64Encode(ZD_EMAIL_TOKEN) },
muteHttpExceptions : true
};
const res = UrlFetchApp.fetch(url, options);
const body = JSON.parse(res.getContentText());
if (res.getResponseCode() !== 200) {
throw new Error(`Zendesk API error: ${res.getResponseCode()} ${body.error}`);
}
const sheet = getOrCreateSheet_(SHEET_NAME);
const rows = body.comments.map(c => [
ticketId,
c.id,
c.created_at,
c.public ? 'Public' : 'Internal',
c.author_id,
c.body.replace(/\\r?\\n/g, '⏎') // 改行を可視化
]);
// ヘッダ無ければ付ける
if (sheet.getLastRow() === 0) {
sheet.appendRow(['ticket_id','comment_id','created_at','type','author_id','body']);
}
sheet.getRange(sheet.getLastRow()+1,1,rows.length,rows[0].length).setValues(rows);
}
/* Util */
function getOrCreateSheet_(name) {
const ss = SpreadsheetApp.getActive();
return ss.getSheetByName(name) || ss.insertSheet(name);
}
- 関数
getTicketCommentsを手動実行(デバッグ ▶︎ボタン) - シートにコメント行が入れば OK。
利用例:過去3ヶ月のコメントを取得
作業の流れ
-
Zendesk で API トークンを発行
管理センター → Apps and integrations → Zendesk API → Add API token
例:4xDs8reJGxv……
-
新しいスプレッドシートを作成
拡張機能 → Apps Script を開く
-
下記スクリプトをまるごと貼り付けて
ZD_SUBDOMAIN / ZD_EMAIL_TOKEN を書き換える
-
Apps Script 右上の「ディスクマーク」で保存
メニューから関数 exportLast3Months() を実行
→ 初回は認可ダイアログが出るので許可
-
数分待つと “Comments” シートに結果が入る
3 か月分が多すぎる場合は日付を短くして再実行
処理の仕組みの概要と補足
-
件名は “コメント API” では返らないため、まず
incremental/tickets.json で「過去 3 か月に更新されたチケット一覧」を取得し
① ID ② 件名 をマップ化します。
-
つづいて incremental/ticket_events.json を同じ start_time で呼び、
child_events の中の type=comment をすべて抽出。
これに 1 のマップを突き合わせて件名を付け、シートへ書き込みます。
-
Zendesk API レートリミット(400 req/分)対策で
ページループのたびに Utilities.sleep(200ms) を挟んでいます。
3 か月分でも通常数百~数千リクエストで終わります。
GASスクリプト
/************ 認証変数 ************/
const ZD_SUBDOMAIN = 'greenspoonsupport'; // 例: greenspoonsupport
const ZD_EMAIL = 'ren.kurosaki@greenspoon.co.jp';
const ZD_API_TOKEN = 'L2PO90mkVmvWUzV40LaLOKbbbQGwN5mnpu2KQjg9'; // 必ず新しく発行したAPIトークンを使用
const SHEET_NAME = 'Comments_ALL'; // 出力先のシート名
const BACK_DAYS = 3; // 過去◯日分のチケットを対象(例:3日)
/***********************************************/
const AUTH_HEADER = {
'Authorization': 'Basic ' + Utilities.base64Encode(`${ZD_EMAIL}/token:${ZD_API_TOKEN}`)
};
// time-based incremental API は1分に1回しか叩けない制限 → 安全策で65秒空ける
const SLEEP_MS = 65 * 1000;
/**
* ユーザーの role をキャッシュするマップ
* key: user_id (number), value: role文字列 ("agent", "end-user" など)
*/
let userRoleMap = {}; // グローバル変数(Apps Script 上では実行ごとにリセットされます)
/**
* =============================
* メインエントリポイント
* =============================
*/
function exportDescriptionAndComments() {
const startTime = Math.floor(Date.now() / 1000 - BACK_DAYS * 86400);
Logger.log(`startTime=${startTime}, now=${Math.floor(Date.now()/1000)}, days=${BACK_DAYS}`);
const sheet = prepSheet_();
// ① チケット一覧を取得
// ticket_id → subject
// ticket_id → ticket_created_at
const { ticketIds, subjectMap, createdMap } = fetchTicketsAndSubjects_(startTime);
Logger.log(`Total tickets fetched: ${ticketIds.length}`);
// ② 監査ログで description + comments を取得
ticketIds.forEach((tid, idx) => {
Logger.log(`[${idx+1}/${ticketIds.length}] ticket #${tid}`);
fetchTicketAudits_(tid, sheet, subjectMap, createdMap);
// 1 秒ほど待つ(audits API は比較的レート制限緩め → 700/min 程度)
Utilities.sleep(1000);
});
SpreadsheetApp.getUi().alert('完了しました!');
}
/**
* =============================
* 1) チケット一覧の取得
* - ticket_id / subject / created_at をマップ化
* =============================
*/
function fetchTicketsAndSubjects_(startTime) {
let url = `https://${ZD_SUBDOMAIN}.zendesk.com/api/v2/incremental/tickets.json?start_time=${startTime}`;
const ticketIds = [];
const subjectMap = {};
const createdMap = {}; // { ticketId: new Date(...) }
while (url) {
const { code, data } = callZendesk_(url);
if (code !== 200) {
throw new Error(`tickets API error: HTTP ${code}`);
}
(data.tickets || []).forEach(t => {
ticketIds.push(t.id);
subjectMap[t.id] = t.subject || '';
if (t.created_at) {
createdMap[t.id] = new Date(t.created_at); // Date型に変換
} else {
createdMap[t.id] = null; // ない場合は null
}
});
if (data.end_of_stream) break;
url = data.next_page;
// 1分待機
Utilities.sleep(SLEEP_MS);
}
return { ticketIds, subjectMap, createdMap };
}
/**
* =============================
* 2) 監査ログを取得し
* description / Comment を行ごとに組み立て→シート書込
* =============================
*
* @param {number} ticketId
* @param {Sheet} sheet - 出力シート
* @param {object} subjectMap - { ticketId: subject }
* @param {object} createdMap - { ticketId: Date(ticketCreatedAt) }
*/
function fetchTicketAudits_(ticketId, sheet, subjectMap, createdMap) {
let url = `https://${ZD_SUBDOMAIN}.zendesk.com/api/v2/tickets/${ticketId}/audits.json?sort_order=asc`;
while (url) {
try {
const { code, data } = callZendesk_(url);
if (code === 404) {
// チケット削除・アクセス不能 → スキップ
Logger.log(`Ticket ${ticketId} => 404 Not Found. Skipping.`);
break;
}
if (code !== 200) {
throw new Error(`audits API error: HTTP ${code} for ticket ${ticketId}`);
}
const rows = [];
(data.audits || []).forEach(audit => {
const commentDate = new Date(audit.created_at); // コメントの発生日時
const events = audit.events || [];
events.forEach(ev => {
if (ev.type === 'Comment') {
// 回答/追記コメントイベント
const text = ev.body || ev.plain_body || ev.html_body || '';
const role = getUserRole_(ev.author_id); // end-user or agent など
rows.push([
// [0] ticket_id
ticketId,
// [1] subject
subjectMap[ticketId] || '',
// [2] ticket_created_at
createdMap[ticketId],
// [3] comment_datetime
commentDate,
// [4] comment_author_role
role,
// [5] author_id
ev.author_id,
// [6] comment_body
text.replace(/\r?\n/g, '⏎')
]);
} else if (ev.type === 'Create' && ev.field_name === 'description') {
// チケット作成時の本文
const desc = ev.value || '';
const role = getUserRole_(audit.author_id); // 作成者もaudits.author_id
rows.push([
ticketId,
subjectMap[ticketId] || '',
createdMap[ticketId],
commentDate, // createイベントの発生日時
role,
audit.author_id,
desc.replace(/\r?\n/g, '⏎')
]);
}
});
});
if (rows.length) {
sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length).setValues(rows);
Logger.log(`Ticket ${ticketId}: wrote ${rows.length} rows`);
} else {
Logger.log(`Ticket ${ticketId}: no description or comments in this page.`);
}
if (!data.next_page) {
break;
}
url = data.next_page;
// 次ページへ 1秒待機
Utilities.sleep(1000);
} catch (err) {
Logger.log(`Error on ticket ${ticketId}: ${err.message}`);
// このチケットはスキップして次へ
break;
}
}
}
/**
* =============================
* ユーザーの role を取得(キャッシュあり)
* /api/v2/users/{id}.json を呼び出し role を取得する
* =============================
*/
function getUserRole_(userId) {
if (!userId) {
return 'unknown';
}
if (userRoleMap[userId]) {
return userRoleMap[userId];
}
// 初回だけ呼び出す
const url = `https://${ZD_SUBDOMAIN}.zendesk.com/api/v2/users/${userId}.json`;
const { code, data } = callZendesk_(url);
if (code === 200 && data.user) {
const role = data.user.role || 'unknown'; // "end-user", "agent", "admin" 等
userRoleMap[userId] = role;
return role;
} else {
userRoleMap[userId] = 'unknown';
return 'unknown';
}
}
/**
* =============================
* Zendesk API 共通コール
* =============================
*/
function callZendesk_(url) {
const response = UrlFetchApp.fetch(url, {
headers: AUTH_HEADER,
muteHttpExceptions: true
});
return {
code: response.getResponseCode(),
data: JSON.parse(response.getContentText() || '{}')
};
}
/**
* =============================
* 出力シート初期化
* カラム =
* [ ticket_id,
* subject,
* ticket_created_at,
* comment_datetime,
* comment_author_role,
* author_id,
* comment_body ]
* =============================
*/
function prepSheet_() {
const ss = SpreadsheetApp.getActive();
let sh = ss.getSheetByName(SHEET_NAME);
if (!sh) {
sh = ss.insertSheet(SHEET_NAME);
}
if (sh.getLastRow() === 0) {
sh.appendRow([
'ticket_id',
'subject',
'ticket_created_at',
'comment_datetime',
'comment_author_role',
'author_id',
'comment_body'
]);
}
return sh;
}