本文へ移動
知恵の一覧へ
運営/DX/AIX道具・素材

ZendeskAPIの使い方

Zendeskのチケット本文とコメントをGASでスプレッドシートに書き出すコード一式

この知恵で考えられること

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 トークンを発行する

  1. Zendesk Support 管理センター > Apps and Integrations > Zendesk API
  2. 「Token Access」を ON → [Add API token]
  3. メモ帳にコピー(例 → 4xDs8reJGxv...) ※トークンは一度しか表示されません。
  4. 「Email + /token」の形で認証に使う 例)you@example.com/token

テスト疎通をしてtokenの有効性を確認する

  • 1件のチケットコメントを取る
  1. 空のスプレッドシートを作成 > 拡張機能 > Apps Script
  2. 以下を貼り付け、冒頭を書き換え • 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);
}

  1. 関数 getTicketComments を手動実行(デバッグ ▶︎ボタン)
  2. シートにコメント行が入れば OK。

利用例:過去3ヶ月のコメントを取得

作業の流れ

  1. Zendesk で API トークンを発行

    管理センター → Apps and integrations → Zendesk API → Add API token

    例:4xDs8reJGxv……

  2. 新しいスプレッドシートを作成

    拡張機能 → Apps Script を開く

  3. 下記スクリプトをまるごと貼り付けて

    ZD_SUBDOMAIN / ZD_EMAIL_TOKEN を書き換える

  4. Apps Script 右上の「ディスクマーク」で保存

    メニューから関数 exportLast3Months() を実行

    → 初回は認可ダイアログが出るので許可

  5. 数分待つと “Comments” シートに結果が入る

    3 か月分が多すぎる場合は日付を短くして再実行

処理の仕組みの概要と補足

  1. 件名は “コメント API” では返らないため、まず

    incremental/tickets.json で「過去 3 か月に更新されたチケット一覧」を取得し

    ① ID ② 件名 をマップ化します。

  2. つづいて incremental/ticket_events.json を同じ start_time で呼び、

    child_events の中の type=comment をすべて抽出。

    これに 1 のマップを突き合わせて件名を付け、シートへ書き込みます。

  3. Zendesk API レートリミット(400 req/分)対策で

    ページループのたびに Utilities.sleep(200ms) を挟んでいます。

    3 か月分でも通常数百~数千リクエストで終わります。

GASスクリプト

docs.google.com

/************ 認証変数 ************/
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;
}
ZendeskAPIの使い方|Tinker