Open29

hubspot sdk

nicopinnicopin

searchAPI

memo

  • filterでINを利用する場合にはvalues配列が空だとエラーになる(400
  • propertiessortsに存在しないプロパティ名を指定するとエラーになる(400
nicopinnicopin

OR

複数のフィルター条件を指定するには、filterGroups内でfiltersを次のようにグループ化できます。
「AND」ロジックを適用するには、1つのfilters内に複数の条件リストをカンマで区切って並べます。
「OR」ロジックを適用するには、filterGroup内に複数のfiltersで分けて条件を並べます。
https://developers.hubspot.jp/docs/api/crm/search

nicopinnicopin

Deal

新しい取引レコードを作成する際は、dealnameおよびdealstageプロパティーをリクエストに含める必要があります。また、複数のパイプラインがある場合はpipelineも含めます。パイプラインが指定されていない場合は、既定のパイプラインが使用されます。
https://developers.hubspot.jp/docs/api/crm/deals

nicopinnicopin

create contact

 curl --request POST \
  --url https://api.hubapi.com/crm/v3/objects/contacts \
  --header 'authorization: Bearer OAUTH_OR_API_TOKEN' \
  --header 'content-type: application/json' \
  --data '{
  "associations": [],
  "properties": {
    "email": "example@example.com",
    "firstname": "John",        
    "lastname": "Doe",         
    "website": "http://www.example.com",
    "company": "Example Company",
    "phone": "555-122-2323",
    "address": "123 Elm St",
    "city": "Somewhere",
    "state": "IL",
    "zip": "12345"
  }
}'
nicopinnicopin
nicopinnicopin

bool field

{
	description: 'Friend Status',
	fieldType: 'booleancheckbox',
	groupName: GROUP_NAME,
	label: 'Friend Status',
	name: `friend_status`,
	options: [
		{
			hidden: false,
			label: '友達',
			value: 'true',
		},
		{
			hidden: false,
			label: '友達解除',
			value: 'false',
		},
	],
	type: 'bool',
},

bool値フィールドを使うときにはoptionが必要。バリデーションエラーになる

nicopinnicopin
nicopinnicopin

rawなwebhookを送信する場合もカスタムアクションのactionUrl にリクエストを送信する場合も、署名の検証は変わらないっぽい
(webhookでリクエストヘッダーを確認する限り)
署名の検証手順は以下の通り
https://developers.hubspot.jp/docs/api/webhooks/validating-requests#v3-

node sdkならutilがあるのでそれを使った方が良さそう
https://github.com/HubSpot/hubspot-api-nodejs/blob/master/src/utils/signature.ts#L7

nicopinnicopin

payload

portalIdがあるのでそれでどのアカウントからなのか識別できる

// {
  "callbackId": "ap-102670506-56776413549-7-0",
  "origin": {
    "portalId": 102670506,
    "actionDefinitionId": 10646377,
    "actionDefinitionVersion": 1
  },
  "context": {
    "source": "WORKFLOWS",
    "workflowId": 192814114
  },
  "object": {
    "objectId": 904,
    "properties": {
      "email": "ajenkenbb@gnu.org"
    },
    "objectType": "CONTACT"
  },
  "inputFields": {
    "staticInput": "My Static Input",
    "objectInput": "995",
    "optionsInput": "1"
  }
}
nicopinnicopin

external fetch

headerはworkflowのactionUrlに送るものと同様

{
   "origin":{
      "portalId":45468877,
      "actionDefinitionId":58539609,
      "actionDefinitionVersion":1,
      "actionExecutionIndexIdentifier":null,
      "extensionDefinitionId":58539609,
      "extensionDefinitionVersionId":1
   },
   "inputFieldName":"optionsInput",
   "fetchOptions":{
      "q":""
   },
   "objectTypeId":"0-1",
   "fields":{
      
   },
   "portalId":45468877,
   "extensionDefinitionId":58539609,
   "extensionsDefinitionVersion":1,
   "inputFields":{
      
   }
}
nicopinnicopin

入力フィールド
入力フィールドの定義は、以下の形式に従います。

  • name:入力フィールドの内部名。フィールドのラベルとは異なります。UIに表示されるラベルは、カスタムアクション定義のlabelsセクション使用して定義する必要があります。
  • type:入力に必要な値の型。
  • fieldType:入力フィールドをUIでレンダリングする方法。入力フィールドは、CRMプロパティーと同様にレンダリングされます。有効なtypeとfieldTypeの組み合わせについて詳細をご確認ください。
  • supportedValueTypesに有効な値は2つあります。
    • OBJECT_PROPERTY:ユーザーが登録されたオブジェクトからプロパティーを選択するか、フィールドの値として使用する、先行アクションによる出力を選択できます。
    • STATIC_VALUE:上記以外の場合は常にこの値を使用します。これは、ユーザー自身が値を入力する必要があることを意味します。
  • isRequired:ユーザーが入力フィールドの値を入力することが必須かどうかを指定します。
    入力フィールドの定義は、次の形式になります。
nicopinnicopin

External options fetch

初期のオプションリクエストでは、afterパラメータは含まれていません。これは、オプションリストの最初のページを取得するためのリクエストです。
レスポンスにafterパラメータが含まれる場合、これはさらに読み込むためのオプションが存在することを意味します。HubSpotのワークフローUIは「Load More」ボタンを表示し、ユーザーがこれをクリックするとafterパラメータをリクエストペイロードに含めて次のページのオプションをフェッチします。
searchableプロパティがtrueに設定されている場合、ワークフローUIは検索フィールドを表示します。ユーザーが検索クエリを入力すると、その検索語でオプションが再フェッチされます。この検索クエリはリクエストペイロードのfetchOptions.qに含まれます。

      {
          "typeDefinition": {
            "name": "targetTemplateId",
            "type": "enumeration",
            "fieldType": "select",
            "optionsUrl": "/api/webhook/options/template"
          },
          "supportedValueTypes": [
            "STATIC_VALUE"
          ],
          "searchable": true
        },
//
{
  "origin": {
    // The customer's portal ID
    "portalId": 1,

    // Your custom action definition ID
    "actionDefinitionId": 2,

    // Your custom action definition version
    "actionDefinitionVersion": 3
  },

   // The input field you are fetching options for 
  "inputFieldName": "optionsInput",

  // Your configured external data field webhook URL
  "webhookUrl": "https://myapi.com/hubspot/widget-sizes",

   // The values for the fields that have already been filled out by the workflow user
  "inputFields": {
    "widgetName": {
      "type": "OBJECT_PROPERTY",
      "propertyName": "widget_name"
    },
    "widgetColor": {
      "type": "STATIC_VALUE",
      "value": "blue"
    },
    
    "fetchOptions": {
      // The search query provided by the user. This should be used to filter the returned
      // options. This will only be included if the previous option fetch returned
      // `searchable: true` and the user has entered a search query.
      "q": "option label",

      // The pagination cursor. This will be the same pagination cursor that was returned by
      // the previous option fetch; it can be used to keep track of which options have already
      // been fetched.
      "after": "1234="
    }
  }
}

試すとわかるがフォームの変更に基づいてフェッチするらしく、テキストエリアなどのフィールドがあると1文字ずつの入力でエンドポイントへのリクエストが発火してしまうっぽい。

nicopinnicopin

Export api

https://developers.hubspot.com/docs/api/crm/exports

Limits

  • When setting filters for your export, you can include a maximum of three filterGroups with up to three filters in each group.
  • You can complete up to thirty exports within a rolling 24 hour window, and one export at a time. Additional exports will be queued until the previous export is completed.
nicopinnicopin

Request export


 curl --request POST \
  --url https://api.hubapi.com/crm/v3/exports/export/async \
  --header 'authorization: Bearer YOUR_ACCESS_TOKEN' \
  --header 'content-type: application/json' \
  --data '{
  "exportType": "VIEW",
  "format": "CSV",
  "exportName": "export_debug",
  "objectProperties": ["email"],
  "objectType": "CONTACT",
  "language": "JA"
}'

Response

{"id":"TASK_ID","links":{"status":"https://api-na1.hubspot.com/crm/v3/exports/export/async/tasks/TASK_ID/status"}}
nicopinnicopin

Check Result

curl --request GET \                        
  --url https://api.hubapi.com/crm/v3/exports/export/async/tasks/TASK_ID/status \
  --header 'authorization: Bearer YOUR_ACCESS_TOKEN'

Response

{"status":"COMPLETE","result":"EXPORT_FILE_URL","completedAt":"2024-04-02T00:17:36.877Z"}

Please note: prior to expiration, an export's download URL can be accessed without any additional authorization. To protect your data, proceed with caution when sharing a URL or integrating with HubSpot via this API.

nicopinnicopin

to handle CSV

Export apiでダウンロードできるCSVはダブルクオーテーションで値が囲われているので、fsを使って文字列と改行でCSVを解析使用すると"の除去が手間。csv-parseを使ってクオーテーションの処理を任せてしまった方がバグレス。

nicopinnicopin
nicopinnicopin

v1 get lists

https://legacydocs.hubspot.com/docs/methods/lists/get_lists

curl -X GET "https://api.hubapi.com/contacts/v1/lists" \
     -H "Authorization: Bearer ACCESS_TOKEN" \
     -H "Content-Type: application/json"
{"offset":1,"total":null,"lists":[],"has-more":false}
{
  "offset": 34,
  "total": null,
  "lists": [
    {
      "portalId": 1234567,
      "listId": 25,
      "createdAt": 1712840257934,
      "updatedAt": 1712840263562,
      "name": "first",
      "listType": "DYNAMIC",
      "authorId": 1234567,
      "filters": [],
      "metaData": {
        "size": 2,
        "lastSizeChangeAt": 1712840264208,
        "processing": "DONE",
        "lastProcessingStateChangeAt": 1712840264409,
        "error": "",
        "listReferencesCount": null,
        "parentFolderId": null
      },
      "archived": false,
      "teamIds": [],
      "ilsFilterBranch": "{\"filterBranchOperator\":\"OR\",\"filters\":[],\"filterBranches\":[{\"filterBranchOperator\":\"AND\",\"filters\":[{\"filterType\":\"PROPERTY\",\"property\":\"cci_line_user_id\",\"operation\":{\"propertyType\":\"alltypes\",\"operator\":\"IS_KNOWN\",\"defaultValue\":null,\"includeObjectsWithNoValueSet\":false,\"pruningRefineBy\":null,\"coalescingRefineBy\":{\"setType\":\"ANY\",\"type\":\"SetOccurrencesRefineBy\"},\"operationType\":\"alltypes\",\"operatorName\":\"IS_KNOWN\"},\"frameworkFilterId\":null}],\"filterBranches\":[],\"filterBranchType\":\"AND\"}],\"filterBranchType\":\"OR\"}",
      "readOnly": false,
      "dynamic": true,
      "internal": false,
      "limitExempt": false
    }
  ],
  "has-more": true
}
nicopinnicopin

Signature.isValid

nestjsでmiddlewareを使うとgetリクエストのreq.bodyで参照するとからオブジェクトを返すが、これを署名検証のハッシュ生成にrequestBodyとして渡すとハッシュが一致しない。
仕様が不明瞭だけでワークアラウンドとして、GETなどリクエストボディがない場合には、空文字列を渡すようにしておく(NodeのSDKでは内部的に文字列の加算の処理をしているので)

const bodyStr = this.getBodyStrForValidation(req);
		const isValid = Signature.isValid({
			clientSecret: this.configService.get('HUBSPOT_PUBLIC_APP_SECRET'),
			method: req.method,
			requestBody: bodyStr,
			signature,
			signatureVersion: 'v3',
			timestamp,
			url,
		});
/**
	 * get body string for validation
	 * @param req
	 */
	getBodyStrForValidation(req: Request) {
		if (['GET', 'DELETE'].includes(req.method)) {
			return '';
		}
		return JSON.stringify(req.body);
	}