hubspot sdk
searchAPI
memo
- filterで
IN
を利用する場合にはvalues
配列が空だとエラーになる(400 -
properties
やsorts
に存在しないプロパティ名を指定するとエラーになる(400
OR
複数のフィルター条件を指定するには、filterGroups内でfiltersを次のようにグループ化できます。
「AND」ロジックを適用するには、1つのfilters内に複数の条件リストをカンマで区切って並べます。
「OR」ロジックを適用するには、filterGroup内に複数のfiltersで分けて条件を並べます。
https://developers.hubspot.jp/docs/api/crm/search
Deal
新しい取引レコードを作成する際は、dealnameおよびdealstageプロパティーをリクエストに含める必要があります。また、複数のパイプラインがある場合はpipelineも含めます。パイプラインが指定されていない場合は、既定のパイプラインが使用されます。
https://developers.hubspot.jp/docs/api/crm/deals
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"
}
}'
Docs
Get Portal ID
OAuth
Properties
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が必要。バリデーションエラーになる
custom workflow action
webhook test endpoint
一時的なテストエンドポイントとして利用できる
rawなwebhookを送信する場合もカスタムアクションのactionUrl
にリクエストを送信する場合も、署名の検証は変わらないっぽい
(webhookでリクエストヘッダーを確認する限り)
署名の検証手順は以下の通り
node sdkならutilがあるのでそれを使った方が良さそう
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"
}
}
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":{
}
}
入力フィールド
入力フィールドの定義は、以下の形式に従います。
- name:入力フィールドの内部名。フィールドのラベルとは異なります。UIに表示されるラベルは、カスタムアクション定義のlabelsセクション使用して定義する必要があります。
- type:入力に必要な値の型。
- fieldType:入力フィールドをUIでレンダリングする方法。入力フィールドは、CRMプロパティーと同様にレンダリングされます。有効なtypeとfieldTypeの組み合わせについて詳細をご確認ください。
- supportedValueTypesに有効な値は2つあります。
- OBJECT_PROPERTY:ユーザーが登録されたオブジェクトからプロパティーを選択するか、フィールドの値として使用する、先行アクションによる出力を選択できます。
- STATIC_VALUE:上記以外の場合は常にこの値を使用します。これは、ユーザー自身が値を入力する必要があることを意味します。
- isRequired:ユーザーが入力フィールドの値を入力することが必須かどうかを指定します。
入力フィールドの定義は、次の形式になります。
label
ラベルの反映までラグ?があるっぽい
apiで作成後しばらくは、revision-definitionid
のような形式で表示される
タイムラグじゃないケースもあるっぽいが不明
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文字ずつの入力でエンドポイントへのリクエストが発火してしまうっぽい。
Export api
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.
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"}}
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.
to handle CSV
Export apiでダウンロードできるCSVはダブルクオーテーションで値が囲われているので、fsを使って文字列と改行でCSVを解析使用すると"
の除去が手間。csv-parse
を使ってクオーテーションの処理を任せてしまった方がバグレス。
Markeing List
v1 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
}
v3
export apiでv1のidが使えないのでlidを利用できるV3を使う
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);
}