Zoho CRM API v2を使う (& zoho_hubの使い方)


Zoho CRMというCRMのサービスがあるのですが、会社でこれを利用しています。
https://www.zoho.com/jp/crm/

CRMは顧客の情報を管理するシステムだと思えばOKです。

Zoho CRMにはAPIがあって、v1を使っているのですが、v1は2019/12/31にサポート終了するので、v2に移行した話です。

v1とv2の違い、注意点

v1とv2の違いは色々ありますが、こちらにまとまっています。
https://www.zoho.com/jp/crm/help/api-diff/

注意すべき点は

  • XMLからjsonに変わった
  • 日付がISO 8601準拠になった
    • v1ではyyyy-MM-dd HH:mm:ssの形式だったけど、v2ではyyyy-MM-dd’T’HH:mm:ssTZ
  • フィールド名が表示ラベルではなくAPI名になった
    • 詳細後述。カスタムフィールドを使う人は特に注意。作ったときのフィールドの名称ではなく、裏で持つ別名(API名, アルファベット)が必要
  • 1回に取得できる最大件数は200なので、全件取得したい場合はpageを変えた複数回リクエストが必要
  • 認証が単純なtoken方式ではなくOAuthになった。さらに認可もあるので権限要求に注意

です。これらに触れつつ、解説していきます。

v2 APIを使うための準備

(ZohoCRMアカウントは登録するだけです。既に用意されている前提)
APIは認証が必要なので、まずは認証を突破するための準備を行います。

基本的にはここに従うだけですが、結構はまりました・・。英語だし。
https://www.zoho.com/crm/developer/docs/api/overview.html

1. クライアント(アプリ)の登録

まず、API v1時代に使っていたtoken生成の画面は一切使いません。
代わりに Zoho Developer Console を開いて、クライアントの登録を行います。

ClientName: 任意
ClientDomain: 任意
Authorized redirect URIs: 適当。後述のSelf-Client方式の場合、リダイレクトは使わないのでなんでも良い
ClientType: WEB Based (今回の私の場合)

作成するとClient IDとClient Secretが生成されるのでこれをメモしておきます。

2. 認可リクエストとGrant tokenの生成

最終的に必要なものはAccess Token(と、Refresh Token)ですが、ここではそれの生成に必要なGrant Tokenを生成します。

https://www.zoho.com/crm/developer/docs/api/auth-request.html
こちらによるとHTTP(CLI)による生成も可能ですが、ブラウザによるGUIでの生成も可能です。

Self-Clientというところから生成するのですが、このときScopeに設定するものはこちらに書かれています。
https://www.zoho.com/crm//developer/docs/api/oauth-overview.html#scopes

APIで操作したいものを入力します。例えばAPIでZohoCRMのユーザだけ操作したいのであればZohoCRM.users.allと入力すれば良いです。ContactやAccountなども操作したいのであればZohoCRM.modules.contacts,ZohoCRM.modules.accountsなども追加します。

自分の場合はこうしました。(ほぼなんでもOK)
ZohoCRM.users.all,ZohoCRM.org.all,ZohoCRM.settings.all,ZohoCRM.modules.all,ZohoCRM.bulk.all

権限の範囲を狭めたりきっちり管理したい人はドキュメントを読むと良いかと思います。

Expiryは10分などに設定しておいてください。Grant Tokenの有効期限です。

View Codeボタンを押せばGrant Tokenが生成されますので、それをメモして、次の手順を10分以内に行います。(時間切れしたらまたGrant Token生成)

3. Access Token などの生成

$ curl -X POST "https://accounts.zoho.com/oauth/v2/token?grant_type=authorization_code&client_id=上記で生成されたもの&client_secret=上記で生成されたもの&redirect_uri=上記で設定したリダイレクトURI&code=上記で生成されたGrantToken"

ClientId, ClientSecret, GrantTokenを自分が生成したものに変更して、上記のPOSTを行います。https://accounts.zoho.comにアクセスしていますが、契約しているZohoがEUなどの場合https://accounts.zoho.euなど、適宜変更します。

このcurlで以下のようなjsonがかえってきます。

{
    "access_token": "{access_token}",
    "refresh_token": "{refresh_token}",
    "expires_in_sec": 3600,
    "api_domain": "https://www.zohoapis.com",
    "token_type": "Bearer",
    "expires_in": 3600000
}

access_tokenとrefresh_tokenをメモしておきます。

expires_in_secとexpires_inは3600秒(3600000ミリ秒)つまり1時間でaccess_tokenが有効切れになるということを意味しています。

refresh_tokenは無期限です。いつまでも使えます。なので漏洩するなどした場合は専用のAPIで失効させましょう。
https://www.zoho.com/crm/developer/docs/api/revoke-tokens.html

4. リクエストの実験とrefresh_token

上記の手順でaccess_tokenが生成できました!というわけでこれを使ってアクセスしてみましょう。AccessTokenはAuthorization: Zoho-oauthtokenヘッダに入れます。

$ curl -X GET -H "Authorization: Zoho-oauthtoken あなたのAccessToken" "https://www.zohoapis.com/crm/v2/Contacts"

以下、レスポンスの一部

{"data":[{"Owner":{"name":"foo","id":"1111111111111"},"Email":"foo@example.com","$currency_symbol":"¥","Visitor_Score":null,"$followers":null,"Last_Activity_Time":null,"Department":null,"$process_flow":false,"id":"22222222","$approved":true,"Reporting_To":null,"$approval":{"delegate":false,"approve":false,"reject":false,"resubmit":false},"First_Visited_URL":null,"Days_Visited":null,"Created_Time":"2019-10-03T18:42:00+09:00","$followed":false,"$editable":true,"SignUpURL":null,"field":null,"Last_Visited_Time":null,"Secondary_Email":null,"Description":null,"Vendor_Name":null,"Mailing_Zip":null,"Number_Of_Chats":null,"$review_process":null,"Average_Time_Spent_Minutes":null,"Salutation":"様","First_Name":null,"Full_Name":"foo bar","Record_Image":null,"$review":null,"Phone":null,"Account_Name":{"name":"foobar株式会社","id":"3333333"},"Email_Opt_Out":false,"Modified_Time":"2019-10-03T18:42:00+09:00","Title":null,"Mobile":null,"field1":null,"First_Visited_Time":null,"Last_Name":"foo","Referrer":null,"field7":false,"field6":null,"Tag":[],"Fax":null,"field3":null,"field2":null,"field5":false,"field4":null},..

access_tokenは1時間で失効します。なので、その場合はrefresh_tokenを使って新しいaccess_tokenを生成します。

$ curl -X POST "https://accounts.zoho.com/oauth/v2/token?refresh_token=上記で生成されたRefreshToken&client_id=上記で(略)&client_secret=上記で(略)&grant_type=refresh_token"

これで新しいAccessTokenが返ってくるのでそれを使ってまたアクセスします。

このTokenのRefreshは大抵のOAuthに対応したAPIライブラリならばやってくれることでしょう。

zoho_hubというRubyのwrapperライブラリ

私は会社ではRuby on Railsを使っていて、Rubyのzoho_hubというライブラリがあるので、今回はこれを利用しました。ここからはzoho_hubの使い方・特有の問題・Zoho v2 APIの注意点などを説明していきます。

https://github.com/rikas/zoho_hub

基本的にREADMEに従って設定して使うだけですが、一部苦労したので、そこについて解説します。

Tokenの設定

(この部分の理解が甘いですが)おそらく、GrantTokenのCLIによる生成はできないと思います。zoho_hubではZohoHub::Auth.urlでGrantTokenを生成するURLを発行していますが、これはブラウザでアクセスする必要があるのかと思います。

今回私が作成するプログラムはJob(GUIなし)なので、ブラウザアクセスを挟む余地はありません。なので方針を変えました。

ZohoHub.configure do |config|
    config.client_id = 'your_id'
    config.secret = 'your_secret'
    config.redirect_uri = 'https://your_url'
    config.api_domain = 'https://accounts.zoho.com'
    # config.debug = true
end

refresh_token = 'your_token'
params = ZohoHub::Auth.refresh_token(refresh_token)
  ZohoHub.setup_connection(access_token: params[:access_token], refresh_token: params[:refresh_token])

refresh_tokenというメソッドにRefreshTokenを与えると新しいAccessTokenを生成してくれます。なので、それを使ってconnectionを初期化するようにしました。

RefreshTokeは失効しないので、これで安定してAPIを使い続けることができます。

単にGETやPOSTをするか、ActiveRecord的にOR Mapperを挟むか

READMEを見ればわかりますが、ZohoHub.connection.get ‘Leads’ というように使うことができます。これはhttps://www.zohoapis.com/crm/v2/leadsにGETをしています。postやputもあり、jsonを引数にします。

この方法でもいいのですが、ActiveRecordのようにmodel(class)を経由して使うこともできます。READMEにサンプルがありますし、ここのディレクトリにもサンプルがあります。
https://github.com/rikas/zoho_hub/tree/master/examples/models

class Lead < ZohoHub::BaseRecord
  attributes: :id, :first_name, :last_name, :phone, :email, :source, # etc.
end

# Request a (paginated) list of all Lead records
Lead.all

# Get the Lead instance with a specific ID
Lead.find('78265000003433063')

lead = Lead.new(
    first_name: 'First name',
    last_name: 'Last name',
    phone: '+35197736281',
    email: 'myemail@gmail.com',
    source: 'Homepage')

# Creates the new lead
lead.save

カスタムフィールドを使っている場合は特に注意。jsonのkey名は項目名ではなくAPI名を使う

例えば資料請求日というカスタムフィールドを作成したとします。そしてこれを更新するために{“data”:[{“資料請求日”: “2019/01/01”, …以下略 という感じでPOST or PUTしたとします。

上記のzoho_hubでやる場合はこんな感じですね

class Account < ZohoHub::BaseRecord
  attribute :id, :document_request_date # etc

  attribute_translation(
    document_request_date: :資料請求日
  )
end

この場合、リクエストは成功しますが、資料請求日は更新されません。
Zoho v2 APIではリクエスト時のjsonのkey名は項目名ではなくAPI名を使う必要があります。API名は以下のURLで確認することができます。
https://crm.zoho.com/crm/settings/api/modules

この画像の例だと「あいう」という項目名ですが、API名は「field2」となっていますね。なので、API名を編集して好きな名前をつけると良いかと思います。「Document_Request_Date」とか。field2のまま行く場合で、zoho_hubを利用する場合は

attribute_translation(
  document_request_date: :field2
)

こうですね。attribute_translationを使わずにattributeを:field2とする方法は?と思いますが、それは多分動きません。内部的にjsonを生成するときに先頭を大文字にするのでattribute :field2の場合”Field2”: “foo”というJsonになります。なので、API名を大文字にするかattribute_translationを使うか、どちらかが良いです。

List APIで一発で全件取得はできない。200件に注意

https://www.zoho.com/crm/developer/docs/api/whats-new.html

一言で言うとPaginationがあるので、Query Stringの?page=1&per_page=200が必要、です。

こちらに書いてありますが、 “https://www.zohoapis.com/crm/v2/Contacts” などにGETすることで複数のデータを一度に取得できます。jsonのtop levelにはdataとinfoというkeyがあり、dataが配列になっているので、ここに複数入っています。デフォルトだと最大の200件なので、一度に全件取得はできません。また1000件とかも取れません(MAX 200)

Paginationの制御が必要で、次の200件が必要なら “https://www.zohoapis.com/crm/v2/Contacts&page=2” というようにページを変える必要があります。

zoho_hubの場合、こちらを見ればわかりますが、
https://github.com/rikas/zoho_hub/blob/master/lib/zoho_hub/base_record.rb
def allにはparamsという引数があるので、pageを指定してあげればOKです。ちなみに次のページを要求する必要があるのかどうかはdataではなく、infoのmore_recordsを見れば良いです。

zoho_hubで全件リクエストが面倒なので再帰的に全部取るメソッドを用意する

PR予定。とりあえず自前でこうしました。

# custom_base_record.rb

module ZohoHub
class CustomBaseRecord < ZohoHub::BaseRecord
  # recursive call
  def self.all_recursively(
      params: {},
      page: DEFAULT_PAGE)
    params[:page] = page
    params[:per_page] = DEFAULT_RECORDS_PER_PAGE

    body = get(request_path, params)
    response = build_response(body)

    data = response.nil? ? [] : response.data

    if response.info[:more_records]
      data.map { |json| new(json) } +
        all_recursively(params: params, page: page + 1)
    else
      data.map { |json| new(json) }
    end
  end

  def self.build_response(body)
    response = CustomResponse.new(body)

    raise InvalidTokenError, response.msg if response.invalid_token?
    raise RecordInvalid, response.msg if response.invalid_data?
    raise InvalidModule, response.msg if response.invalid_module?
    raise NoPermission, response.msg if response.no_permission?
    raise MandatoryNotFound, response.msg if response.mandatory_not_found?
    raise RecordInBlueprint, response.msg if response.record_in_blueprint?

    response
  end
end
end
# custom_response.rb

module ZohoHub
class CustomResponse < ZohoHub::Response
  def info
    info = @params[:info] if @params.dig(:info)
    info || @params
  end
end
end

わりと適当です。末尾再帰じゃないし。改良したい人はどうぞ。
CustomResponseはinfoを見るようにするために必要です。
これを作ったらclass Account < Zoho::CustomBaseRecordというように継承元を変えて、all_recursivelyを呼べばOKです。

zoho_hubでbulk insert/update したい

zoho_hubでBaseRecordを使う方法を書きました。BaseRecordを使う場合はsaveメソッドを呼ぶことでPOSTまたはPUTすることができます。この方法だと1件の更新なので、対象とするレコードが500件とかあると大変です。
(500回 HTTP Requestするので遅いし、それだけAPI creditも使います。API creditはアカウントの契約状態によりますが、5000-15000/24hくらいあると思っておけばいいです。無茶な使い方をすると制限にひっかかります。)

なので、できればbulk insert/updateを行いたいところです。これは考慮されていて、以下で確認することができます。

Bulk API
https://www.zoho.com/crm/developer/docs/api/bulk-write/overview.html
Insert Records
https://www.zoho.com/crm/developer/docs/api/insert-records.html

Bulk APIはcsvを作成し、アップロードし、そのcsvを取り込むjobを作るリクエストを行う、というちょっと面倒だけど強力なものです。一度に25000件まで行けるようですね。自分は使ってません。

自分はInsert Records(Update Records)の方を使いました。
使い方は簡単で、data配列に複数のデータを詰めてtriggerを付けてPOSTするだけです(更新ならPUT)
詳細はリンク先でサンプルを見れます。

$ curl "https://www.zohoapis.com/crm/v2/Leads"
 -H "Authorization: Zoho-oauthtoken 1000.8cb99dxxxxxxxxxxxxx9be93.9b8xxxxxxxxxxxxxxxf"
 -d "@newlead.json"
 -X POST

これをzoho_hub経由でやります。以下のようにやればOKです。

# Accountの例
class Account
  attribute :id, :name, :employees #etc

  attribute_translation(id: :id)
end

arr = Account.all
ZohoHub.connection.put(
  'Accounts',
  {data: arr.map(&:to_params), trigger: 'approval'}.to_json)

BaseRecordにto_paramsというメソッドがあるのでこれでまずHashに変換します。その後、必要なtriggerやdata keyを付けた完全なHashを作り、最後にjsonにしてPUTでリクエストしています。(PUTの場合、data内の各レコードにidフィールドが必要。更新なので当たり前ですね。POSTの場合は不要です。)

注意点はattribute_translation(id: :id)で、これがないとjson内で “Id”:“12345” というように大文字になってしまいます。小文字でないとエラーが出ます。

where(APIでは/search?)を使う時は()のエスケープに注意

# ruby code
Account.where(criteria: 'Account_Name:starts_with:(æ ª)abc')

一見うまくいきそうな検索ですが、これはエラーになります。

TypeError (no implicit conversion of Symbol into Integer)

これは()をエスケープしていないためです。

When you use special characters like parentheses or comma in the value for a criteria, you must escape them using a back slash. Example: (Last_Name:starts_with:ABC**\(inc\)), (Last_Name:starts_with:Patricia\,**Boyle)

https://www.zoho.com/crm/developer/docs/api/search-records.html

()と,はエスケープが必要なようですね。

というわけで以下は成功します。

# ruby code
Account.where(criteria: 'Account_Name:starts_with:\(æ ª\)abc')

ちなみにzoho_hubでは内部的にURL encodeされてリクエストが投げられるので、curlなどでリクエストする時は自分でURL encodeする必要があります。
(は%28
)は%29
\は%5C
,は%2C
です。

以上です。

再帰的な全件取得とbulk insertはwrapper libraryが持ってて良い機能だと思うので、そのうちPRしてみようかと思います。

Zoho APIを利用するみなさんの助けになれば幸いです。終わり。